feat: 设置-流程设置 90%

This commit is contained in:
Zzz1zz
2026-01-10 11:39:33 +08:00
14 changed files with 2964 additions and 22 deletions
+1 -1
View File
@@ -8,7 +8,7 @@
![Vite](https://img.shields.io/badge/Vite-3.2.5-646CFF?style=flat-square&logo=vite)
![Pinia](https://img.shields.io/badge/Pinia-2.0.23-FFD93D?style=flat-square)
一个基于 Vue 3 + TypeScript + Arco Design 的现代化租赁管理系统
一个基于 Vue 3 + TypeScript + Arco Design 的现代化租赁管理系统
</div>
+4
View File
@@ -35,6 +35,10 @@
"dependencies": {
"@arco-design/web-vue": "^2.57.0",
"@onlyoffice/document-editor-vue": "^1.6.1",
"@vue-flow/background": "^1.3.2",
"@vue-flow/core": "^1.47.0",
"@vue-flow/controls": "^1.1.3",
"@vue-flow/minimap": "^1.5.4",
"@vueuse/core": "^9.3.0",
"arco-design-pro-vue": "^2.7.3",
"axios": "^0.24.0",
+170
View File
@@ -19,6 +19,18 @@ importers:
'@onlyoffice/document-editor-vue':
specifier: ^1.6.1
version: 1.6.1(vue@3.5.22(typescript@4.9.5))
'@vue-flow/background':
specifier: ^1.3.2
version: 1.3.2(@vue-flow/core@1.48.0(vue@3.5.22(typescript@4.9.5)))(vue@3.5.22(typescript@4.9.5))
'@vue-flow/controls':
specifier: ^1.1.3
version: 1.1.3(@vue-flow/core@1.48.0(vue@3.5.22(typescript@4.9.5)))(vue@3.5.22(typescript@4.9.5))
'@vue-flow/core':
specifier: ^1.47.0
version: 1.48.0(vue@3.5.22(typescript@4.9.5))
'@vue-flow/minimap':
specifier: ^1.5.4
version: 1.5.4(@vue-flow/core@1.48.0(vue@3.5.22(typescript@4.9.5)))(vue@3.5.22(typescript@4.9.5))
'@vueuse/core':
specifier: ^9.3.0
version: 9.13.0(vue@3.5.22(typescript@4.9.5))
@@ -809,6 +821,9 @@ packages:
'@types/web-bluetooth@0.0.16':
resolution: {integrity: sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==}
'@types/web-bluetooth@0.0.20':
resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==}
'@typescript-eslint/eslint-plugin@8.46.0':
resolution: {integrity: sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -997,6 +1012,29 @@ packages:
'@volar/typescript@1.11.1':
resolution: {integrity: sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==}
'@vue-flow/background@1.3.2':
resolution: {integrity: sha512-eJPhDcLj1wEo45bBoqTXw1uhl0yK2RaQGnEINqvvBsAFKh/camHJd5NPmOdS1w+M9lggc9igUewxaEd3iCQX2w==}
peerDependencies:
'@vue-flow/core': ^1.23.0
vue: ^3.3.0
'@vue-flow/controls@1.1.3':
resolution: {integrity: sha512-XCf+G+jCvaWURdFlZmOjifZGw3XMhN5hHlfMGkWh9xot+9nH9gdTZtn+ldIJKtarg3B21iyHU8JjKDhYcB6JMw==}
peerDependencies:
'@vue-flow/core': ^1.23.0
vue: ^3.3.0
'@vue-flow/core@1.48.0':
resolution: {integrity: sha512-keW9HGaEZEe4SKYtrzp5E+qSGJ5/z+9i2yRDtCr3o72IUnS0Ns1qQNsIbGGz0ygpKzg6LdtbVLWeYAvl3dzLQA==}
peerDependencies:
vue: ^3.3.0
'@vue-flow/minimap@1.5.4':
resolution: {integrity: sha512-l4C+XTAXnRxsRpUdN7cAVFBennC1sVRzq4bDSpVK+ag7tdMczAnhFYGgbLkUw3v3sY6gokyWwMl8CDonp8eB2g==}
peerDependencies:
'@vue-flow/core': ^1.23.0
vue: ^3.3.0
'@vue/babel-helper-vue-transform-on@1.5.0':
resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==}
@@ -1062,12 +1100,21 @@ packages:
'@vue/shared@3.5.22':
resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==}
'@vueuse/core@10.11.1':
resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==}
'@vueuse/core@9.13.0':
resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
'@vueuse/metadata@10.11.1':
resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==}
'@vueuse/metadata@9.13.0':
resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
'@vueuse/shared@10.11.1':
resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==}
'@vueuse/shared@9.13.0':
resolution: {integrity: sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==}
@@ -1553,6 +1600,44 @@ packages:
engines: {node: '>=10'}
hasBin: true
d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
d3-dispatch@3.0.1:
resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
engines: {node: '>=12'}
d3-drag@3.0.0:
resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
engines: {node: '>=12'}
d3-ease@3.0.1:
resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
engines: {node: '>=12'}
d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-selection@3.0.0:
resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
engines: {node: '>=12'}
d3-timer@3.0.1:
resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
engines: {node: '>=12'}
d3-transition@3.0.1:
resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
engines: {node: '>=12'}
peerDependencies:
d3-selection: 2 - 3
d3-zoom@3.0.0:
resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
engines: {node: '>=12'}
dargs@7.0.0:
resolution: {integrity: sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==}
engines: {node: '>=8'}
@@ -5374,6 +5459,8 @@ snapshots:
'@types/web-bluetooth@0.0.16': {}
'@types/web-bluetooth@0.0.20': {}
'@typescript-eslint/eslint-plugin@8.46.0(@typescript-eslint/parser@8.46.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)':
dependencies:
'@eslint-community/regexpp': 4.12.1
@@ -5556,6 +5643,34 @@ snapshots:
'@volar/language-core': 1.11.1
path-browserify: 1.0.1
'@vue-flow/background@1.3.2(@vue-flow/core@1.48.0(vue@3.5.22(typescript@4.9.5)))(vue@3.5.22(typescript@4.9.5))':
dependencies:
'@vue-flow/core': 1.48.0(vue@3.5.22(typescript@4.9.5))
vue: 3.5.22(typescript@4.9.5)
'@vue-flow/controls@1.1.3(@vue-flow/core@1.48.0(vue@3.5.22(typescript@4.9.5)))(vue@3.5.22(typescript@4.9.5))':
dependencies:
'@vue-flow/core': 1.48.0(vue@3.5.22(typescript@4.9.5))
vue: 3.5.22(typescript@4.9.5)
'@vue-flow/core@1.48.0(vue@3.5.22(typescript@4.9.5))':
dependencies:
'@vueuse/core': 10.11.1(vue@3.5.22(typescript@4.9.5))
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-zoom: 3.0.0
vue: 3.5.22(typescript@4.9.5)
transitivePeerDependencies:
- '@vue/composition-api'
'@vue-flow/minimap@1.5.4(@vue-flow/core@1.48.0(vue@3.5.22(typescript@4.9.5)))(vue@3.5.22(typescript@4.9.5))':
dependencies:
'@vue-flow/core': 1.48.0(vue@3.5.22(typescript@4.9.5))
d3-selection: 3.0.0
d3-zoom: 3.0.0
vue: 3.5.22(typescript@4.9.5)
'@vue/babel-helper-vue-transform-on@1.5.0': {}
'@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.4)':
@@ -5673,6 +5788,16 @@ snapshots:
'@vue/shared@3.5.22': {}
'@vueuse/core@10.11.1(vue@3.5.22(typescript@4.9.5))':
dependencies:
'@types/web-bluetooth': 0.0.20
'@vueuse/metadata': 10.11.1
'@vueuse/shared': 10.11.1(vue@3.5.22(typescript@4.9.5))
vue-demi: 0.14.10(vue@3.5.22(typescript@4.9.5))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/core@9.13.0(vue@3.5.22(typescript@4.9.5))':
dependencies:
'@types/web-bluetooth': 0.0.16
@@ -5683,8 +5808,17 @@ snapshots:
- '@vue/composition-api'
- vue
'@vueuse/metadata@10.11.1': {}
'@vueuse/metadata@9.13.0': {}
'@vueuse/shared@10.11.1(vue@3.5.22(typescript@4.9.5))':
dependencies:
vue-demi: 0.14.10(vue@3.5.22(typescript@4.9.5))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
'@vueuse/shared@9.13.0(vue@3.5.22(typescript@4.9.5))':
dependencies:
vue-demi: 0.14.10(vue@3.5.22(typescript@4.9.5))
@@ -6227,6 +6361,42 @@ snapshots:
bin-build: 3.0.0
bin-wrapper: bin-wrapper-china@0.1.0
d3-color@3.1.0: {}
d3-dispatch@3.0.1: {}
d3-drag@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-selection: 3.0.0
d3-ease@3.0.1: {}
d3-interpolate@3.0.1:
dependencies:
d3-color: 3.1.0
d3-selection@3.0.0: {}
d3-timer@3.0.1: {}
d3-transition@3.0.1(d3-selection@3.0.0):
dependencies:
d3-color: 3.1.0
d3-dispatch: 3.0.1
d3-ease: 3.0.1
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-timer: 3.0.1
d3-zoom@3.0.0:
dependencies:
d3-dispatch: 3.0.1
d3-drag: 3.0.0
d3-interpolate: 3.0.1
d3-selection: 3.0.0
d3-transition: 3.0.1(d3-selection@3.0.0)
dargs@7.0.0: {}
data-view-buffer@1.0.2:
+8
View File
@@ -76,3 +76,11 @@ export function getBanksList(keyword: string): Promise<BankListRes> {
export function getDeptProjectListFromDeptId(id: number): Promise<any> {
return axios.get(`common/getDeptProjectList?departmentId=${id}`);
}
// 审批流类型列表
export function getWorkflowsTypeList(): Promise<any> {
return axios.get(`common/getWorkflowsTypeList`);
}
// 公共接口-用户列表
export function getCommonUserList(): Promise<any> {
return axios.get(`common/getUserList`);
}
+20
View File
@@ -548,3 +548,23 @@ export function getContractTags(
export function getWorkflowsList(type: number): Promise<any> {
return axios.get(`workflows/getList?type=${type}`);
}
// 创建流程
export function createWorkflows(params: any): Promise<any> {
return axios.post(`workflows/create`, params);
}
// 保存流程
export function saveWorkflows(params: any): Promise<any> {
return axios.post(`workflows/save`, params);
}
// 审批流详情
export function getWorkflowsDetail(id: number): Promise<any> {
return axios.get(`workflows/getDetail?id=${id}`);
}
// 审批流启用禁用
export function toggleWorkflowsDisable(params: any): Promise<any> {
return axios.post('workflows/disable', params);
}
// 删除审批流
export function delWorkflows(id: number): Promise<any> {
return axios.get(`workflows/del?id=${id}`);
}
+61
View File
@@ -0,0 +1,61 @@
declare module '@vue-flow/core' {
import type { Component } from 'vue';
export type Position = 'left' | 'right' | 'top' | 'bottom';
export interface XYPosition {
x: number;
y: number;
}
export interface Node<Data = any> {
id: string;
position: XYPosition;
data?: Data;
type?: string;
style?: Record<string, any>;
class?: string;
width?: number;
height?: number;
sourcePosition?: Position;
targetPosition?: Position;
draggable?: boolean;
connectable?: boolean;
}
export interface Edge<Data = any> {
id: string;
source: string;
target: string;
type?: string;
animated?: boolean;
data?: Data;
style?: Record<string, any>;
}
export const VueFlow: Component;
export const Background: Component;
export const MiniMap: Component;
export const Handle: Component;
export const Position: {
Left: 'left';
Right: 'right';
Top: 'top';
Bottom: 'bottom';
};
export function useVueFlow(): any;
}
declare module '@vue-flow/controls' {
import type { Component } from 'vue';
export const Controls: Component;
}
declare module '@vue-flow/minimap' {
import type { Component } from 'vue';
export const MiniMap: Component;
}
declare module '@vue-flow/background' {
import type { Component } from 'vue';
export const Background: Component;
}
@@ -0,0 +1,551 @@
<template>
<div class="node-config-panel" :class="{ 'is-modal': isModal }">
<div v-if="!isModal" class="panel-header">
<div class="panel-title">{{ panelTitle }}</div>
</div>
<div class="panel-content">
<!-- 条件节点配置 -->
<template v-if="nodeType === 'condition'">
<div class="config-section">
<div class="section-title">条件设置</div>
<a-form :model="conditionForm" layout="vertical" auto-label-width>
<a-form-item label="条件类型" field="fieldType">
<a-select
v-model="conditionForm.fieldType"
placeholder="请选择条件类型"
@change="handleConditionChange"
>
<a-option
v-for="option in fieldTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item
v-if="conditionForm.fieldType === 4"
label="发起人"
field="contractApprovers"
>
<a-select
v-model="conditionForm.contractApprovers"
:options="userList"
:field-names="{ value: 'id', label: 'realname' }"
placeholder="请选择发起人"
multiple
allow-clear
/>
</a-form-item>
<a-form-item
v-if="conditionForm.fieldType !== 4"
label="条件判断"
field="operatorType"
>
<a-select
v-model="conditionForm.operatorType"
placeholder="请选择运算类型"
>
<a-option
v-for="option in operatorTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item
v-if="conditionForm.fieldType === 2"
label="使用条款类型"
field="termType"
>
<a-select
v-model="conditionForm.termType"
placeholder="请选择使用条款类型"
>
<a-option
v-for="option in termTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
<a-form-item
v-if="
conditionForm.fieldType === 2 || conditionForm.fieldType === 3
"
label="值"
field="amount"
>
<a-input-number
v-model="conditionForm.amount"
:precision="2"
:min="0"
placeholder="请输入金额"
style="width: 100%"
/>
</a-form-item>
<a-form-item
v-if="conditionForm.fieldType === 1"
label="值"
field="area"
>
<div class="flex items-center gap-2">
<a-input-number
v-model="conditionForm.area"
:precision="2"
:min="0"
placeholder="请输入面积"
style="flex: 1"
/>
<a-select v-model="conditionForm.unit" style="width: 80px">
<a-option :value="1">元</a-option>
<a-option :value="2">元/㎡*月</a-option>
<a-option :value="3">㎡</a-option>
</a-select>
</div>
</a-form-item>
<a-form-item
v-if="
conditionForm.fieldType === 5 || conditionForm.fieldType === 6
"
label="值"
field="days"
>
<a-input-number
v-model="conditionForm.days"
:min="0"
placeholder="请输入天数"
style="width: 100%"
/>
</a-form-item>
</a-form>
</div>
<div class="config-tip">
<div class="tip-text"
>条件设置有:
面积、合同(租赁)单价、合同总价、发起人、免租期、装修期</div
>
</div>
</template>
<!-- 审批人节点配置 -->
<template v-if="nodeType === 'approver'">
<div class="config-section">
<a-form :model="approverForm" layout="vertical" auto-label-width>
<a-form-item label="选择审批人" field="approvers">
<a-select
v-model="approverForm.approvers"
:options="userList"
:field-names="{ value: 'id', label: 'realname' }"
placeholder="请选择审批人"
multiple
allow-clear
/>
</a-form-item>
<a-form-item label="通过方式" field="approvalType">
<a-radio-group v-model="approverForm.approvalType">
<a-radio
v-for="option in approvalTypeOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="审批不通过操作" field="rejectAction">
<a-select
v-model="approverForm.rejectAction"
placeholder="请选择"
>
<a-option
v-for="option in rejectActionOptions"
:key="option.value"
:value="option.value"
>
{{ option.label }}
</a-option>
</a-select>
</a-form-item>
</a-form>
</div>
<!-- 如果审批人为空,显示抄送人配置选项(用于支持从审批人节点转换为抄送节点) -->
<div v-if="approverForm.approvers.length === 0" class="config-section">
<div class="section-title">或配置为抄送节点</div>
<a-form :model="ccForm" layout="vertical" auto-label-width>
<a-form-item label="选择抄送人" field="ccto">
<a-select
v-model="ccForm.ccto"
:options="userList"
:field-names="{ value: 'id', label: 'realname' }"
placeholder="请选择抄送人"
multiple
allow-clear
/>
</a-form-item>
</a-form>
</div>
</template>
<!-- 抄送节点配置 -->
<template v-if="nodeType === 'cc'">
<div class="config-section">
<a-form :model="ccForm" layout="vertical" auto-label-width>
<a-form-item label="选择抄送人" field="ccto">
<a-select
v-model="ccForm.ccto"
:options="userList"
:field-names="{ value: 'id', label: 'realname' }"
placeholder="请选择抄送人"
multiple
allow-clear
/>
</a-form-item>
</a-form>
</div>
</template>
</div>
<div v-if="!isModal" class="panel-footer">
<a-button type="primary" @click="handleSave">保存</a-button>
<a-button type="outline" status="danger" @click="handleDelete"
>删除节点</a-button
>
</div>
<div v-else class="panel-footer-modal">
<a-button type="outline" status="danger" @click="handleDelete"
>删除节点</a-button
>
<a-button type="primary" @click="handleSave">保存</a-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import type { Node } from '@vue-flow/core';
import { getCommonUserList } from '@/api/common';
const props = defineProps<{
node: Node | null;
isModal?: boolean;
}>();
const emit = defineEmits<{
(e: 'save', data: any): void;
(e: 'delete'): void;
}>();
const userList = ref<any[]>([]);
const nodeType = computed(() => props.node?.type || '');
// 条件类型选项
const fieldTypeOptions = [
{ value: 1, label: '面积' },
{ value: 2, label: '合同单价' },
{ value: 3, label: '合同总价' },
{ value: 4, label: '发起人' },
{ value: 5, label: '免租期' },
{ value: 6, label: '装修期' },
];
// 运算类型选项
const operatorTypeOptions = [
{ value: 1, label: '大于(>)' },
{ value: 2, label: '大于等于(>=)' },
{ value: 3, label: '小于(<)' },
{ value: 4, label: '小于等于(<=)' },
{ value: 5, label: '等于(=)' },
{ value: 6, label: '区间值' },
];
// 使用条款类型选项
const termTypeOptions = [
{ value: 1, label: '租赁' },
{ value: 2, label: '物业' },
];
// 单位选项
const unitOptions = [
{ value: 1, label: '元' },
{ value: 2, label: '元/㎡*月' },
{ value: 3, label: '㎡' },
];
// 审批方式选项
const approvalTypeOptions = [
{ value: 1, label: '会签' },
{ value: 2, label: '或签' },
];
// 审批不通过操作选项
const rejectActionOptions = [
{ value: 1, label: '退回发起人' },
{ value: 2, label: '终止流程' },
];
const panelTitle = computed(() => {
if (nodeType.value === 'condition') return '条件判断';
if (nodeType.value === 'approver') return '审批人';
if (nodeType.value === 'cc') return '抄送人';
return '';
});
// 条件表单
const conditionForm = ref<any>({
fieldType: undefined,
operatorType: undefined,
termType: 0,
amount: '',
area: '',
unit: 3,
days: undefined,
contractApprovers: [],
});
// 审批人表单
const approverForm = ref<any>({
approvers: [],
approvalType: 2,
rejectAction: 1,
});
// 抄送表单
const ccForm = ref<any>({
ccto: [],
});
// 监听节点变化,更新表单
watch(
() => props.node,
(node) => {
if (!node) return;
if (node.type === 'condition') {
const firstRule = node.data?.rules?.[0] || {};
// 处理数值:接口返回的可能是字符串,需要转换为数字
const amount = firstRule.amount;
const area = firstRule.area;
const days = firstRule.days;
conditionForm.value = {
fieldType: firstRule.fieldType,
operatorType: firstRule.operatorType,
termType: firstRule.termType || 0,
// 转换数值:如果是字符串且不为空,转换为数字;如果是数字,直接使用;否则为空字符串
amount:
amount !== undefined && amount !== null && amount !== ''
? typeof amount === 'string'
? parseFloat(amount) || ''
: amount
: '',
area:
area !== undefined && area !== null && area !== ''
? typeof area === 'string'
? parseFloat(area) || ''
: area
: '',
unit: firstRule.unit || 3,
days:
days !== undefined && days !== null && days !== ''
? typeof days === 'string'
? parseInt(days, 10) || undefined
: days
: undefined,
contractApprovers:
firstRule.contractApprovers?.map((u: any) => u.id) || [],
};
} else if (node.type === 'approver') {
approverForm.value = {
approvers: node.data?.approvers?.map((u: any) => u.id) || [],
approvalType: node.data?.approvalType || 2,
rejectAction: node.data?.rejectAction || 1,
};
// 如果审批人节点有 ccto 数据,也加载到表单中(用于支持从审批人节点转换为抄送节点)
if (node.data?.ccto && node.data.ccto.length > 0) {
ccForm.value = {
ccto: node.data.ccto.map((u: any) => u.id) || [],
};
}
} else if (node.type === 'cc') {
ccForm.value = {
ccto: node.data?.ccto?.map((u: any) => u.id) || [],
};
}
},
{ immediate: true }
);
const handleConditionChange = () => {
// 切换条件类型时重置相关字段
conditionForm.value.operatorType = undefined;
conditionForm.value.amount = '';
conditionForm.value.area = '';
conditionForm.value.unit = 3;
conditionForm.value.days = undefined;
conditionForm.value.contractApprovers = [];
conditionForm.value.termType = 0;
};
const handleSave = () => {
if (nodeType.value === 'condition') {
// 获取选中的用户信息
const selectedUsers = conditionForm.value.contractApprovers
.map((id: number) => userList.value.find((u) => u.id === id))
.filter(Boolean);
const ruleData = {
...conditionForm.value,
contractApprovers: selectedUsers,
workflowsSettingNodesId: props.node?.data?.nodeId,
};
emit('save', {
rules: [ruleData],
});
} else if (nodeType.value === 'approver') {
// 获取选中的用户信息
const selectedUsers = approverForm.value.approvers
.map((id: number) => userList.value.find((u) => u.id === id))
.filter(Boolean);
// 如果审批人为空,但配置了抄送人,也保存抄送人数据(用于支持从审批人节点转换为抄送节点)
const selectedCcUsers = ccForm.value.ccto
.map((id: number) => userList.value.find((u) => u.id === id))
.filter(Boolean);
emit('save', {
...approverForm.value,
approvers: selectedUsers,
// 如果审批人为空且有抄送人,保存抄送人数据
...(selectedUsers.length === 0 && selectedCcUsers.length > 0
? { ccto: selectedCcUsers }
: {}),
});
} else if (nodeType.value === 'cc') {
// 获取选中的用户信息
const selectedUsers = ccForm.value.ccto
.map((id: number) => userList.value.find((u) => u.id === id))
.filter(Boolean);
emit('save', {
...ccForm.value,
ccto: selectedUsers,
});
}
// 如果是弹窗模式,保存后自动关闭
if (props.isModal) {
// 由父组件处理关闭
}
};
const handleDelete = () => {
emit('delete');
// 删除后如果是弹窗模式,需要关闭弹窗(由父组件处理)
};
onMounted(() => {
getUserListData();
});
async function getUserListData() {
try {
const { code, data } = await getCommonUserList();
if (code === 200) {
// 兼容不同的数据结构:可能是 data.user 或 data.userList 或直接是 data
userList.value =
data?.user || data?.userList || (Array.isArray(data) ? data : []);
}
} catch (error) {
// console.error('获取用户列表失败:', error);
}
}
</script>
<style lang="less" scoped>
.node-config-panel {
height: 100%;
display: flex;
flex-direction: column;
background: #fff;
border-left: 1px solid #e6e6eb;
}
.panel-header {
padding: 16px 20px;
border-bottom: 1px solid #e6e6eb;
}
.panel-title {
font-size: 16px;
font-weight: 500;
color: #1d2129;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.config-section {
margin-bottom: 20px;
}
.section-title {
font-size: 14px;
font-weight: 500;
color: #1d2129;
margin-bottom: 16px;
}
.config-tip {
padding: 12px;
background: #f7f8fa;
border-radius: 4px;
margin-top: 20px;
}
.tip-text {
font-size: 12px;
color: #86909c;
line-height: 18px;
}
.panel-footer {
padding: 16px 20px;
border-top: 1px solid #e6e6eb;
display: flex;
gap: 12px;
}
.panel-footer .arco-btn {
flex: 1;
}
.panel-footer-modal {
padding: 16px 0 0 0;
display: flex;
justify-content: flex-end;
gap: 12px;
}
.node-config-panel.is-modal {
.panel-content {
max-height: 60vh;
overflow-y: auto;
}
}
</style>
@@ -0,0 +1,180 @@
<template>
<a-drawer
:width="430"
:visible="visible"
:title="isEdit ? '编辑审批流程' : '新增审批流程'"
unmount-on-close
@ok="handleOk"
@cancel="handleCancel"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
auto-label-width
layout="vertical"
>
<a-form-item label="名称" field="name" required>
<a-input
v-model="form.name"
:placeholder="$t('please.enter', { placeholder: '名称' })"
/>
</a-form-item>
<a-form-item label="审批类型" field="workflowsTypeId">
<a-select v-model="form.workflowsTypeId" placeholder="请选择审批类型">
<a-option
v-for="item in approvalType"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
<!-- 适用范围(部门) -->
<a-form-item label="适用范围" field="projectIds">
<a-select
v-model="form.projectIds"
multiple
placeholder="请选择审批类型"
>
<a-option
v-for="item in allProjetList"
:key="item.id"
:value="item.id"
>
{{ item.name }}
</a-option>
</a-select>
</a-form-item>
<a-form-item label="备注" field="remark">
<a-textarea
v-model="form.remark"
placeholder="输入详细原因"
allow-clear
/>
</a-form-item>
</a-form>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, watch, reactive } from 'vue';
import type { FormInstance } from '@arco-design/web-vue';
import { Message } from '@arco-design/web-vue';
import { createWorkflows } from '@/api/setting';
import { getAllProjectIdNameList, getWorkflowsTypeList } from '@/api/common';
const allProjetList = ref<any>([]);
const isLoading = ref<boolean>(false);
const props = defineProps<{
visible: boolean;
isEdit: boolean;
record?: any | null;
}>();
const emit = defineEmits<{
(e: 'update:visible', val: boolean): void;
(e: 'submitted'): void;
}>();
const approvalType = ref<any>([]);
// 表单数据
const formRef = ref<FormInstance>();
const defaultForm: any = {
name: '',
workflowsTypeId: null,
projectIds: null,
remark: '',
};
let form = reactive<any>({ ...defaultForm });
// 校验规则
const rules = reactive({
name: [{ required: true, message: '请输入合同编号' }],
workflowsTypeId: [{ required: true, message: '请选择审批类型' }],
projectIds: [{ required: true, message: '请选择适用范围' }],
});
watch(
() => props.visible,
(val) => {
if (val) {
if (props.isEdit && props.record) {
// 编辑模式:回显数据
Object.assign(form, {
name: props.record.name || '',
workflowsTypeId: props.record.workflowsTypeId || null,
projectIds: props.record.projectIds || null,
remark: props.record.remark || '',
});
} else {
// 新增模式:重置表单
Object.assign(form, defaultForm);
}
if (allProjetList.value?.length === 0) getAllProjectListData();
if (approvalType.value?.length === 0) getWorkflowsTypeListData();
}
},
{ immediate: true }
);
const handleOk = async () => {
if (isLoading.value) return;
try {
isLoading.value = true;
const valid = await formRef.value.validate();
if (valid) return;
const params = {
...form,
// projectIds 可能是单个值或数组,统一转为数组
projectIds: form.projectIds
? Array.isArray(form.projectIds)
? form.projectIds
: [form.projectIds]
: [],
};
// 如果是编辑模式,添加 id
if (props.isEdit && props.record?.id) {
params.id = props.record.id;
}
const res = await createWorkflows(params);
if (res?.code === 200) {
Message.success(res?.message);
form = { ...defaultForm };
emit('submitted');
emit('update:visible', false);
}
} catch (err) {
// 捕获异常,暂不处理
} finally {
isLoading.value = false;
}
};
// 部门列表
async function getAllProjectListData() {
try {
const { code, data } = await getAllProjectIdNameList();
if (code === 200) {
allProjetList.value = data?.projectsList || [];
}
} catch (error) {
// console.error('获取部门列表失败:', error);
}
}
async function getWorkflowsTypeListData() {
try {
const { code, data } = await getWorkflowsTypeList();
if (code === 200) {
approvalType.value = data.workflowsTypeList;
}
} catch (error) {}
}
const handleCancel = () => {
emit('update:visible', false);
};
</script>
<style lang="less" scoped>
:deep(label.arco-form-item-label) {
width: 100% !important;
}
</style>
@@ -0,0 +1,115 @@
<template>
<div class="approver-node" @click="handleClick">
<!-- 输入连接点(顶部) -->
<Handle type="target" :position="Position.Top" />
<div class="node-content">
<div class="node-header">
<div class="node-icon">
<icon-customer-service :size="20" />
</div>
<div class="node-label">审批</div>
</div>
<div v-if="approversText" class="node-desc">
{{ approversText }}
</div>
<div v-if="approvalTypeText" class="node-desc">
审批方式: {{ approvalTypeText }}
</div>
</div>
<!-- 输出连接点(底部) -->
<Handle type="source" :position="Position.Bottom" />
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { Handle, Position } from '@vue-flow/core';
import { IconCustomerService } from '@arco-design/web-vue/es/icon';
const props = defineProps<{
data?: {
nodeId?: number;
approvers?: any[];
approvalType?: number; // 1会签,2或签
label?: string;
};
}>();
const emit = defineEmits<{
(e: 'click', nodeId?: number): void;
}>();
const approversText = computed(() => {
const approvers = props.data?.approvers || [];
if (approvers.length > 0) {
const names = approvers.map((u: any) => u.realname || u.name).join('、');
return names;
}
return '';
});
const approvalTypeText = computed(() => {
return props.data?.approvalType === 1 ? '会签' : '或签';
});
const lastClickTime = ref(0);
const DOUBLE_CLICK_DELAY = 300;
const handleClick = () => {
const now = Date.now();
// 判断是否为双击
if (now - lastClickTime.value < DOUBLE_CLICK_DELAY) {
// 双击事件
emit('click', props.data?.nodeId);
lastClickTime.value = 0;
} else {
lastClickTime.value = now;
}
};
</script>
<style lang="less" scoped>
.approver-node {
position: relative;
padding: 12px 16px;
background: #e8ffea;
border: 2px solid #81b337;
border-radius: 8px;
min-width: 140px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(129, 179, 55, 0.3);
}
.node-content {
display: flex;
flex-direction: column;
gap: 6px;
}
.node-header {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.node-icon {
color: #81b337;
}
.node-label {
font-size: 14px;
color: #1d2129;
font-weight: 500;
}
.node-desc {
font-size: 12px;
color: #86909c;
}
}
</style>
@@ -0,0 +1,107 @@
<template>
<div class="cc-node" @click="handleClick">
<!-- 输入连接点(顶部) -->
<Handle type="target" :position="Position.Top" />
<div class="node-content">
<div class="node-header">
<div class="node-icon">
<icon-send :size="20" />
</div>
<div class="node-label">抄送</div>
</div>
<div v-if="ccText" class="node-desc">
{{ ccText }}
</div>
</div>
<!-- 输出连接点(底部) -->
<Handle type="source" :position="Position.Bottom" />
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { Handle, Position } from '@vue-flow/core';
import { IconSend } from '@arco-design/web-vue/es/icon';
const props = defineProps<{
data?: {
nodeId?: number;
ccto?: any[];
label?: string;
};
}>();
const emit = defineEmits<{
(e: 'click', nodeId?: number): void;
}>();
const ccText = computed(() => {
const ccto = props.data?.ccto || [];
if (ccto.length > 0) {
const names = ccto.map((u: any) => u.realname || u.name).join('、');
return names;
}
return '';
});
const lastClickTime = ref(0);
const DOUBLE_CLICK_DELAY = 300;
const handleClick = () => {
const now = Date.now();
// 判断是否为双击
if (now - lastClickTime.value < DOUBLE_CLICK_DELAY) {
// 双击事件
emit('click', props.data?.nodeId);
lastClickTime.value = 0;
} else {
lastClickTime.value = now;
}
};
</script>
<style lang="less" scoped>
.cc-node {
position: relative;
padding: 12px 16px;
background: #e8ffea;
border: 2px solid #81b337;
border-radius: 8px;
min-width: 140px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(129, 179, 55, 0.3);
}
.node-content {
display: flex;
flex-direction: column;
gap: 6px;
}
.node-header {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.node-icon {
color: #81b337;
}
.node-label {
font-size: 14px;
color: #1d2129;
font-weight: 500;
}
.node-desc {
font-size: 12px;
color: #86909c;
}
}
</style>
@@ -0,0 +1,171 @@
<template>
<div class="condition-node" @click="handleClick">
<!-- 输入连接点(顶部) -->
<Handle type="target" :position="Position.Top" />
<div class="node-content">
<div class="node-header">
<div class="node-icon">
<icon-share-alt :size="20" />
</div>
<div class="node-label">{{ conditionTitle }}</div>
</div>
<div v-if="conditionText" class="node-desc">
{{ conditionText }}
</div>
</div>
<!-- 输出连接点(底部) -->
<Handle type="source" :position="Position.Bottom" />
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from 'vue';
import { Handle, Position } from '@vue-flow/core';
import { IconShareAlt } from '@arco-design/web-vue/es/icon';
const props = defineProps<{
data?: {
nodeId?: number;
rules?: any[];
label?: string;
};
}>();
const emit = defineEmits<{
(e: 'click', nodeId?: number): void;
}>();
const fieldTypeMap: Record<number, string> = {
1: '面积',
2: '合同单价',
3: '合同总价',
4: '发起人',
5: '免租期',
6: '装修期',
};
const operatorTypeMap: Record<number, string> = {
1: '大于(>)',
2: '大于等于(>=)',
3: '小于(<)',
4: '小于等于(<=)',
5: '等于(=)',
6: '区间值',
};
const unitMap: Record<number, string> = {
1: '元',
2: '元/㎡*月',
3: '㎡',
};
const conditionTitle = computed(() => {
const firstRule = props.data?.rules?.[0];
if (firstRule) {
const fieldType = fieldTypeMap[firstRule.fieldType] || '条件';
return `条件:判断${fieldType}`;
}
return '条件';
});
const conditionText = computed(() => {
const firstRule = props.data?.rules?.[0];
if (!firstRule) return '';
const fieldType = fieldTypeMap[firstRule.fieldType];
if (!fieldType) return '';
if (firstRule.fieldType === 4) {
// 发起人
const approvers = firstRule.contractApprovers || [];
if (approvers.length > 0) {
const names = approvers
.map((u: any) => u.realname || u.name)
.join('、');
return `发起人: ${names}`;
}
} else {
// 其他类型
const operator = operatorTypeMap[firstRule.operatorType] || '';
let value = '';
if (firstRule.fieldType === 1) {
// 面积
value = `${firstRule.area || ''}${unitMap[firstRule.unit] || '㎡'}`;
} else if (firstRule.fieldType === 2 || firstRule.fieldType === 3) {
// 金额
value = `${firstRule.amount || ''}元`;
} else if (firstRule.fieldType === 5 || firstRule.fieldType === 6) {
// 天数
value = `${firstRule.days || ''}天`;
}
if (operator && value) {
return `${fieldType}:${operator} ${value}`;
}
}
return '';
});
const lastClickTime = ref(0);
const DOUBLE_CLICK_DELAY = 300;
const handleClick = () => {
const now = Date.now();
// 判断是否为双击
if (now - lastClickTime.value < DOUBLE_CLICK_DELAY) {
// 双击事件
emit('click', props.data?.nodeId);
lastClickTime.value = 0;
} else {
lastClickTime.value = now;
}
};
</script>
<style lang="less" scoped>
.condition-node {
position: relative;
padding: 12px 16px;
background: #fff7e8;
border: 2px solid #ffbf6b;
border-radius: 8px;
min-width: 140px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(255, 191, 107, 0.3);
}
.node-content {
display: flex;
flex-direction: column;
gap: 6px;
}
.node-header {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
}
.node-icon {
color: #ffbf6b;
}
.node-label {
font-size: 14px;
color: #1d2129;
font-weight: 500;
}
.node-desc {
font-size: 12px;
color: #86909c;
}
}
</style>
@@ -0,0 +1,46 @@
<template>
<div class="end-node">
<!-- 输入连接点(顶部) -->
<Handle type="target" :position="Position.Top" />
<div class="node-content">
<div class="node-icon">
<icon-check-circle-fill :size="24" />
</div>
<div class="node-label">结束</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core';
import { IconCheckCircleFill } from '@arco-design/web-vue/es/icon';
</script>
<style lang="less" scoped>
.end-node {
position: relative;
padding: 8px 16px;
background: #fff;
border: 2px solid #7298f8;
border-radius: 8px;
min-width: 120px;
text-align: center;
.node-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.node-icon {
color: #7298f8;
}
.node-label {
font-size: 14px;
color: #1d2129;
font-weight: 500;
}
}
</style>
@@ -0,0 +1,46 @@
<template>
<div class="start-node">
<div class="node-content">
<div class="node-icon">
<icon-play-circle-fill :size="24" />
</div>
<div class="node-label">开始</div>
</div>
<!-- 输出连接点(底部) -->
<Handle type="source" :position="Position.Bottom" />
</div>
</template>
<script lang="ts" setup>
import { Handle, Position } from '@vue-flow/core';
import { IconPlayCircleFill } from '@arco-design/web-vue/es/icon';
</script>
<style lang="less" scoped>
.start-node {
position: relative;
padding: 8px 16px;
background: #fff;
border: 2px solid #7298f8;
border-radius: 8px;
min-width: 120px;
text-align: center;
.node-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.node-icon {
color: #7298f8;
}
.node-label {
font-size: 14px;
color: #1d2129;
font-weight: 500;
}
}
</style>
File diff suppressed because it is too large Load Diff