mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-03 16:53:29 +00:00
feat(前端): 添加页面切换动画功能并优化工作流节点样式
refactor(后端): 统一查询参数处理使用QueueEnum枚举 style(前端): 优化菜单搜索组件样式 fix(后端): 移除不支持的数据库类型dm chore: 更新依赖和配置文件
This commit is contained in:
@@ -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>
|
||||
+27
-15
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user