| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973 |
- import { ref, onMounted, onUnmounted, nextTick, reactive, computed, type Ref } from 'vue'
- import { useRoute, useRouter } from 'vue-router'
- import LogicFlow, {
- RectNode,
- RectNodeModel,
- CircleNode,
- CircleNodeModel,
- PolygonNode,
- PolygonNodeModel,
- } from '@logicflow/core'
- import { Menu, Snapshot } from '@logicflow/extension'
- import '@logicflow/core/dist/style/index.css'
- import '@logicflow/extension/lib/style/index.css'
- import { ElMessage } from 'element-plus'
- import type { FormRules } from 'element-plus'
- import { listRole } from '@/api/system/role'
- import { addDefinition, updateDefinition, getDefinition } from '@/api/flow/definition'
- import type { Role } from '@/types/system'
- import type { FlowDefinition, FormField, FormFieldOption } from '@/types/flow'
- /* eslint-disable @typescript-eslint/no-explicit-any */
- export function useFlowDesigner(lfContainer: Ref<HTMLDivElement | undefined>) {
- const route = useRoute()
- const router = useRouter()
- let lf: LogicFlow | null = null
- const lfRef = ref<any>(null)
- const mode = computed(() => (route.query.mode as string) || '')
- const flowId = computed(() => (route.query.id as string) || '')
- const flowCode = computed(() => (route.query.code as string) || '')
- const flowName = computed(() => (route.query.name as string) || '')
- const flowCategory = computed(() => (route.query.category as string) || '')
- const flowDescription = computed(() => (route.query.description as string) || '')
- const flowInfoVisible = computed(() => mode.value === 'create' || mode.value === 'edit')
- const selectedNode = ref<any>(null)
- const roleList = ref<Role[]>([])
- const nodeProps = reactive<Record<string, any>>({
- assigneeType: 'ROLE',
- assigneeValue: '',
- nodeKey: '',
- approveMode: 'or',
- timeoutHours: undefined as number | undefined,
- timeoutAction: 'remind',
- approveTimeField: '',
- ccUsers: '',
- subNodesEnabled: false,
- subNodeMode: 'parallel',
- subNodes: [] as any[],
- })
- // 条件节点出连线配置
- const outgoingEdges = ref<any[]>([])
- function loadOutgoingEdges(nodeId?: string) {
- if (!lf) {
- outgoingEdges.value = []
- return
- }
- const id = nodeId || selectedNode.value?.id
- if (!id) {
- outgoingEdges.value = []
- return
- }
- const graphData = lf.getGraphData()
- const edges = (graphData.edges || []).filter((e: any) => e.sourceNodeId === id)
- outgoingEdges.value = edges.map((e: any) => ({
- ...e,
- properties: {
- condition: '',
- isDefault: false,
- branchName: '',
- ...(e.properties || {}),
- },
- }))
- }
- function getEdgeTargetName(edge: any): string {
- if (!lf || !edge?.targetNodeId) return '—'
- try {
- const node = lf.getNodeData(edge.targetNodeId)
- return node?.text?.value || node?.text || '未命名节点'
- } catch {
- return '—'
- }
- }
- function getEdgeDisplayText(edge: any): string {
- const props = edge.properties || {}
- if (props.branchName) return props.branchName
- if (props.isDefault) return '默认'
- if (props.condition) return props.condition
- return ''
- }
- function syncEdgeToLf(edge: any) {
- if (!lf || !edge?.id) return
- const props = edge.properties || {}
- const displayText = getEdgeDisplayText(edge)
- lf.setEdgeData(edge.id, {
- properties: { ...props },
- text: displayText ? { value: displayText } : undefined,
- })
- }
- function onDefaultChange(edge: any) {
- if (edge.properties.isDefault) {
- edge.properties.condition = ''
- // 同一条件节点下只能有一个默认分支
- const nodeId = edge.sourceNodeId
- outgoingEdges.value.forEach((e: any) => {
- if (e.id !== edge.id && e.sourceNodeId === nodeId) {
- e.properties.isDefault = false
- syncEdgeToLf(e)
- }
- })
- }
- syncEdgeToLf(edge)
- }
- function onBranchBlur(edge: any) {
- syncEdgeToLf(edge)
- }
- function generateSubNodeId(): string {
- return 'sub-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8)
- }
- function getDefaultProps(type: string) {
- if (type === 'approval-node')
- return {
- assigneeType: 'ROLE',
- assigneeValue: '',
- nodeKey: '',
- approveMode: 'or',
- timeoutHours: undefined,
- timeoutAction: 'remind',
- approveTimeField: '',
- subNodesEnabled: false,
- subNodeMode: 'parallel',
- subNodes: [],
- }
- if (type === 'cc-node') return { ccUsers: '' }
- return {}
- }
- function saveCurrentNodeProps() {
- if (selectedNode.value && lf) {
- const type = selectedNode.value.type
- const defaultProps = getDefaultProps(type)
- // 只保存当前节点类型相关的属性,避免不同类型节点属性互相污染
- const props: Record<string, any> = JSON.parse(JSON.stringify(defaultProps))
- if (type === 'approval-node') {
- props.assigneeType = nodeProps.assigneeType ?? 'ROLE'
- props.assigneeValue = nodeProps.assigneeValue ?? ''
- props.nodeKey = nodeProps.nodeKey ?? ''
- props.approveMode = nodeProps.approveMode ?? 'or'
- props.timeoutHours = nodeProps.timeoutHours
- props.timeoutAction = nodeProps.timeoutAction ?? 'remind'
- props.approveTimeField = nodeProps.approveTimeField ?? ''
- props.subNodesEnabled = nodeProps.subNodesEnabled ?? false
- props.subNodeMode = nodeProps.subNodeMode ?? 'parallel'
- props.subNodes = Array.isArray(nodeProps.subNodes) ? nodeProps.subNodes : []
- } else if (type === 'cc-node') {
- props.ccUsers = nodeProps.ccUsers ?? ''
- }
- lf.setProperties(selectedNode.value.id, props)
- // 同步子节点数量角标到节点文本
- if (type === 'approval-node') {
- syncNodeTextWithSubNodeBadge(selectedNode.value.id)
- }
- // 通知画布刷新子节点 overlay
- lf.emit('properties:changed', { nodeId: selectedNode.value.id })
- }
- }
- function getNodeRawText(nodeId: string): string {
- if (!lf) return ''
- try {
- const node = lf.getNodeData(nodeId)
- const text = node?.text?.value || node?.text || ''
- return String(text).replace(/ \(\+\d+\)$/, '')
- } catch {
- return ''
- }
- }
- function syncNodeTextWithSubNodeBadge(nodeId: string) {
- if (!lf) return
- const rawText = getNodeRawText(nodeId)
- if (!rawText) return
- // 不再把子节点数量拼进节点文本,改为画布浮层角标
- lf.updateText(nodeId, rawText)
- }
- function loadNodeProps(data: any) {
- const props = data.properties || getDefaultProps(data.type)
- const cloned = JSON.parse(JSON.stringify(props))
- // 先重置为默认值,避免上一个节点的属性残留
- Object.assign(nodeProps, getDefaultProps(data.type))
- // 兼容旧格式:approver + approveType -> 新格式
- if (data.type === 'approval-node') {
- const supportedTypes = ['ROLE', 'SELF', 'EXTERNAL']
- let assigneeType = cloned.assigneeType ?? 'ROLE'
- let assigneeValue = cloned.assigneeValue ?? ''
- const approveMode = cloned.approveMode ?? cloned.approveType ?? 'or'
- if (!cloned.assigneeType && !cloned.assigneeValue && cloned.approver) {
- assigneeType = 'ROLE'
- assigneeValue = cloned.approver
- }
- // 当前设计器支持 ROLE / SELF / EXTERNAL,其它旧类型降级为 ROLE 并清空值
- if (!supportedTypes.includes(assigneeType)) {
- assigneeType = 'ROLE'
- assigneeValue = ''
- }
- // SELF / EXTERNAL 不需要审批角色值
- if (assigneeType === 'SELF' || assigneeType === 'EXTERNAL') {
- assigneeValue = ''
- }
- nodeProps.assigneeType = assigneeType
- nodeProps.assigneeValue = assigneeValue
- nodeProps.nodeKey = cloned.nodeKey ?? ''
- nodeProps.approveMode = approveMode
- nodeProps.timeoutHours = cloned.timeoutHours ?? undefined
- nodeProps.timeoutAction = cloned.timeoutAction ?? 'remind'
- nodeProps.approveTimeField = cloned.approveTimeField ?? ''
- nodeProps.subNodesEnabled = cloned.subNodesEnabled === true
- nodeProps.subNodeMode = cloned.subNodeMode ?? 'parallel'
- nodeProps.subNodes = Array.isArray(cloned.subNodes)
- ? cloned.subNodes.map((s: any) => ({
- id: s.id || generateSubNodeId(),
- name: s.name || '',
- assigneeType: s.assigneeType ?? 'ROLE',
- assigneeValue: s.assigneeValue ?? '',
- approveMode: s.approveMode ?? 'or',
- timeoutHours: s.timeoutHours ?? undefined,
- timeoutAction: s.timeoutAction ?? 'remind',
- }))
- : []
- } else if (data.type === 'cc-node') {
- nodeProps.ccUsers = cloned.ccUsers ?? ''
- }
- }
- function registerNodes() {
- if (!lf) return
- class StartNodeModel extends CircleNodeModel {
- setAttributes() {
- this.r = 30
- if (!this.text.value) this.text.value = '开始'
- }
- getNodeStyle() {
- const style = super.getNodeStyle()
- style.fill = '#67C23A'
- style.stroke = '#67C23A'
- return style
- }
- getTextStyle() {
- const style = super.getTextStyle()
- style.color = '#fff'
- return style
- }
- }
- lf.register({ type: 'start-node', view: CircleNode, model: StartNodeModel })
- class ApprovalNodeModel extends RectNodeModel {
- setAttributes() {
- this.width = 160
- this.height = 60
- this.radius = 8
- if (!this.text.value) this.text.value = '审批节点'
- }
- getNodeStyle() {
- const style = super.getNodeStyle()
- style.fill = '#fff'
- style.stroke = '#409EFF'
- style.strokeWidth = 1
- return style
- }
- }
- lf.register({ type: 'approval-node', view: RectNode, model: ApprovalNodeModel })
- class CcNodeModel extends RectNodeModel {
- setAttributes() {
- this.width = 160
- this.height = 60
- this.radius = 8
- if (!this.text.value) this.text.value = '抄送节点'
- }
- getNodeStyle() {
- const style = super.getNodeStyle()
- style.fill = '#fff'
- style.stroke = '#E6A23C'
- style.strokeWidth = 1
- return style
- }
- }
- lf.register({ type: 'cc-node', view: RectNode, model: CcNodeModel })
- class ConditionNodeModel extends PolygonNodeModel {
- setAttributes() {
- this.points = [
- [60, 0],
- [120, 40],
- [60, 80],
- [0, 40],
- ]
- if (!this.text.value) this.text.value = '条件'
- }
- getNodeStyle() {
- const style = super.getNodeStyle()
- style.fill = '#fff'
- style.stroke = '#909399'
- return style
- }
- }
- lf.register({ type: 'condition-node', view: PolygonNode, model: ConditionNodeModel })
- class EndNodeModel extends CircleNodeModel {
- setAttributes() {
- this.r = 30
- if (!this.text.value) this.text.value = '结束'
- }
- getNodeStyle() {
- const style = super.getNodeStyle()
- style.fill = '#F56C6C'
- style.stroke = '#F56C6C'
- return style
- }
- getTextStyle() {
- const style = super.getTextStyle()
- style.color = '#fff'
- return style
- }
- }
- lf.register({ type: 'end-node', view: CircleNode, model: EndNodeModel })
- }
- function defaultNodes() {
- return [
- { id: 'start-1', type: 'start-node', x: 200, y: 100, text: '开始' },
- { id: 'end-1', type: 'end-node', x: 200, y: 500, text: '结束' },
- ]
- }
- function initLogicFlow() {
- if (!lfContainer.value) return
- lf = new LogicFlow({
- container: lfContainer.value,
- grid: true,
- plugins: [Menu, Snapshot],
- pluginsOptions: {
- menu: {
- nodeMenu: [
- {
- text: '删除',
- callback(node: any) {
- lf?.deleteNode(node.id)
- },
- },
- ],
- },
- } as any,
- })
- lfRef.value = lf
- registerNodes()
- lf.on('node:click', ({ data }: any) => {
- saveCurrentNodeProps()
- selectedNode.value = data
- loadNodeProps(data)
- loadOutgoingEdges(data.id)
- })
- lf.on('blank:click', () => {
- saveCurrentNodeProps()
- selectedNode.value = null
- outgoingEdges.value = []
- })
- // 当条件节点的出连线发生变化时,刷新右侧分支配置面板
- lf.on('edge:add', ({ data }: any) => {
- if (selectedNode.value?.id && data?.sourceNodeId === selectedNode.value.id) {
- loadOutgoingEdges(selectedNode.value.id)
- }
- })
- lf.on('edge:delete', ({ data }: any) => {
- if (selectedNode.value?.id && data?.sourceNodeId === selectedNode.value.id) {
- loadOutgoingEdges(selectedNode.value.id)
- }
- })
- // 编辑模式:加载已有流程图
- if (mode.value === 'edit' && flowId.value) {
- getDefinition(Number(flowId.value))
- .then((def: FlowDefinition) => {
- parseFormSchema(def.formSchema)
- if (def.flowJson) {
- try {
- const parsed = JSON.parse(def.flowJson)
- const graphData = convertToFrontendFormat(parsed)
- lf?.render(graphData)
- nextTick(() => syncAllSubNodeBadges())
- } catch {
- lf?.render({ nodes: defaultNodes(), edges: [] })
- }
- } else {
- lf?.render({ nodes: defaultNodes(), edges: [] })
- }
- })
- .catch(() => {
- lf?.render({ nodes: defaultNodes(), edges: [] })
- })
- } else {
- lf.render({ nodes: defaultNodes(), edges: [] })
- }
- nextTick(() => syncAllSubNodeBadges())
- }
- function syncAllSubNodeBadges() {
- if (!lf) return
- const graphData = lf.getGraphData()
- for (const node of graphData.nodes || []) {
- if (node.type !== 'approval-node') continue
- syncNodeTextWithSubNodeBadge(node.id)
- }
- }
- const typeToBackend: Record<string, string> = {
- 'start-node': 'start',
- 'approval-node': 'approval',
- 'cc-node': 'cc',
- 'condition-node': 'condition',
- 'end-node': 'end',
- }
- const typeToFrontend: Record<string, string> = {
- start: 'start-node',
- approval: 'approval-node',
- cc: 'cc-node',
- condition: 'condition-node',
- end: 'end-node',
- }
- // 将 LogicFlow 的 graphData 转换为后端格式
- function convertToBackendFormat(graphData: any) {
- const data = JSON.parse(JSON.stringify(graphData))
- if (data.nodes) {
- data.nodes = data.nodes.map((node: any) => {
- const n = { ...node }
- if (n.type && typeToBackend[n.type]) {
- n.type = typeToBackend[n.type]
- }
- // text -> name
- if (n.text !== undefined) {
- n.name = typeof n.text === 'object' ? n.text.value : n.text
- // 去掉子节点数量角标,避免保存到后端
- n.name = String(n.name).replace(/ \(\+\d+\)$/, '')
- delete n.text
- }
- return n
- })
- }
- // 将 LogicFlow edge.properties 转换为后端的 edge.condition 结构
- if (data.edges) {
- data.edges = data.edges.map((edge: any) => {
- const e = { ...edge }
- const props = e.properties || {}
- e.condition = {
- condition: props.condition || '',
- isDefault: props.isDefault ?? !props.condition,
- branchName: props.branchName || '',
- }
- // 清理 LogicFlow 运行时不需要的属性,避免后端序列化冗余
- if (e.text !== undefined && (!e.text || !e.text.value)) {
- delete e.text
- }
- delete e.properties
- return e
- })
- }
- return data
- }
- // 将后端的 graphData 转换为 LogicFlow 格式
- function convertToFrontendFormat(graphData: any) {
- const data = JSON.parse(JSON.stringify(graphData))
- // 兼容旧格式:条件表达式写在节点 properties.condition 上,需要迁移到连线
- if (data.nodes && data.edges) {
- data.nodes.forEach((node: any) => {
- if ((node.type === 'condition' || node.type === 'condition-node') && node.properties?.condition) {
- const nodeCondition = String(node.properties.condition)
- const outEdges = data.edges.filter((e: any) => e.sourceNodeId === node.id)
- if (outEdges.length > 0) {
- // 第一条连线作为条件分支,其余作为默认分支
- outEdges.forEach((e: any, idx: number) => {
- if (!e.condition) {
- e.condition = {}
- }
- if (idx === 0) {
- e.condition.condition = nodeCondition
- e.condition.isDefault = false
- } else {
- e.condition.condition = ''
- e.condition.isDefault = true
- }
- })
- }
- delete node.properties.condition
- }
- })
- }
- if (data.edges) {
- data.edges = data.edges.map((edge: any) => {
- const e = { ...edge }
- // 将后端的 edge.condition 转换为 LogicFlow 的 edge.properties
- const conditionMap = e.condition || {}
- e.properties = {
- condition: conditionMap.condition || '',
- isDefault: conditionMap.isDefault ?? !conditionMap.condition,
- branchName: conditionMap.branchName || '',
- }
- // 设置/清理连线文本
- const displayText = e.properties.branchName || (!e.properties.isDefault && e.properties.condition)
- if (displayText) {
- e.text = { value: displayText }
- } else {
- delete e.text
- }
- delete e.condition
- return e
- })
- }
- if (data.nodes) {
- data.nodes = data.nodes.map((node: any) => {
- const n = { ...node }
- if (n.type && typeToFrontend[n.type]) {
- n.type = typeToFrontend[n.type]
- }
- // name -> text
- if (n.name !== undefined) {
- n.text = String(n.name).replace(/ \(\+\d+\)$/, '')
- delete n.name
- }
- return n
- })
- }
- return data
- }
- function handleDrop(e: DragEvent) {
- e.preventDefault()
- const dataStr = e.dataTransfer?.getData('application/json')
- if (!dataStr || !lf || !lfContainer.value) return
- const nodeData = JSON.parse(dataStr)
- const rect = lfContainer.value.getBoundingClientRect()
- lf.addNode({
- type: nodeData.type,
- x: e.clientX - rect.left,
- y: e.clientY - rect.top,
- text: nodeData.label,
- })
- }
- function updateNodeName() {
- if (!lf || !selectedNode.value) return
- const text = selectedNode.value.text
- const value = typeof text === 'object' && text !== null ? text.value || '' : text || ''
- // 使用 LogicFlow 公开 API updateText 更新节点文本
- lf.updateText(selectedNode.value.id, value)
- }
- // 保存流程
- const saveDialogVisible = ref(false)
- const saveForm = reactive({
- code: '',
- name: '',
- category: '',
- description: '',
- })
- const saveFormRules: FormRules = {
- code: [{ required: true, message: '请输入流程编码', trigger: 'blur' }],
- name: [{ required: true, message: '请输入流程名称', trigger: 'blur' }],
- }
- const formFields = ref<FormField[]>([])
- const fieldTypeOptions = [
- { label: '文本', value: 'text' },
- { label: '多行文本', value: 'textarea' },
- { label: '数字', value: 'number' },
- { label: '日期', value: 'date' },
- { label: '下拉选择', value: 'select' },
- ]
- function addFormField() {
- formFields.value.push({ name: '', label: '', type: 'text', required: false })
- }
- function removeFormField(index: number) {
- formFields.value.splice(index, 1)
- }
- function getFormSchema(): string {
- const validFields = formFields.value.filter((f) => f.name.trim() && f.label.trim())
- return validFields.length > 0 ? JSON.stringify(validFields) : ''
- }
- function parseFormSchema(schema?: string) {
- if (!schema) return
- try {
- const parsed = JSON.parse(schema)
- if (Array.isArray(parsed)) {
- formFields.value = parsed
- }
- } catch {
- formFields.value = []
- }
- }
- // 下拉选项编辑
- const optionDialogVisible = ref(false)
- const optionEditingIndex = ref<number | null>(null)
- const optionEditingField = ref<FormField | null>(null)
- const optionList = ref<FormFieldOption[]>([])
- function openOptionDialog(index: number) {
- optionEditingIndex.value = index
- optionEditingField.value = formFields.value[index]
- const rawOptions = optionEditingField.value.options || []
- optionList.value = rawOptions.map((opt: any) => {
- if (typeof opt === 'string') {
- return { label: opt, value: opt }
- }
- return { label: String(opt.label ?? opt.value), value: opt.value }
- })
- optionDialogVisible.value = true
- }
- function addOption() {
- optionList.value.push({ label: '', value: '' })
- }
- function removeOption(index: number) {
- optionList.value.splice(index, 1)
- }
- function saveOptions() {
- if (optionEditingIndex.value === null || !optionEditingField.value) return
- const validOptions = optionList.value.filter(
- (opt) => String(opt.label).trim() !== '' && String(opt.value).trim() !== ''
- )
- formFields.value[optionEditingIndex.value].options = validOptions
- formFields.value[optionEditingIndex.value].multiple = optionEditingField.value.multiple
- optionDialogVisible.value = false
- }
- function validateFlow(graphData: any): string | null {
- const nodes = graphData.nodes || []
- const edges = graphData.edges || []
- if (nodes.length === 0) return '流程图不能为空'
- const startNodes = nodes.filter((n: any) => n.type === 'start-node' || n.type === 'start')
- const endNodes = nodes.filter((n: any) => n.type === 'end-node' || n.type === 'end')
- if (startNodes.length === 0) return '流程图缺少开始节点'
- if (endNodes.length === 0) return '流程图缺少结束节点'
- if (startNodes.length > 1) return '流程图只能有一个开始节点'
- // 检查审批节点是否配置了审批对象
- const externalNodeKeys = new Set<string>()
- for (const node of nodes) {
- if (node.type === 'approval-node' || node.type === 'approval') {
- const props = node.properties || {}
- const assigneeType = props.assigneeType ?? 'ROLE'
- const assigneeValue = props.assigneeValue || props.approver
- if (assigneeType === 'ROLE' && !assigneeValue && props.subNodesEnabled !== true) {
- return `审批节点 ${node.text || node.name} 未选择审批角色`
- }
- if (assigneeType === 'EXTERNAL') {
- if (props.subNodesEnabled === true) {
- return `审批节点 "${node.text || node.name}" 使用外部指定审批人时不支持子节点`
- }
- const nodeKey = String(props.nodeKey || '').trim()
- if (!nodeKey) {
- return `审批节点 "${node.text || node.name}" 未填写节点编码(nodeKey)`
- }
- if (externalNodeKeys.has(nodeKey)) {
- return `节点编码 "${nodeKey}" 重复,请保证每个外部指定节点编码唯一`
- }
- externalNodeKeys.add(nodeKey)
- }
- if (props.subNodesEnabled === true) {
- const subNodes = props.subNodes || []
- if (subNodes.length === 0) {
- return `审批节点 "${node.text || node.name}" 已启用子节点,请至少配置一个子节点`
- }
- for (let i = 0; i < subNodes.length; i++) {
- const sub = subNodes[i]
- const subAssigneeType = sub.assigneeType ?? 'ROLE'
- const subAssigneeValue = sub.assigneeValue
- if (subAssigneeType === 'ROLE' && !subAssigneeValue) {
- return `审批节点 ${node.text || node.name} 的子节点 ${i + 1} 未选择审批角色`
- }
- }
- }
- }
- }
- // 检查孤立节点
- const connectedNodeIds = new Set<string>()
- for (const edge of edges) {
- connectedNodeIds.add(edge.sourceNodeId)
- connectedNodeIds.add(edge.targetNodeId)
- }
- for (const node of nodes) {
- if (
- !connectedNodeIds.has(node.id) &&
- node.type !== 'start-node' &&
- node.type !== 'start' &&
- node.type !== 'end-node' &&
- node.type !== 'end'
- ) {
- return `节点 "${node.text || node.name}" 是孤立节点,请删除或连接`
- }
- }
- // 检查条件分支:有且只能有一个默认分支,非默认分支必须填写条件
- const sourceMap: Record<string, any[]> = {}
- for (const edge of edges) {
- if (!sourceMap[edge.sourceNodeId]) sourceMap[edge.sourceNodeId] = []
- sourceMap[edge.sourceNodeId].push(edge)
- }
- for (const node of nodes) {
- if (node.type === 'condition-node' || node.type === 'condition') {
- const outEdges = sourceMap[node.id] || []
- if (outEdges.length > 1) {
- const defaultEdges = outEdges.filter((e: any) => {
- const props = e.properties || {}
- return props.isDefault || (!props.condition && !e.condition)
- })
- if (defaultEdges.length === 0) {
- return `条件节点 "${node.text || node.name}" 需要至少一条默认分支`
- }
- if (defaultEdges.length > 1) {
- return `条件节点 "${node.text || node.name}" 只能有一条默认分支`
- }
- const invalid = outEdges.some((e: any) => {
- const props = e.properties || {}
- return !props.isDefault && !props.condition && !e.condition
- })
- if (invalid) {
- return `条件节点 "${node.text || node.name}" 的非默认分支必须填写条件表达式`
- }
- }
- }
- }
- return null
- }
- async function handleSave() {
- if (!lf) return
- saveCurrentNodeProps()
- // 先刷新属性面板中可能未失焦的节点名称修改,再读取画布数据
- updateNodeName()
- const rawGraphData = lf.getGraphData()
- const error = validateFlow(rawGraphData)
- if (error) {
- ElMessage.warning(error)
- return
- }
- // 统一弹出保存弹窗,方便配置流程表单字段
- if (mode.value === 'create') {
- saveForm.code = flowCode.value
- saveForm.name = flowName.value
- saveForm.category = flowCategory.value
- saveForm.description = flowDescription.value
- formFields.value = []
- } else if (mode.value === 'edit' && flowId.value) {
- saveForm.code = flowCode.value
- saveForm.name = flowName.value
- saveForm.category = flowCategory.value
- saveForm.description = flowDescription.value
- // formFields 在编辑模式加载流程时已通过 parseFormSchema 填充
- } else {
- saveForm.code = ''
- saveForm.name = ''
- saveForm.category = ''
- saveForm.description = ''
- formFields.value = []
- }
- saveDialogVisible.value = true
- }
- async function submitSave() {
- if (!lf) return
- saveCurrentNodeProps()
- // 先刷新属性面板中可能未失焦的节点名称修改,再读取画布数据
- updateNodeName()
- const rawGraphData = lf.getGraphData()
- const error = validateFlow(rawGraphData)
- if (error) {
- ElMessage.warning(error)
- return
- }
- const graphData = convertToBackendFormat(rawGraphData)
- if (mode.value === 'create') {
- const data: Partial<FlowDefinition> = {
- code: saveForm.code,
- name: saveForm.name,
- category: saveForm.category,
- description: saveForm.description,
- formSchema: getFormSchema(),
- flowJson: JSON.stringify(graphData),
- status: 0,
- }
- await addDefinition(data)
- ElMessage.success('流程新增成功')
- saveDialogVisible.value = false
- router.push('/flow/definition')
- } else if (mode.value === 'edit' && flowId.value) {
- const data: Partial<FlowDefinition> = {
- id: Number(flowId.value),
- code: saveForm.code,
- name: saveForm.name,
- category: saveForm.category,
- description: saveForm.description,
- formSchema: getFormSchema(),
- flowJson: JSON.stringify(graphData),
- }
- const newId = await updateDefinition(data)
- if (newId !== Number(flowId.value)) {
- ElMessage.success('流程已创建为新版本,请重新发布')
- } else {
- ElMessage.success('流程修改成功')
- }
- saveDialogVisible.value = false
- router.push('/flow/definition')
- } else {
- const data: Partial<FlowDefinition> = {
- code: saveForm.code,
- name: saveForm.name,
- category: saveForm.category,
- description: saveForm.description,
- formSchema: getFormSchema(),
- flowJson: JSON.stringify(graphData),
- status: 0,
- }
- await addDefinition(data)
- ElMessage.success('流程保存成功')
- saveDialogVisible.value = false
- router.push('/flow/definition')
- }
- }
- function deleteSelectedNode() {
- if (!lf || !selectedNode.value) return
- lf.deleteNode(selectedNode.value.id)
- selectedNode.value = null
- outgoingEdges.value = []
- }
- function selectNodeById(nodeId: string) {
- if (!lf) return
- try {
- const nodeData = lf.getNodeData(nodeId)
- if (!nodeData) return
- lf.selectElementById(nodeId)
- saveCurrentNodeProps()
- selectedNode.value = nodeData
- loadNodeProps(nodeData)
- loadOutgoingEdges(nodeData.id)
- } catch {
- // ignore
- }
- }
- function handleClear() {
- if (!lf) return
- lf.clearData()
- selectedNode.value = null
- }
- function goBack() {
- router.push('/flow/definition')
- }
- async function loadRoles() {
- try {
- const res = await listRole({ pageNum: 1, pageSize: 100 })
- roleList.value = res.list
- } catch {
- /* ignore */
- }
- }
- onMounted(() => {
- // 防止快速进入/离开设计器时重复创建 LogicFlow 实例(v1.x 无 destroy 方法)
- if (lf) {
- if (typeof lf.destroy === 'function') {
- lf.destroy()
- }
- lf = null
- }
- nextTick(initLogicFlow)
- loadRoles()
- })
- onUnmounted(() => {
- if (lf) {
- if (typeof lf.destroy === 'function') {
- lf.destroy()
- }
- lf = null
- }
- selectedNode.value = null
- })
- return {
- // flow info
- mode,
- flowId,
- flowCode,
- flowName,
- flowCategory,
- flowDescription,
- flowInfoVisible,
- // logic flow / selection
- selectedNode,
- outgoingEdges,
- roleList,
- nodeProps,
- lfRef,
- // property panel helpers
- loadOutgoingEdges,
- getEdgeTargetName,
- onDefaultChange,
- onBranchBlur,
- saveCurrentNodeProps,
- loadNodeProps,
- updateNodeName,
- generateSubNodeId,
- deleteSelectedNode,
- selectNodeById,
- // save dialog
- saveDialogVisible,
- saveForm,
- saveFormRules,
- formFields,
- fieldTypeOptions,
- addFormField,
- removeFormField,
- getFormSchema,
- parseFormSchema,
- // option dialog
- optionDialogVisible,
- optionEditingIndex,
- optionEditingField,
- optionList,
- openOptionDialog,
- addOption,
- removeOption,
- saveOptions,
- // toolbar actions
- handleSave,
- submitSave,
- handleClear,
- goBack,
- // drag/drop
- handleDrop,
- }
- }
|