feat(前端): 添加页面切换动画功能并优化工作流节点样式

refactor(后端): 统一查询参数处理使用QueueEnum枚举

style(前端): 优化菜单搜索组件样式

fix(后端): 移除不支持的数据库类型dm

chore: 更新依赖和配置文件
This commit is contained in:
zhangtao
2026-02-06 22:31:39 +08:00
parent 2af82b3959
commit 51f9a9acfa
63 changed files with 2947 additions and 555 deletions
@@ -0,0 +1,167 @@
<template>
<div class="edge-config-panel">
<div class="panel-header">
<span>连线配置</span>
<ElButton type="text" class="close-btn" @click="handleClose">
<ElIcon><Close /></ElIcon>
</ElButton>
</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="连线颜色">
<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>
<div class="panel-actions">
<ElButton type="primary" size="small" @click="handleSave">保存</ElButton>
<ElButton type="danger" size="small" @click="handleDelete">删除连线</ElButton>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
import {
ElButton,
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElInputNumber,
ElSwitch,
ElColorPicker,
ElMessage,
ElIcon,
} from "element-plus";
import { Close } from "@element-plus/icons-vue";
const props = defineProps({
edge: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["close", "save", "delete"]);
const formData = ref({
label: props.edge?.label || "",
type: props.edge?.type || "smoothstep",
color: props.edge?.style?.stroke || "#000000",
strokeWidth: props.edge?.style?.strokeWidth || 2,
animated: props.edge?.animated || false,
condition: props.edge?.data?.condition || "",
description: props.edge?.data?.description || "",
});
watch(
() => props.edge,
(newEdge) => {
if (newEdge) {
formData.value = {
label: newEdge.label || "",
type: newEdge.type || "smoothstep",
color: newEdge.style?.stroke || "#000000",
strokeWidth: newEdge.style?.strokeWidth || 2,
animated: newEdge.animated || false,
condition: newEdge.data?.condition || "",
description: newEdge.data?.description || "",
};
}
},
{ deep: true }
);
function handleClose() {
emit("close");
}
function handleSave() {
emit("save", formData.value);
ElMessage.success("保存成功");
}
function handleDelete() {
emit("delete");
}
</script>
<style scoped>
.edge-config-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
font-weight: 600;
border-bottom: 1px solid #e5e7eb;
}
.close-btn {
padding: 4px;
}
.panel-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.panel-actions {
display: flex;
gap: 8px;
padding-top: 16px;
border-top: 1px solid #e5e7eb;
}
.panel-actions .el-button {
flex: 1;
}
</style>
@@ -0,0 +1,208 @@
<template>
<div class="node-config-panel">
<div class="panel-header">
<span>节点配置</span>
<ElButton type="text" class="close-btn" @click="handleClose">
<ElIcon><Close /></ElIcon>
</ElButton>
</div>
<div class="panel-content">
<ElForm :model="formData" label-width="80px" size="small">
<ElFormItem label="节点名称">
<ElInput v-model="formData.label" placeholder="请输入节点名称" />
</ElFormItem>
<ElFormItem v-if="nodeType === 'task'" label="执行人">
<ElSelect v-model="formData.assignee" placeholder="请选择执行人">
<ElOption label="张三" value="zhangsan" />
<ElOption label="李四" value="lisi" />
<ElOption label="王五" value="wangwu" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="nodeType === 'task'" label="优先级">
<ElSelect v-model="formData.priority" placeholder="请选择优先级">
<ElOption label="高" value="high" />
<ElOption label="中" value="medium" />
<ElOption label="低" value="low" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="nodeType === 'approval'" label="审批人">
<ElSelect v-model="formData.approver" placeholder="请选择审批人">
<ElOption label="经理A" value="manager1" />
<ElOption label="经理B" value="manager2" />
<ElOption label="总监" value="director" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="nodeType === 'approval'" label="审批类型">
<ElSelect v-model="formData.approvalType" placeholder="请选择审批类型">
<ElOption label="单人审批" value="single" />
<ElOption label="会签" value="all" />
<ElOption label="或签" value="any" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="nodeType === 'notification'" label="通知类型">
<ElSelect v-model="formData.notifyType" placeholder="请选择通知类型">
<ElOption label="邮件" value="email" />
<ElOption label="短信" value="sms" />
<ElOption label="站内信" value="message" />
</ElSelect>
</ElFormItem>
<ElFormItem v-if="nodeType === 'notification'" label="接收人">
<ElInput v-model="formData.recipients" placeholder="请输入接收人" />
</ElFormItem>
<ElFormItem v-if="nodeType === 'timer'" label="延迟时间">
<ElInputNumber v-model="formData.delay" :min="0" :max="3600" />
<span style="margin-left: 8px">秒</span>
</ElFormItem>
<ElFormItem v-if="nodeType === 'condition'" label="条件表达式">
<ElInput
v-model="formData.condition"
type="textarea"
:rows="3"
placeholder="请输入条件表达式"
/>
</ElFormItem>
<ElFormItem v-if="nodeType === 'parallel'" label="分支数量">
<ElInputNumber v-model="formData.branchCount" :min="2" :max="10" />
</ElFormItem>
<ElFormItem label="描述">
<ElInput
v-model="formData.description"
type="textarea"
:rows="2"
placeholder="请输入描述信息"
/>
</ElFormItem>
</ElForm>
<div class="panel-actions">
<ElButton type="primary" size="small" @click="handleSave">保存</ElButton>
<ElButton type="danger" size="small" @click="handleDelete">删除节点</ElButton>
</div>
</div>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
import {
ElButton,
ElForm,
ElFormItem,
ElInput,
ElSelect,
ElOption,
ElInputNumber,
ElMessage,
ElIcon,
} from "element-plus";
import { Close } from "@element-plus/icons-vue";
const props = defineProps({
node: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["close", "save", "delete"]);
const nodeType = ref(props.node?.type || "custom");
const formData = ref({
label: props.node?.data?.label || "",
assignee: props.node?.data?.assignee || "",
priority: props.node?.data?.priority || "medium",
approver: props.node?.data?.approver || "",
approvalType: props.node?.data?.approvalType || "single",
notifyType: props.node?.data?.notifyType || "email",
recipients: props.node?.data?.recipients || "",
delay: props.node?.data?.delay || 0,
condition: props.node?.data?.condition || "",
branchCount: props.node?.data?.branchCount || 2,
description: props.node?.data?.description || "",
});
watch(
() => props.node,
(newNode) => {
if (newNode) {
nodeType.value = newNode.type || "custom";
formData.value = {
label: newNode.data?.label || "",
assignee: newNode.data?.assignee || "",
priority: newNode.data?.priority || "medium",
approver: newNode.data?.approver || "",
approvalType: newNode.data?.approvalType || "single",
notifyType: newNode.data?.notifyType || "email",
recipients: newNode.data?.recipients || "",
delay: newNode.data?.delay || 0,
condition: newNode.data?.condition || "",
branchCount: newNode.data?.branchCount || 2,
description: newNode.data?.description || "",
};
}
},
{ deep: true }
);
function handleClose() {
emit("close");
}
function handleSave() {
emit("save", formData.value);
ElMessage.success("保存成功");
}
function handleDelete() {
emit("delete");
}
</script>
<style scoped>
.node-config-panel {
display: flex;
flex-direction: column;
height: 100%;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
font-weight: 600;
border-bottom: 1px solid #e5e7eb;
}
.close-btn {
padding: 4px;
}
.panel-content {
flex: 1;
padding: 16px;
overflow-y: auto;
}
.panel-actions {
display: flex;
gap: 8px;
padding-top: 16px;
border-top: 1px solid #e5e7eb;
}
.panel-actions .el-button {
flex: 1;
}
</style>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,73 @@
<template>
<div class="approval-node">
<div class="node-header">
<span class="icon">✓</span>
<span>{{ data.label }}</span>
</div>
<div class="node-body">
<div class="approval-info">
<span>审批人: {{ data.approver || "待指定" }}</span>
<span>类型: {{ data.approvalType || "单人审批" }}</span>
</div>
</div>
<Handle :id="'top-' + id" type="source" position="top" :style="{ background: '#10b981' }" />
<Handle :id="'left-' + id" type="source" position="left" :style="{ background: '#10b981' }" />
<Handle :id="'right-' + id" type="source" position="right" :style="{ background: '#10b981' }" />
<Handle
:id="'bottom-' + id"
type="source"
position="bottom"
:style="{ background: '#10b981' }"
/>
</div>
</template>
<script setup>
import { Handle } from "@vue-flow/core";
defineProps({
id: String,
data: Object,
});
</script>
<style scoped>
.approval-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 150px;
height: 70px;
color: white;
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
border: 2px solid #047857;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
}
.node-header {
display: flex;
gap: 5px;
align-items: center;
margin-bottom: 5px;
font-size: 14px;
font-weight: bold;
}
.icon {
font-size: 16px;
}
.node-body {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
}
.approval-info {
display: flex;
flex-direction: column;
gap: 2px;
}
</style>
@@ -0,0 +1,45 @@
<template>
<div class="condition-node">
<div class="node-content">{{ data.label }}</div>
<Handle :id="'top-' + id" type="source" position="top" :style="{ background: '#f59e0b' }" />
<Handle :id="'left-' + id" type="source" position="left" :style="{ background: '#f59e0b' }" />
<Handle :id="'right-' + id" type="source" position="right" :style="{ background: '#f59e0b' }" />
<Handle
:id="'bottom-' + id"
type="source"
position="bottom"
:style="{ background: '#f59e0b' }"
/>
</div>
</template>
<script setup>
import { Handle } from "@vue-flow/core";
defineProps({
id: String,
data: Object,
});
</script>
<style scoped>
.condition-node {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100px;
height: 100px;
color: white;
background-color: #f59e0b;
border-radius: 8px;
transform: rotate(45deg);
}
.node-content {
font-size: 12px;
font-weight: bold;
text-align: center;
transform: rotate(-45deg);
}
</style>
@@ -0,0 +1,72 @@
<template>
<div class="notification-node">
<div class="node-header">
<span class="icon">🔔</span>
<span>{{ data.label }}</span>
</div>
<div class="node-body">
<div class="notification-info">
<span>类型: {{ data.notifyType || "邮件" }}</span>
</div>
</div>
<Handle :id="'top-' + id" type="source" position="top" :style="{ background: '#ec4899' }" />
<Handle :id="'left-' + id" type="source" position="left" :style="{ background: '#ec4899' }" />
<Handle :id="'right-' + id" type="source" position="right" :style="{ background: '#ec4899' }" />
<Handle
:id="'bottom-' + id"
type="source"
position="bottom"
:style="{ background: '#ec4899' }"
/>
</div>
</template>
<script setup>
import { Handle } from "@vue-flow/core";
defineProps({
id: String,
data: Object,
});
</script>
<style scoped>
.notification-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 150px;
height: 70px;
color: white;
background: linear-gradient(135deg, #ec4899 0%, #db2777 100%);
border: 2px solid #be185d;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(236, 72, 153, 0.3);
}
.node-header {
display: flex;
gap: 5px;
align-items: center;
margin-bottom: 5px;
font-size: 14px;
font-weight: bold;
}
.icon {
font-size: 16px;
}
.node-body {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
}
.notification-info {
display: flex;
flex-direction: column;
gap: 2px;
}
</style>
@@ -1,22 +1,22 @@
<template>
<div class="custom-node">
<div class="parallel-node">
<div class="node-header">{{ data.label }}</div>
<!-- Handle 定义 -->
<Handle :id="'top-' + id" type="source" position="top" :style="{ background: '#4a5568' }" />
<Handle :id="'left-' + id" type="source" position="left" :style="{ background: '#4a5568' }" />
<Handle :id="'right-' + id" type="source" position="right" :style="{ background: '#4a5568' }" />
<div class="node-body">
<div class="branch-label">并行分支</div>
</div>
<Handle :id="'top-' + id" type="source" position="top" :style="{ background: '#8b5cf6' }" />
<Handle :id="'left-' + id" type="source" position="left" :style="{ background: '#8b5cf6' }" />
<Handle :id="'right-' + id" type="source" position="right" :style="{ background: '#8b5cf6' }" />
<Handle
:id="'bottom-' + id"
type="source"
position="bottom"
:style="{ background: '#4a5568' }"
:style="{ background: '#8b5cf6' }"
/>
</div>
</template>
<script setup>
import { defineProps } from "vue";
import { Handle } from "@vue-flow/core";
defineProps({
@@ -26,22 +26,34 @@ defineProps({
</script>
<style scoped>
.custom-node {
.parallel-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 120px;
height: 40px;
width: 140px;
height: 80px;
color: white;
background-color: #4a5568;
background-color: rgba(148, 146, 146, 0.32);
border: 1px solid rgba(148, 146, 146, 0.79);
border-radius: 3px;
background-color: #8b5cf6;
border: 2px solid #7c3aed;
border-radius: 8px;
}
.node-header {
margin-bottom: 5px;
font-size: 14px;
font-weight: bold;
}
.node-body {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
}
.branch-label {
display: flex;
gap: 5px;
align-items: center;
}
</style>
@@ -0,0 +1,72 @@
<template>
<div class="task-node">
<div class="node-header">
<span class="icon">📋</span>
<span>{{ data.label }}</span>
</div>
<div class="node-body">
<div class="task-info">
<span>执行人: {{ data.assignee || "未指定" }}</span>
</div>
</div>
<Handle :id="'top-' + id" type="source" position="top" :style="{ background: '#3b82f6' }" />
<Handle :id="'left-' + id" type="source" position="left" :style="{ background: '#3b82f6' }" />
<Handle :id="'right-' + id" type="source" position="right" :style="{ background: '#3b82f6' }" />
<Handle
:id="'bottom-' + id"
type="source"
position="bottom"
:style="{ background: '#3b82f6' }"
/>
</div>
</template>
<script setup>
import { Handle } from "@vue-flow/core";
defineProps({
id: String,
data: Object,
});
</script>
<style scoped>
.task-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 150px;
height: 70px;
color: white;
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
border: 2px solid #1d4ed8;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
}
.node-header {
display: flex;
gap: 5px;
align-items: center;
margin-bottom: 5px;
font-size: 14px;
font-weight: bold;
}
.icon {
font-size: 16px;
}
.node-body {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
}
.task-info {
display: flex;
flex-direction: column;
gap: 2px;
}
</style>
@@ -0,0 +1,72 @@
<template>
<div class="timer-node">
<div class="node-header">
<span class="icon">⏰</span>
<span>{{ data.label }}</span>
</div>
<div class="node-body">
<div class="timer-info">
<span>延迟: {{ data.delay || "0" }}秒</span>
</div>
</div>
<Handle :id="'top-' + id" type="source" position="top" :style="{ background: '#6366f1' }" />
<Handle :id="'left-' + id" type="source" position="left" :style="{ background: '#6366f1' }" />
<Handle :id="'right-' + id" type="source" position="right" :style="{ background: '#6366f1' }" />
<Handle
:id="'bottom-' + id"
type="source"
position="bottom"
:style="{ background: '#6366f1' }"
/>
</div>
</template>
<script setup>
import { Handle } from "@vue-flow/core";
defineProps({
id: String,
data: Object,
});
</script>
<style scoped>
.timer-node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 150px;
height: 70px;
color: white;
background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%);
border: 2px solid #4338ca;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.3);
}
.node-header {
display: flex;
gap: 5px;
align-items: center;
margin-bottom: 5px;
font-size: 14px;
font-weight: bold;
}
.icon {
font-size: 16px;
}
.node-body {
font-size: 11px;
color: rgba(255, 255, 255, 0.9);
}
.timer-info {
display: flex;
flex-direction: column;
gap: 2px;
}
</style>
@@ -0,0 +1,158 @@
import type { CSSProperties } from "vue";
export type NodeType =
| "input"
| "output"
| "task"
| "approval"
| "condition"
| "parallel"
| "notification"
| "timer"
| "default";
export type EdgeType = "default" | "straight" | "step" | "smoothstep" | "bezier";
export type HandlePosition = "left" | "right" | "top" | "bottom";
export interface TaskNodeData {
label: string;
assignee?: string;
priority?: "low" | "medium" | "high";
description?: string;
}
export interface ApprovalNodeData {
label: string;
approver?: string;
approvalType?: "single" | "multi" | "sequential" | "parallel";
description?: string;
}
export interface ConditionNodeData {
label: string;
condition?: string;
description?: string;
}
export interface ParallelNodeData {
label: string;
branchCount?: number;
description?: string;
}
export interface NotificationNodeData {
label: string;
notifyType?: "email" | "sms" | "push" | "webhook";
recipients?: string;
description?: string;
}
export interface TimerNodeData {
label: string;
delay?: number;
unit?: "seconds" | "minutes" | "hours" | "days";
description?: string;
}
export type NodeData =
| TaskNodeData
| ApprovalNodeData
| ConditionNodeData
| ParallelNodeData
| NotificationNodeData
| TimerNodeData
| Record<string, any>;
export interface Marker {
type: "arrow" | "arrowclosed";
color?: string;
width?: number;
height?: number;
orient?: "auto" | "auto-start-reverse";
}
export interface Node {
id: string;
position: { x: number; y: number };
type?: NodeType;
data?: NodeData;
label?: string;
style?: CSSProperties;
class?: string | string[];
sourcePosition?: HandlePosition;
targetPosition?: HandlePosition;
hidden?: boolean;
selected?: boolean;
draggable?: boolean;
connectable?: boolean;
deletable?: boolean;
selectable?: boolean;
focusable?: boolean;
dragHandle?: string;
extent?: "parent" | [number, number] | [[number, number], [number, number]];
parentNode?: string;
expandParent?: boolean;
zIndex?: number;
}
export interface Edge {
id?: string;
source: string;
target: string;
sourceHandle?: string;
targetHandle?: string;
type?: EdgeType;
label?: string;
labelStyle?: CSSProperties;
labelShowBg?: boolean;
labelBgStyle?: CSSProperties;
labelBgPadding?: [number, number];
labelBgBorderRadius?: number;
style?: CSSProperties;
class?: string | string[];
animated?: boolean;
hidden?: boolean;
selected?: boolean;
deletable?: boolean;
selectable?: boolean;
focusable?: boolean;
updatable?: boolean | "source" | "target";
markerStart?: Marker | string;
markerEnd?: Marker | string;
pathOptions?: {
offset?: number;
borderRadius?: number;
curvature?: number;
};
interactionWidth?: number;
}
export interface WorkflowTemplate {
id: string;
name: string;
description?: string;
nodes: Node[];
edges: Edge[];
}
export interface WorkflowStats {
totalNodes: number;
totalEdges: number;
nodeTypes: Record<NodeType, number>;
}
export interface NodeConfig {
id: string;
type: NodeType;
data: NodeData;
}
export interface EdgeConfig {
id: string;
source: string;
target: string;
label?: string;
type?: EdgeType;
animated?: boolean;
}