feat: 迁移前端资源文件并重构项目结构

refactor: 优化前端代码结构和资源管理

style: 调整前端代码格式和样式

chore: 更新.gitignore和构建配置

fix: 修复前端资源路径和引用问题

docs: 更新前端文档和注释

perf: 优化前端性能和加载速度

test: 更新前端测试用例

build: 调整前端构建配置

ci: 更新CI/CD配置
This commit is contained in:
zhangtao
2026-05-09 00:46:43 +08:00
parent 1bd125d99b
commit bf81480ada
967 changed files with 21018 additions and 83019 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -95,14 +95,14 @@ const nodeClass = computed(() => {
width: 80px !important;
height: 80px !important;
padding: 0 !important;
color: #ffffff !important;
color: #fff !important;
cursor: pointer !important;
background: linear-gradient(135deg, #67c23a 0%, #5daf34 100%) !important;
border: 3px solid #5daf34 !important;
border-radius: 50% !important;
box-shadow:
0 6px 12px rgba(103, 194, 58, 0.4),
0 2px 4px rgba(0, 0, 0, 0.1) !important;
0 6px 12px rgb(103 194 58 / 40%),
0 2px 4px rgb(0 0 0 / 10%) !important;
}
.vue-flow__node-output {
@@ -112,14 +112,14 @@ const nodeClass = computed(() => {
width: 80px !important;
height: 80px !important;
padding: 0 !important;
color: #ffffff !important;
color: #fff !important;
cursor: pointer !important;
background: linear-gradient(135deg, #f56c6c 0%, #e04e4e 100%) !important;
border: 3px solid #e04e4e !important;
border-radius: 50% !important;
box-shadow:
0 6px 12px rgba(245, 108, 108, 0.4),
0 2px 4px rgba(0, 0, 0, 0.1) !important;
0 6px 12px rgb(245 108 108 / 40%),
0 2px 4px rgb(0 0 0 / 10%) !important;
}
.dynamic-node {
@@ -127,7 +127,7 @@ const nodeClass = computed(() => {
cursor: pointer;
border: 2px solid #409eff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
box-shadow: 0 2px 8px rgb(64 158 255 / 20%);
}
.vue-flow__node-input .dynamic-node,
@@ -8,49 +8,22 @@
</div>
<div class="panel-content">
<ElForm :model="formData" label-width="80px" size="small">
<ElFormItem label="连线名称">
<ElInput v-model="formData.label" placeholder="请输入连线名称" />
</ElFormItem>
<ElFormItem label="连线类型">
<ElSelect v-model="formData.type" placeholder="请选择连线类型">
<ElOption label="折线" value="smoothstep" />
<ElOption label="曲线" value="default" />
<ElOption label="直线" value="straight" />
</ElSelect>
</ElFormItem>
<ElFormItem label="连线颜色">
<ArtForm
v-model="formData"
:items="edgeFormItems"
label-width="80px"
size="small"
label-position="right"
:span="24"
:gutter="12"
:show-reset="false"
:show-submit="false"
class="panel-art-form"
>
<template #color>
<ElColorPicker v-model="formData.color" />
</ElFormItem>
<ElFormItem label="线条宽度">
<ElInputNumber v-model="formData.strokeWidth" :min="1" :max="10" />
</ElFormItem>
<ElFormItem label="启用动画">
<ElSwitch v-model="formData.animated" />
</ElFormItem>
<ElFormItem label="条件表达式">
<ElInput
v-model="formData.condition"
type="textarea"
:rows="3"
placeholder="请输入条件表达式"
/>
</ElFormItem>
<ElFormItem label="描述">
<ElInput
v-model="formData.description"
type="textarea"
:rows="2"
placeholder="请输入描述信息"
/>
</ElFormItem>
</ElForm>
</template>
</ArtForm>
<div class="panel-actions">
<ElButton type="primary" size="small" @click="handleSave">保存</ElButton>
@@ -61,21 +34,10 @@
</template>
<script setup>
import { ref, watch } from "vue";
import {
ElButton,
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElInputNumber,
ElSwitch,
ElColorPicker,
ElMessage,
ElIcon,
} from "element-plus";
import { ref, watch, computed } from "vue";
import { ElButton, ElColorPicker, ElMessage, ElIcon } from "element-plus";
import { Close } from "@element-plus/icons-vue";
import ArtForm from "@/components/Core/forms/art-form/index.vue";
const props = defineProps({
edge: {
@@ -96,6 +58,72 @@ const formData = ref({
description: props.edge?.data?.description || "",
});
const edgeFormItems = computed(() => [
{
label: "连线名称",
key: "label",
type: "input",
span: 24,
props: { placeholder: "请输入连线名称" },
},
{
label: "连线类型",
key: "type",
type: "select",
span: 24,
props: {
placeholder: "请选择连线类型",
options: [
{ label: "折线", value: "smoothstep" },
{ label: "曲线", value: "default" },
{ label: "直线", value: "straight" },
],
},
},
{
label: "连线颜色",
key: "color",
type: "input",
span: 24,
placeholder: "",
},
{
label: "线条宽度",
key: "strokeWidth",
type: "number",
span: 24,
props: { min: 1, max: 10 },
},
{
label: "启用动画",
key: "animated",
type: "switch",
span: 24,
},
{
label: "条件表达式",
key: "condition",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 3,
placeholder: "请输入条件表达式",
},
},
{
label: "描述",
key: "description",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 2,
placeholder: "请输入描述信息",
},
},
]);
watch(
() => props.edge,
(newEdge) => {
@@ -164,4 +192,16 @@ function handleDelete() {
.panel-actions .el-button {
flex: 1;
}
.panel-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.panel-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
.panel-art-form :deep(section) {
padding: 0;
}
</style>
@@ -8,46 +8,44 @@
</div>
<div class="panel-content">
<ElForm :model="formData" label-width="80px" size="small">
<ElFormItem label="节点类型">
<ArtForm
v-model="formData"
:items="nodePanelFormItems"
label-width="80px"
size="small"
label-position="right"
:span="24"
:gutter="12"
:show-reset="false"
:show-submit="false"
class="panel-art-form"
>
<template #type>
<ElSelect v-model="formData.type" placeholder="请选择节点类型" @change="handleTypeChange">
<ElOption
v-for="type in nodeTypes"
:key="type.id"
:label="type.name"
:value="type.code"
/>
<ElOption v-for="t in nodeTypes" :key="t.id" :label="t.name" :value="t.code" />
</ElSelect>
</ElFormItem>
<ElFormItem label="节点名称">
<ElInput v-model="formData.label" placeholder="请输入节点名称" />
</ElFormItem>
<ElFormItem label="位置参数">
<ElInput v-model="formData.args" placeholder="多个参数用逗号分隔,如: arg1, arg2, arg3" />
<div class="field-hint">多个参数用逗号分隔</div>
</ElFormItem>
<ElFormItem label="关键字参数">
<ElInput
v-model="formData.kwargsStr"
type="textarea"
:rows="4"
placeholder='JSON格式,如: {"key": "value", "count": 10}'
/>
<div class="field-hint">JSON 格式的关键字参数</div>
</ElFormItem>
<ElFormItem label="描述">
<ElInput
v-model="formData.description"
type="textarea"
:rows="2"
placeholder="请输入描述信息"
/>
</ElFormItem>
</ElForm>
</template>
<template #args>
<div>
<ElInput
v-model="formData.args"
placeholder="多个参数用逗号分隔,如: arg1, arg2, arg3"
/>
<div class="field-hint">多个参数用逗号分隔</div>
</div>
</template>
<template #kwargsStr>
<div>
<ElInput
v-model="formData.kwargsStr"
type="textarea"
:rows="4"
placeholder='JSON格式,如: {"key": "value", "count": 10}'
/>
<div class="field-hint">JSON 格式的关键字参数</div>
</div>
</template>
</ArtForm>
<div class="panel-actions">
<ElButton type="primary" size="small" @click="handleSave">保存</ElButton>
@@ -58,18 +56,11 @@
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from "vue";
import {
ElButton,
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElMessage,
ElIcon,
} from "element-plus";
import { ref, watch, onMounted, computed } from "vue";
import { ElButton, ElInput, ElSelect, ElOption, ElMessage, ElIcon } from "element-plus";
import { Close } from "@element-plus/icons-vue";
import ArtForm from "@/components/Core/forms/art-form/index.vue";
import type { FormItem } from "@/components/Core/forms/art-form/index.vue";
import WorkflowNodeTypeAPI, {
type WorkflowNodeTypeOption,
} from "@/api/module_task/workflow/node-type";
@@ -93,6 +84,48 @@ const formData = ref({
description: props.node?.data?.description || "",
});
const nodePanelFormItems = computed<FormItem[]>(() => [
{
label: "节点类型",
key: "type",
type: "input",
span: 24,
placeholder: "",
},
{
label: "节点名称",
key: "label",
type: "input",
span: 24,
props: { placeholder: "请输入节点名称" },
},
{
label: "位置参数",
key: "args",
type: "input",
span: 24,
placeholder: "",
},
{
label: "关键字参数",
key: "kwargsStr",
type: "input",
span: 24,
placeholder: "",
},
{
label: "描述",
key: "description",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 2,
placeholder: "请输入描述信息",
},
},
]);
const loadNodeTypes = async () => {
try {
const res = await WorkflowNodeTypeAPI.getWorkflowNodeTypeOptions();
@@ -216,4 +249,16 @@ onMounted(() => {
.panel-actions .el-button {
flex: 1;
}
.panel-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.panel-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
.panel-art-form :deep(section) {
padding: 0;
}
</style>
@@ -1,5 +1,5 @@
<template>
<el-drawer
<ElDrawer
v-model="dialogVisible"
:title="drawerTitle"
:close-on-click-modal="true"
@@ -7,41 +7,33 @@
class="workflow-drawer"
@close="handleClose"
>
<el-container class="workflow-create-content">
<el-splitter direction="horizontal" style="height: 100%">
<el-splitter-panel size="250px" :min="200" :max="400">
<el-scrollbar style="height: 100%">
<ElContainer class="workflow-create-content">
<ElSplitter direction="horizontal" style="height: 100%">
<ElSplitterPanel size="250px" :min="200" :max="400">
<ElScrollbar style="height: 100%">
<div class="panel-section">
<div class="section-title">基础信息</div>
<el-form
<ArtForm
ref="formRef"
:model="formData"
label-width="50px"
v-model="formData"
:items="workflowBaseFormItems"
:rules="formRules"
label-width="50px"
label-position="right"
size="small"
>
<el-form-item label="编码" prop="code">
<el-input v-model="formData.code" placeholder="请输入流程编码" />
</el-form-item>
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入流程名称" />
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input
v-model="formData.description"
type="textarea"
:rows="2"
placeholder="请输入流程描述"
/>
</el-form-item>
</el-form>
:span="24"
:gutter="12"
:show-reset="false"
:show-submit="false"
class="workflow-base-art-form"
/>
</div>
<el-divider style="margin: 4px 0" />
<ElDivider style="margin: 4px 0" />
<div class="panel-section">
<div class="section-title">节点</div>
<el-input
<ElInput
v-model="searchKeyword"
placeholder="搜索节点名称"
clearable
@@ -49,11 +41,11 @@
class="search-box"
>
<template #prefix>
<el-icon><Search /></el-icon>
<ElIcon><Search /></ElIcon>
</template>
</el-input>
<el-space direction="vertical" :size="8" fill style="width: 100%; margin-top: 8px">
<el-tag
</ElInput>
<ElSpace direction="vertical" :size="8" fill style="width: 100%; margin-top: 8px">
<ElTag
v-for="item in filteredNodes"
:key="item.id"
:type="getCategoryType(item.category) as any"
@@ -67,13 +59,13 @@
<span style="margin-left: 4px; font-size: 10px; opacity: 0.7">
[{{ getCategoryText(item.category) }}]
</span>
</el-tag>
</el-space>
</ElTag>
</ElSpace>
</div>
</el-scrollbar>
</el-splitter-panel>
</ElScrollbar>
</ElSplitterPanel>
<el-splitter-panel>
<ElSplitterPanel>
<div class="canvas-main">
<div class="canvas-container" @click="handleCanvasClick">
<VueFlow
@@ -93,76 +85,76 @@
<Controls />
<Background pattern-color="#aaa" :gap="16" />
<Panel position="top-right" class="workflow-toolbar">
<el-button
<ElButton
class="vue-flow__controls-button"
title="格式化画布"
:icon="Grid"
@click="handleFormatCanvas"
/>
<el-dropdown trigger="click" @command="handleEdgeStyleChange">
<el-button class="vue-flow__controls-button" title="连线样式" :icon="Share" />
<ElDropdown trigger="click" @command="handleEdgeStyleChange">
<ElButton class="vue-flow__controls-button" title="连线样式" :icon="Share" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
<ElDropdownMenu>
<ElDropdownItem
command="bezier"
:class="{ active: edgeStyle === 'bezier' }"
>
平滑曲线
</el-dropdown-item>
<el-dropdown-item
</ElDropdownItem>
<ElDropdownItem
command="smoothstep"
:class="{ active: edgeStyle === 'smoothstep' }"
>
阶梯折线
</el-dropdown-item>
<el-dropdown-item
</ElDropdownItem>
<ElDropdownItem
command="straight"
:class="{ active: edgeStyle === 'straight' }"
>
直线
</el-dropdown-item>
</el-dropdown-menu>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</el-dropdown>
<el-button
</ElDropdown>
<ElButton
class="vue-flow__controls-button"
:title="edgeAnimated ? '关闭动画' : '开启动画'"
:icon="VideoPlay"
@click="handleEdgeAnimatedChange(!edgeAnimated)"
/>
<el-dropdown trigger="click">
<el-button class="vue-flow__controls-button" title="布局方向">
<el-icon><Rank /></el-icon>
</el-button>
<ElDropdown trigger="click">
<ElButton class="vue-flow__controls-button" title="布局方向">
<ElIcon><Rank /></ElIcon>
</ElButton>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
<ElDropdownMenu>
<ElDropdownItem
@click="
layoutDirection = 'LR';
handleLayout();
"
>
横向布局
</el-dropdown-item>
<el-dropdown-item
</ElDropdownItem>
<ElDropdownItem
@click="
layoutDirection = 'TB';
handleLayout();
"
>
纵向布局
</el-dropdown-item>
</el-dropdown-menu>
</ElDropdownItem>
</ElDropdownMenu>
</template>
</el-dropdown>
</ElDropdown>
</Panel>
<MiniMap pannable zoomable />
</VueFlow>
</div>
</div>
</el-splitter-panel>
</ElSplitterPanel>
<el-splitter-panel v-if="updateState" size="320px" :min="280" :max="400">
<ElSplitterPanel v-if="updateState" size="320px" :min="280" :max="400">
<NodeConfigPanel
v-if="updateState === 'node'"
:node="selectedNode"
@@ -177,17 +169,17 @@
@save="handleSaveEdge"
@delete="handleDeleteEdge"
/>
</el-splitter-panel>
</el-splitter>
</el-container>
</ElSplitterPanel>
</ElSplitter>
</ElContainer>
<template #footer>
<div class="drawer-footer">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" @click="handleFinish">保存</el-button>
<ElButton @click="handleClose">取消</ElButton>
<ElButton type="primary" @click="handleFinish">保存</ElButton>
</div>
</template>
</el-drawer>
</ElDrawer>
</template>
<script setup lang="ts">
@@ -209,6 +201,8 @@ import "element-plus/dist/index.css";
import DynamicNode from "./DynamicNode.vue";
import NodeConfigPanel from "./NodeConfigPanel.vue";
import EdgeConfigPanel from "./EdgeConfigPanel.vue";
import ArtForm from "@/components/Core/forms/art-form/index.vue";
import type { FormItem } from "@/components/Core/forms/art-form/index.vue";
import WorkflowDefinitionAPI, {
type WorkflowTable,
type WorkflowForm,
@@ -233,7 +227,7 @@ const props = defineProps({
const emit = defineEmits(["update:visible", "refresh"]);
const formRef = ref();
const formRef = ref<InstanceType<typeof ArtForm> | null>(null);
const workflowId = ref<number>();
const formData = reactive<Partial<WorkflowForm>>({
@@ -247,6 +241,34 @@ const formRules = {
name: [{ required: true, message: "请输入流程名称", trigger: "blur" }],
};
const workflowBaseFormItems = computed<FormItem[]>(() => [
{
label: "编码",
key: "code",
type: "input",
span: 24,
props: { placeholder: "请输入流程编码" },
},
{
label: "名称",
key: "name",
type: "input",
span: 24,
props: { placeholder: "请输入流程名称" },
},
{
label: "描述",
key: "description",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 2,
placeholder: "请输入流程描述",
},
},
]);
const dialogVisible = computed({
get: () => props.visible,
set: (val) => emit("update:visible", val),
@@ -739,7 +761,7 @@ const handleFinish = async () => {
if (!formRef.value) return;
try {
await formRef.value.validate();
await formRef.value.validate?.();
await handleValidate();
await handleSave();
emit("refresh");
@@ -965,7 +987,7 @@ function deleteEdge(edgeId: string, getEdges: () => Edge[], setEdges: (edges: Ed
gap: 4px;
padding: 8px;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
}
.drawer-footer {
@@ -973,4 +995,16 @@ function deleteEdge(edgeId: string, getEdges: () => Edge[], setEdges: (edges: Ed
gap: 12px;
justify-content: flex-end;
}
.workflow-base-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.workflow-base-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
.workflow-base-art-form :deep(section) {
padding: 0;
}
</style>
@@ -1,112 +1,109 @@
<!-- 工作流定义:Art + useTable -->
<template>
<div class="app-container">
<PageSearch
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
<div class="art-full-height">
<ArtSearchBar
v-show="showSearchBar"
ref="searchBarRef"
v-model="searchForm"
:items="workflowSearchItems"
:rules="searchBarRules"
:is-expand="true"
:show-expand="true"
:show-reset="true"
:show-search="true"
:disabled-search="false"
:default-expanded="false"
@search="handleSearchBarSearch"
@reset="onResetSearch"
/>
<PageContent ref="contentRef" :content-config="contentConfig" @add-click="handleCreate">
<template #table="{ data, loading, tableRef, onSelectionChange, pagination }">
<div class="data-table__content">
<el-table
:ref="tableRef as any"
v-loading="loading"
:data="data"
height="100%"
border
stripe
@selection-change="onSelectionChange"
>
<template #empty>
<el-empty :image-size="80" description="暂无数据" />
</template>
<el-table-column type="selection" align="center" min-width="55" />
<el-table-column type="index" fixed label="序号" min-width="60">
<template #default="scope">
{{ (pagination.currentPage - 1) * pagination.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="ID" prop="id" min-width="80" />
<el-table-column label="名称" prop="name" min-width="160" show-overflow-tooltip />
<el-table-column label="编码" prop="code" min-width="120" show-overflow-tooltip />
<el-table-column label="状态" prop="status" min-width="100">
<template #default="scope">
<el-tag :type="getStatusType(scope.row.status) as any">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column
label="描述"
prop="description"
min-width="160"
show-overflow-tooltip
/>
<el-table-column label="创建时间" prop="created_time" min-width="180" />
<ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
<ArtTableHeader
v-model:columns="columnChecks"
v-model:showSearchBar="showSearchBar"
:loading="loading"
@refresh="refreshData"
>
<template #left>
<ArtTableHeaderLeft
:remove-ids="selectedIds"
:perm-create="['module_task:workflow:definition:create']"
:perm-delete="['module_task:workflow:definition:delete']"
:delete-loading="batchDeleting"
@add="handleCreate"
@delete="handleBatchDelete"
/>
</template>
</ArtTableHeader>
<OperationColumn :list-data-length="data.length">
<template #default="scope">
<el-space class="flex">
<el-button
v-if="scope.row.status === 'draft'"
v-hasPerm="['module_task:workflow:definition:update']"
type="success"
size="small"
link
icon="upload"
@click="handlePublish(scope.row)"
>
发布
</el-button>
<el-dropdown
v-if="scope.row.status === 'published'"
v-hasPerm="['module_task:workflow:definition:execute']"
@command="(e: string) => handleExecute(e, scope.row)"
>
<el-button type="warning" size="small" link icon="video-play">
执行
<el-icon><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="execute">立即执行</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button
v-hasPerm="['module_task:workflow:definition:update']"
type="primary"
size="small"
link
icon="edit"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
v-hasPerm="['module_task:workflow:definition:delete']"
type="danger"
size="small"
link
icon="delete"
@click="handleRowDelete(scope.row.id)"
>
删除
</el-button>
</el-space>
<ArtTable
ref="artTableRef"
row-key="id"
:loading="loading"
:data="data"
:columns="columns"
:pagination="paginationBind"
@selection-change="onTableSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
<template #workflow-operation="{ row }">
<ElSpace class="flex">
<ElButton
v-if="row.status === 'draft'"
v-hasPerm="['module_task:workflow:definition:update']"
type="success"
size="small"
link
icon="upload"
@click="handlePublish(row)"
>
发布
</ElButton>
<ElDropdown
v-if="row.status === 'published'"
v-hasPerm="['module_task:workflow:definition:execute']"
@command="(e: string) => handleExecute(e, row)"
>
<ElButton type="warning" size="small" link icon="video-play">
执行
<ElIcon><ArrowDown /></ElIcon>
</ElButton>
<template #dropdown>
<ElDropdownMenu>
<ElDropdownItem command="execute">立即执行</ElDropdownItem>
</ElDropdownMenu>
</template>
</OperationColumn>
</el-table>
</div>
</template>
</PageContent>
</ElDropdown>
<ElButton
v-hasPerm="['module_task:workflow:definition:update']"
type="primary"
size="small"
link
icon="edit"
@click="handleEdit(row)"
>
编辑
</ElButton>
<ElButton
v-hasPerm="['module_task:workflow:definition:delete']"
type="danger"
size="small"
link
icon="delete"
@click="deleteWorkflowRow(row.id)"
>
删除
</ElButton>
</ElSpace>
</template>
</ArtTable>
</ElCard>
<WorkflowDesignDrawer
v-model:visible="createVisible"
:workflow="selectedWorkflow"
@refresh="refreshList"
@refresh="onDrawerRefresh"
/>
</div>
</template>
@@ -117,109 +114,256 @@ defineOptions({
inheritAttrs: false,
});
import { ElMessage, ElMessageBox } from "element-plus";
import WorkflowDefinitionAPI, { type WorkflowTable } from "@/api/module_task/workflow/definition";
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
import ArtTable from "@/components/Core/tables/art-table/index.vue";
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
import { useTable } from "@/hooks/core/useTable";
import type { ColumnOption } from "@/types/component";
import { ArrowDown } from "@element-plus/icons-vue";
import PageSearch from "@/components/CURD/PageSearch.vue";
import PageContent from "@/components/CURD/PageContent.vue";
import type { IContentConfig, ISearchConfig } from "@/components/CURD/types";
import { useCrudList } from "@/components/CURD/useCrudList";
import OperationColumn from "@/components/OperationColumn/index.vue";
import WorkflowDefinitionAPI, {
type WorkflowTable,
type WorkflowPageQuery,
} from "@/api/module_task/workflow/definition";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import { computed, h, ref } from "vue";
import WorkflowDesignDrawer from "../components/WorkflowDesignDrawer.vue";
import { onMounted, reactive, ref } from "vue";
const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
const BATCH_DELETE_MSG = "确认删除选中的工作流吗?";
const selectedWorkflow = ref<WorkflowTable>();
const createVisible = ref(false);
type WorkflowSearchForm = {
name?: string;
code?: string;
status?: string;
};
const searchConfig = reactive<ISearchConfig>({
permPrefix: "module_task:workflow:definition",
colon: true,
isExpandable: true,
showNumber: 2,
form: { labelWidth: "auto" },
formItems: [
{
prop: "name",
label: "流程名称",
type: "input",
attrs: { placeholder: "请输入流程名称", clearable: true },
},
{
prop: "code",
label: "流程编码",
type: "input",
attrs: { placeholder: "请输入流程编码", clearable: true },
},
{
prop: "status",
label: "状态",
type: "select",
function buildWorkflowReplaceParams(u: WorkflowSearchForm): Record<string, unknown> {
return {
name: u.name,
code: u.code,
status: u.status,
};
}
const searchForm = ref<WorkflowSearchForm>({
name: undefined,
code: undefined,
status: undefined,
});
const showSearchBar = ref(true);
const searchBarRef = ref<InstanceType<typeof ArtSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {};
const workflowSearchItems = computed<SearchFormItem[]>(() => [
{
label: "流程名称",
key: "name",
type: "input",
placeholder: "请输入流程名称",
clearable: true,
span: 6,
},
{
label: "流程编码",
key: "code",
type: "input",
placeholder: "请输入流程编码",
clearable: true,
span: 6,
},
{
label: "状态",
key: "status",
type: "select",
props: {
placeholder: "请选择状态",
clearable: true,
options: [
{ label: "草稿", value: "draft" },
{ label: "已发布", value: "published" },
{ label: "已归档", value: "archived" },
],
attrs: { placeholder: "请选择状态", clearable: true, style: { width: "170px" } },
},
],
});
span: 6,
},
]);
function normalizeWorkflowQuery(params: Record<string, unknown>): WorkflowPageQuery {
const p = { ...params } as Record<string, unknown>;
if (p.status === "" || p.status === null) p.status = undefined;
return p as unknown as WorkflowPageQuery;
const artTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<WorkflowTable[]>([]);
const selectedIds = computed(() =>
selectedRows.value.map((r) => r.id).filter((id): id is number => typeof id === "number")
);
const batchDeleting = ref(false);
function onTableSelectionChange(rows: WorkflowTable[]) {
selectedRows.value = rows;
}
const contentConfig = reactive<IContentConfig<WorkflowPageQuery>>({
permPrefix: "module_task:workflow:definition",
title: "工作流管理",
tooltip: "流程编排列表,支持发布与执行",
cols: [],
hideColumnFilter: true,
toolbar: [
{ name: "add", text: "新增", attrs: { icon: "plus", type: "success" }, perm: "create" },
"delete",
],
defaultToolbar: ["refresh"],
initialFetch: false,
pagination: {
pageSize: 10,
pageSizes: [10, 20, 30, 50],
},
request: { page_no: "page_no", page_size: "page_size" },
indexAction: async (params) => {
const res = await WorkflowDefinitionAPI.getWorkflowList(
normalizeWorkflowQuery(params as unknown as Record<string, unknown>)
);
return {
total: res.data.data.total,
list: res.data.data.items,
};
},
deleteAction: (ids) =>
WorkflowDefinitionAPI.deleteWorkflow(
ids
.split(",")
.map((s) => Number(s.trim()))
.filter((n) => !Number.isNaN(n) && n > 0)
),
deleteConfirm: {
title: "警告",
message: "确认删除选中的工作流吗?",
type: "warning",
},
});
function handleRowDelete(id?: number) {
function deleteWorkflowRow(id: number | undefined) {
if (id == null) return;
contentRef.value?.handleDelete(id);
ElMessageBox.confirm("确认删除该工作流吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await WorkflowDefinitionAPI.deleteWorkflow([id]);
ElMessage.success("删除成功");
artTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {});
}
function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(BATCH_DELETE_MSG, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await WorkflowDefinitionAPI.deleteWorkflow(ids);
ElMessage.success("删除成功");
selectedRows.value = [];
await refreshRemove();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
}
function getStatusType(status: string): "success" | "warning" | "info" | "danger" | "primary" {
const typeMap: Record<string, "success" | "warning" | "info" | "danger" | "primary"> = {
draft: "info",
published: "success",
archived: "warning",
};
return typeMap[status] ?? "info";
}
function getStatusText(status: string) {
const textMap: Record<string, string> = {
draft: "草稿",
published: "已发布",
archived: "已归档",
};
return textMap[status] || status;
}
const {
columns,
columnChecks,
data,
loading,
pagination,
getData,
replaceSearchParams,
resetSearchParams,
handleSizeChange,
handleCurrentChange,
refreshData,
refreshRemove,
refreshUpdate,
} = useTable({
core: {
apiFn: WorkflowDefinitionAPI.getWorkflowList,
apiParams: {
page_no: 1,
page_size: 10,
},
columnsFactory: (): ColumnOption<WorkflowTable>[] => [
{ type: "selection", width: 48, fixed: "left" },
{ type: "globalIndex", width: 56, label: "序号" },
{
prop: "id",
label: "ID",
width: 88,
align: "center",
},
{
prop: "name",
label: "名称",
minWidth: 160,
showOverflowTooltip: true,
},
{
prop: "code",
label: "编码",
minWidth: 120,
showOverflowTooltip: true,
},
{
prop: "status",
label: "状态",
minWidth: 100,
align: "center",
formatter: (row) =>
h(ElTag, { type: getStatusType(row.status ?? "") }, () =>
getStatusText(row.status ?? "")
),
},
{
prop: "description",
label: "描述",
minWidth: 160,
showOverflowTooltip: true,
},
{
prop: "created_time",
label: "创建时间",
minWidth: 180,
showOverflowTooltip: true,
},
{
prop: "operation",
label: "操作",
width: 260,
fixed: "right",
align: "center",
useSlot: true,
slotName: "workflow-operation",
},
],
},
});
const paginationBind = computed(() => {
const p = pagination as unknown as {
current?: number;
size?: number;
total?: number;
page_no?: number;
page_size?: number;
};
return {
current: p.current ?? p.page_no ?? 1,
size: p.size ?? p.page_size ?? 10,
total: p.total ?? 0,
};
});
async function handleSearchBarSearch(params: WorkflowSearchForm) {
await searchBarRef.value?.validate?.();
replaceSearchParams(buildWorkflowReplaceParams(params));
getData();
}
async function onResetSearch() {
searchForm.value = {
name: undefined,
code: undefined,
status: undefined,
};
await resetSearchParams();
}
const selectedWorkflow = ref<WorkflowTable>();
const createVisible = ref(false);
function handleCreate() {
selectedWorkflow.value = undefined;
createVisible.value = true;
@@ -230,22 +374,8 @@ function handleEdit(record: WorkflowTable) {
createVisible.value = true;
}
function getStatusType(status: string) {
const typeMap: Record<string, string> = {
draft: "info",
published: "success",
archived: "warning",
};
return typeMap[status] || "";
}
function getStatusText(status: string) {
const textMap: Record<string, string> = {
draft: "草稿",
published: "已发布",
archived: "已归档",
};
return textMap[status] || status;
function onDrawerRefresh() {
void refreshUpdate();
}
function handlePublish(record: WorkflowTable) {
@@ -262,7 +392,7 @@ function handlePublish(record: WorkflowTable) {
}
await WorkflowDefinitionAPI.publishWorkflow(record.id, {});
ElMessage.success("发布成功");
refreshList();
await refreshUpdate();
} catch {
ElMessage.error("发布失败");
}
@@ -291,17 +421,13 @@ function handleExecute(action: string, record: WorkflowTable) {
const result = res.data.data;
ElMessage.success(`工作流执行${result.status === "completed" ? "成功" : "失败"}`);
}
refreshList();
await refreshUpdate();
} catch {
ElMessage.error("执行失败");
}
})
.catch(() => {});
}
onMounted(() => {
refreshList();
});
</script>
<style scoped lang="scss"></style>
@@ -1,135 +1,105 @@
<!-- 工作流节点类型:Art + useTable -->
<template>
<div class="app-container">
<PageSearch
ref="searchRef"
:search-config="searchConfig"
@query-click="handleQueryClick"
@reset-click="handleResetClick"
<div class="art-full-height">
<ArtSearchBar
v-show="showSearchBar"
ref="searchBarRef"
v-model="searchForm"
:items="nodeTypeSearchItems"
:rules="searchBarRules"
:is-expand="true"
:show-expand="true"
:show-reset="true"
:show-search="true"
:disabled-search="false"
:default-expanded="false"
@search="handleSearchBarSearch"
@reset="onResetSearch"
/>
<PageContent ref="contentRef" :content-config="contentConfig" @add-click="openDialog()">
<template #table="{ data, loading, tableRef, onSelectionChange, pagination }">
<div class="data-table__content">
<el-table
:ref="tableRef as any"
v-loading="loading"
:data="data"
height="100%"
border
stripe
@selection-change="onSelectionChange"
>
<template #empty>
<el-empty :image-size="80" description="暂无数据" />
</template>
<el-table-column type="selection" align="center" min-width="55" />
<el-table-column type="index" fixed label="序号" min-width="60">
<template #default="scope">
{{ (pagination.currentPage - 1) * pagination.pageSize + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column label="ID" prop="id" min-width="70" />
<el-table-column label="名称" prop="name" min-width="140" show-overflow-tooltip />
<el-table-column label="编码" prop="code" min-width="120" show-overflow-tooltip />
<el-table-column label="分类" prop="category" min-width="100">
<template #default="scope">
{{ categoryLabel(scope.row.category) }}
</template>
</el-table-column>
<el-table-column label="排序" prop="sort_order" min-width="80" />
<el-table-column label="启用" prop="is_active" min-width="80">
<template #default="scope">
<el-tag :type="scope.row.is_active ? 'success' : 'info'">
{{ scope.row.is_active ? "是" : "否" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" prop="created_time" min-width="170" />
<ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
<ArtTableHeader
v-model:columns="columnChecks"
v-model:showSearchBar="showSearchBar"
:loading="loading"
@refresh="refreshData"
>
<template #left>
<ArtTableHeaderLeft
:remove-ids="selectedIds"
:perm-create="['module_task:workflow:node-type:create']"
:perm-delete="['module_task:workflow:node-type:delete']"
:delete-loading="batchDeleting"
@add="openDialog()"
@delete="handleBatchDelete"
/>
</template>
</ArtTableHeader>
<OperationColumn :list-data-length="data.length">
<template #default="scope">
<el-space class="flex">
<el-button
v-hasPerm="['module_task:workflow:node-type:update']"
type="primary"
size="small"
link
icon="edit"
@click="openDialog(scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPerm="['module_task:workflow:node-type:delete']"
type="danger"
size="small"
link
icon="delete"
@click="handleRowDelete(scope.row.id)"
>
删除
</el-button>
</el-space>
</template>
</OperationColumn>
</el-table>
</div>
</template>
</PageContent>
<ArtTable
ref="artTableRef"
row-key="id"
:loading="loading"
:data="data"
:columns="columns"
:pagination="paginationBind"
@selection-change="onTableSelectionChange"
@pagination:size-change="handleSizeChange"
@pagination:current-change="handleCurrentChange"
>
<template #node-type-operation="{ row }">
<ElSpace class="flex">
<ElButton
v-hasPerm="['module_task:workflow:node-type:update']"
type="primary"
size="small"
link
icon="edit"
@click="openDialog(row.id)"
>
编辑
</ElButton>
<ElButton
v-hasPerm="['module_task:workflow:node-type:delete']"
type="danger"
size="small"
link
icon="delete"
@click="deleteNodeTypeRow(row.id)"
>
删除
</ElButton>
</ElSpace>
</template>
</ArtTable>
</ElCard>
<EnhancedDialog
<ArtDialog
v-model="dialogVisible"
:title="dialogTitle"
width="720px"
destroy-on-close
@close="handleCloseDialog"
>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" maxlength="128" show-word-limit />
</el-form-item>
<el-form-item label="编码" prop="code">
<el-input v-model="form.code" maxlength="64" show-word-limit :disabled="!!editingId" />
</el-form-item>
<el-form-item label="分类" prop="category">
<el-select v-model="form.category" style="width: 100%">
<el-option label="触发器" value="trigger" />
<el-option label="动作" value="action" />
<el-option label="条件" value="condition" />
<el-option label="控制" value="control" />
</el-select>
</el-form-item>
<el-form-item label="代码块" prop="func">
<el-input
v-model="form.func"
type="textarea"
:rows="12"
placeholder="须定义 handler(*args, **kwargs),可接收 upstream、variables"
/>
</el-form-item>
<el-form-item label="位置参数" prop="args">
<el-input v-model="form.args" placeholder="逗号分隔,如 a, b" />
</el-form-item>
<el-form-item label="关键字参数" prop="kwargs">
<el-input
v-model="form.kwargs"
type="textarea"
:rows="3"
placeholder='JSON,如 {"key": "v"}'
/>
</el-form-item>
<el-form-item label="排序" prop="sort_order">
<el-input-number v-model="form.sort_order" :min="0" />
</el-form-item>
<el-form-item label="启用" prop="is_active">
<el-switch v-model="form.is_active" />
</el-form-item>
</el-form>
<ArtForm
:key="nodeTypeFormRenderKey"
ref="formRef"
v-model="form"
:items="nodeTypeDialogFormItems"
:rules="rules"
label-width="100px"
label-position="right"
:span="24"
:gutter="16"
:show-reset="false"
:show-submit="false"
class="crud-dialog-art-form"
/>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="submitForm">保存</el-button>
<ElButton @click="dialogVisible = false">取消</ElButton>
<ElButton type="primary" :loading="submitting" @click="submitForm">保存</ElButton>
</template>
</EnhancedDialog>
</ArtDialog>
</div>
</template>
@@ -139,109 +109,94 @@ defineOptions({
inheritAttrs: false,
});
import { onMounted, reactive, ref } from "vue";
import { ElMessage, type FormInstance, type FormRules } from "element-plus";
import PageSearch from "@/components/CURD/PageSearch.vue";
import PageContent from "@/components/CURD/PageContent.vue";
import EnhancedDialog from "@/components/CURD/EnhancedDialog.vue";
import type { IContentConfig, ISearchConfig } from "@/components/CURD/types";
import { useCrudList } from "@/components/CURD/useCrudList";
import OperationColumn from "@/components/OperationColumn/index.vue";
import WorkflowNodeTypeAPI, {
type WorkflowNodeTypeForm,
type WorkflowNodeTypePageQuery,
type WorkflowNodeTypeTable,
} from "@/api/module_task/workflow/node-type";
import ArtSearchBar from "@/components/Core/forms/art-search-bar/index.vue";
import type { SearchFormItem } from "@/components/Core/forms/art-search-bar/index.vue";
import ArtTable from "@/components/Core/tables/art-table/index.vue";
import ArtTableHeader from "@/components/Core/tables/art-table-header/index.vue";
import ArtTableHeaderLeft from "@/components/Core/tables/art-table-header-left/index.vue";
import ArtDialog from "@/components/Core/modal/art-dialog/index.vue";
import ArtForm from "@/components/Core/forms/art-form/index.vue";
import type { FormItem } from "@/components/Core/forms/art-form/index.vue";
import { useTable } from "@/hooks/core/useTable";
import type { ColumnOption } from "@/types/component";
import { ElMessage, ElMessageBox, ElTag } from "element-plus";
import type { FormRules } from "element-plus";
import { computed, h, reactive, ref } from "vue";
const { searchRef, contentRef, handleQueryClick, handleResetClick, refreshList } = useCrudList();
const BATCH_DELETE_MSG = "确认删除选中的编排节点类型吗?";
const dialogVisible = ref(false);
const dialogTitle = ref("新增节点类型");
const editingId = ref<number | null>(null);
const submitting = ref(false);
const formRef = ref<FormInstance>();
type NodeTypeSearchForm = {
name?: string;
code?: string;
category?: string;
};
const searchConfig = reactive<ISearchConfig>({
permPrefix: "module_task:workflow:node-type",
colon: true,
isExpandable: true,
showNumber: 2,
form: { labelWidth: "auto" },
formItems: [
{
prop: "name",
label: "名称",
type: "input",
attrs: { placeholder: "名称", clearable: true },
},
{
prop: "code",
label: "编码",
type: "input",
attrs: { placeholder: "编码", clearable: true },
},
{
prop: "category",
label: "分类",
type: "select",
function buildNodeTypeReplaceParams(u: NodeTypeSearchForm): Record<string, unknown> {
return {
name: u.name,
code: u.code,
category: u.category,
};
}
const searchForm = ref<NodeTypeSearchForm>({
name: undefined,
code: undefined,
category: undefined,
});
const showSearchBar = ref(true);
const searchBarRef = ref<InstanceType<typeof ArtSearchBar> | null>(null);
const searchBarRules: Record<string, unknown> = {};
const nodeTypeSearchItems = computed<SearchFormItem[]>(() => [
{
label: "名称",
key: "name",
type: "input",
placeholder: "名称",
clearable: true,
span: 6,
},
{
label: "编码",
key: "code",
type: "input",
placeholder: "编码",
clearable: true,
span: 6,
},
{
label: "分类",
key: "category",
type: "select",
props: {
placeholder: "全部",
clearable: true,
options: [
{ label: "触发器", value: "trigger" },
{ label: "动作", value: "action" },
{ label: "条件", value: "condition" },
{ label: "控制", value: "control" },
],
attrs: { placeholder: "全部", clearable: true, style: { width: "170px" } },
},
],
});
function normalizeNodeTypeQuery(params: Record<string, unknown>): WorkflowNodeTypePageQuery {
const p = { ...params } as Record<string, unknown>;
if (p.category === "" || p.category === null) p.category = undefined;
return p as unknown as WorkflowNodeTypePageQuery;
}
const contentConfig = reactive<IContentConfig<WorkflowNodeTypePageQuery>>({
permPrefix: "module_task:workflow:node-type",
cols: [],
hideColumnFilter: true,
toolbar: [
{ name: "add", text: "新增", attrs: { icon: "plus", type: "success" }, perm: "create" },
"delete",
],
defaultToolbar: ["refresh"],
initialFetch: false,
pagination: {
pageSize: 10,
pageSizes: [10, 20, 30, 50],
span: 6,
},
request: { page_no: "page_no", page_size: "page_size" },
indexAction: async (params) => {
const res = await WorkflowNodeTypeAPI.getWorkflowNodeTypeList(
normalizeNodeTypeQuery(params as unknown as Record<string, unknown>)
);
return {
total: res.data.data.total,
list: res.data.data.items,
};
},
deleteAction: (ids) =>
WorkflowNodeTypeAPI.deleteWorkflowNodeType(
ids
.split(",")
.map((s) => Number(s.trim()))
.filter((n) => !Number.isNaN(n) && n > 0)
),
deleteConfirm: {
title: "警告",
message: "确认删除选中的编排节点类型吗?",
type: "warning",
},
});
]);
function handleRowDelete(id?: number) {
if (id == null) return;
contentRef.value?.handleDelete(id);
const artTableRef = ref<{ elTableRef?: { clearSelection: () => void } } | null>(null);
const selectedRows = ref<WorkflowNodeTypeTable[]>([]);
const selectedIds = computed(() =>
selectedRows.value.map((r) => r.id).filter((id): id is number => typeof id === "number")
);
const batchDeleting = ref(false);
function onTableSelectionChange(rows: WorkflowNodeTypeTable[]) {
selectedRows.value = rows;
}
function categoryLabel(c?: string) {
@@ -254,6 +209,239 @@ function categoryLabel(c?: string) {
return c ? m[c] || c : "-";
}
function deleteNodeTypeRow(id: number | undefined) {
if (id == null) return;
ElMessageBox.confirm("确认删除该节点类型吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
await WorkflowNodeTypeAPI.deleteWorkflowNodeType([id]);
ElMessage.success("删除成功");
artTableRef.value?.elTableRef?.clearSelection();
await refreshRemove();
})
.catch(() => {});
}
function handleBatchDelete() {
const ids = selectedIds.value;
if (ids.length === 0) return;
ElMessageBox.confirm(BATCH_DELETE_MSG, "批量删除", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(async () => {
try {
batchDeleting.value = true;
await WorkflowNodeTypeAPI.deleteWorkflowNodeType(ids);
ElMessage.success("删除成功");
selectedRows.value = [];
await refreshRemove();
} finally {
batchDeleting.value = false;
}
})
.catch(() => {});
}
const {
columns,
columnChecks,
data,
loading,
pagination,
getData,
replaceSearchParams,
resetSearchParams,
handleSizeChange,
handleCurrentChange,
refreshData,
refreshRemove,
refreshCreate,
refreshUpdate,
} = useTable({
core: {
apiFn: WorkflowNodeTypeAPI.getWorkflowNodeTypeList,
apiParams: {
page_no: 1,
page_size: 10,
},
columnsFactory: (): ColumnOption<WorkflowNodeTypeTable>[] => [
{ type: "selection", width: 48, fixed: "left" },
{ type: "globalIndex", width: 56, label: "序号" },
{
prop: "id",
label: "ID",
width: 88,
align: "center",
},
{
prop: "name",
label: "名称",
minWidth: 140,
showOverflowTooltip: true,
},
{
prop: "code",
label: "编码",
minWidth: 120,
showOverflowTooltip: true,
},
{
prop: "category",
label: "分类",
minWidth: 100,
formatter: (row) => categoryLabel(row.category),
},
{
prop: "sort_order",
label: "排序",
width: 88,
align: "center",
},
{
prop: "is_active",
label: "启用",
width: 88,
align: "center",
formatter: (row) =>
h(ElTag, { type: row.is_active ? "success" : "info" }, () =>
row.is_active ? "是" : "否"
),
},
{
prop: "created_time",
label: "创建时间",
minWidth: 170,
showOverflowTooltip: true,
},
{
prop: "operation",
label: "操作",
width: 140,
fixed: "right",
align: "center",
useSlot: true,
slotName: "node-type-operation",
},
],
},
});
const paginationBind = computed(() => {
const p = pagination as unknown as {
current?: number;
size?: number;
total?: number;
page_no?: number;
page_size?: number;
};
return {
current: p.current ?? p.page_no ?? 1,
size: p.size ?? p.page_size ?? 10,
total: p.total ?? 0,
};
});
async function handleSearchBarSearch(params: NodeTypeSearchForm) {
await searchBarRef.value?.validate?.();
replaceSearchParams(buildNodeTypeReplaceParams(params));
getData();
}
async function onResetSearch() {
searchForm.value = {
name: undefined,
code: undefined,
category: undefined,
};
await resetSearchParams();
}
const dialogVisible = ref(false);
const dialogTitle = ref("新增节点类型");
const editingId = ref<number | null>(null);
const submitting = ref(false);
const formRef = ref<InstanceType<typeof ArtForm> | null>(null);
const nodeTypeFormRenderKey = ref(0);
const nodeTypeDialogFormItems = computed<FormItem[]>(() => [
{
label: "名称",
key: "name",
type: "input",
span: 24,
props: { maxlength: 128, showWordLimit: true },
},
{
label: "编码",
key: "code",
type: "input",
span: 24,
props: { maxlength: 64, showWordLimit: true, disabled: !!editingId.value },
},
{
label: "分类",
key: "category",
type: "select",
span: 24,
props: {
style: { width: "100%" },
options: [
{ label: "触发器", value: "trigger" },
{ label: "动作", value: "action" },
{ label: "条件", value: "condition" },
{ label: "控制", value: "control" },
],
},
},
{
label: "代码块",
key: "func",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 12,
placeholder: "须定义 handler(*args, **kwargs),可接收 upstream、variables",
},
},
{
label: "位置参数",
key: "args",
type: "input",
span: 24,
props: { placeholder: "逗号分隔,如 a, b" },
},
{
label: "关键字参数",
key: "kwargs",
type: "input",
span: 24,
props: {
type: "textarea",
rows: 3,
placeholder: 'JSON,如 {"key": "v"}',
},
},
{
label: "排序",
key: "sort_order",
type: "number",
span: 24,
props: { min: 0 },
},
{
label: "启用",
key: "is_active",
type: "switch",
span: 24,
},
]);
const defaultForm = (): WorkflowNodeTypeForm => ({
name: "",
code: "",
@@ -277,7 +465,8 @@ const rules: FormRules = {
function resetForm() {
Object.assign(form, defaultForm());
editingId.value = null;
formRef.value?.resetFields();
formRef.value?.ref?.resetFields();
formRef.value?.ref?.clearValidate();
}
function handleCloseDialog() {
@@ -307,6 +496,7 @@ async function openDialog(id?: number) {
return;
}
}
nodeTypeFormRenderKey.value += 1;
dialogVisible.value = true;
}
@@ -326,22 +516,28 @@ async function submitForm() {
if (editingId.value) {
await WorkflowNodeTypeAPI.updateWorkflowNodeType(editingId.value, form);
ElMessage.success("更新成功");
dialogVisible.value = false;
await refreshUpdate();
} else {
await WorkflowNodeTypeAPI.createWorkflowNodeType(form);
ElMessage.success("创建成功");
dialogVisible.value = false;
await refreshCreate();
}
dialogVisible.value = false;
refreshList();
} catch {
ElMessage.error(editingId.value ? "更新失败" : "创建失败");
} finally {
submitting.value = false;
}
}
onMounted(() => {
refreshList();
});
</script>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.crud-dialog-art-form :deep(.el-row > .el-col:last-child) {
display: none;
}
.crud-dialog-art-form :deep(.el-form-item__content) {
max-width: 100%;
}
</style>