1374 lines
34 KiB
Vue
1374 lines
34 KiB
Vue
<template>
|
||
<div class="approval-flow-container">
|
||
<div class="main-content">
|
||
<!-- 左侧基本信息 -->
|
||
<div class="left-panel">
|
||
<h3 class="panel-title">基本信息</h3>
|
||
<el-form :model="form" label-width="80px">
|
||
<el-form-item label="名称">
|
||
<el-input v-model="form.name" placeholder="请输入流程名称" />
|
||
</el-form-item>
|
||
<el-form-item label="业务类型">
|
||
<el-input v-model="form.type" placeholder="请输入业务类型" />
|
||
</el-form-item>
|
||
<el-form-item label="超时时间">
|
||
<ElInput v-model="form.timeout_hours" placeholder="请输入超时时间">
|
||
<template #suffix>
|
||
<span>h</span>
|
||
</template>
|
||
</ElInput>
|
||
</el-form-item>
|
||
<el-form-item label="超时动作">
|
||
<el-select v-model="form.timeout_action" placeholder="请选择超时动作">
|
||
<el-option label="请选择" value="" />
|
||
<el-option label="全部同意" value="all_agree" />
|
||
<el-option label="全部拒绝" value="all_reject" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="是否启用">
|
||
<el-switch v-model="form.is_active" :active-value="1" :inactive-value="0" />
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<!-- 中间审批流程 -->
|
||
<div class="middle-panel">
|
||
<h3 class="panel-title">审批流</h3>
|
||
<div class="flow-diagram">
|
||
<template v-for="(node, index) in form.nodes" :key="node.id">
|
||
<!-- 节点按钮 -->
|
||
<div class="node-container">
|
||
<el-button
|
||
type="primary"
|
||
icon="Plus"
|
||
@click="selectNode(node)"
|
||
class="node-btn"
|
||
:class="{
|
||
selected: selectedNodeId === node.id,
|
||
disabled: index === 0
|
||
}"
|
||
:disabled="index === 0"
|
||
>
|
||
</el-button>
|
||
<div class="node-info" @click="selectNode(node)">
|
||
<div class="node-title">{{ node.title }}</div>
|
||
<div class="node-subtitle">{{ node.subtitle }}</div>
|
||
</div>
|
||
<!-- 删除按钮 -->
|
||
<i
|
||
v-if="form.nodes.length >= 3 && index > 0"
|
||
class="iconfont-sys danger-color"
|
||
@click="removeNode(node.id)"
|
||
></i
|
||
>
|
||
</div>
|
||
|
||
<!-- 连接线 -->
|
||
<div v-if="index < form.nodes.length" class="connection-line">
|
||
<div class="dotted-line"></div>
|
||
<el-button
|
||
type="primary"
|
||
icon="Plus"
|
||
@click="addNodeAfter(node.id)"
|
||
class="add-connection-btn"
|
||
size="small"
|
||
circle
|
||
/>
|
||
</div>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧审批人配置 -->
|
||
<div class="right-panel">
|
||
<div class="panel-header">
|
||
<h3 class="panel-title">审批人</h3>
|
||
</div>
|
||
|
||
<!-- 提示信息 -->
|
||
<div v-if="!selectedNode || selectedNodeId === form.nodes[0]?.id" class="no-selection">
|
||
<div class="no-selection-content">
|
||
<i class="el-icon-info"></i>
|
||
<p v-if="!selectedNode">请选择一个节点进行配置</p>
|
||
<p v-else>申请人节点为默认节点,不可配置</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="selectedNode && selectedNodeId !== form.nodes[0]?.id" class="approver-config">
|
||
<!-- 节点基本信息 -->
|
||
<div class="config-section">
|
||
<h4>节点信息</h4>
|
||
<el-form :model="selectedNode" label-width="60px" size="small">
|
||
<el-form-item label="名称">
|
||
<el-input
|
||
v-model="selectedNode.title"
|
||
placeholder="请输入节点名称"
|
||
@blur="updateNodeLabel"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="类型">
|
||
<el-select
|
||
v-model="selectedNode.type"
|
||
@change="onNodeTypeChange"
|
||
placeholder="请选择节点类型"
|
||
>
|
||
<el-option
|
||
v-for="item in approvalType"
|
||
:key="item.item_value"
|
||
:label="item.item_label"
|
||
:value="item.item_value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="审批条件">
|
||
<el-input
|
||
v-model="selectedNode.condition"
|
||
placeholder="请输入审批条件)"
|
||
type="textarea"
|
||
:rows="2"
|
||
/>
|
||
</el-form-item>
|
||
<el-form-item label="是否必须">
|
||
<el-switch
|
||
v-model="selectedNode.required"
|
||
active-text="必须"
|
||
inactive-text="可选"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
|
||
<div class="config-section">
|
||
<h4>审批人</h4>
|
||
<el-checkbox-group v-model="selectedNode.approverUsers">
|
||
<!-- <el-checkbox label="上级领导">上级领导</el-checkbox> -->
|
||
</el-checkbox-group>
|
||
<div class="approver-groups">
|
||
<div
|
||
v-for="(group, groupIndex) in selectedNode.approverGroups"
|
||
:key="groupIndex"
|
||
class="approver-group"
|
||
>
|
||
<div class="group-header">
|
||
<span class="group-title">审批组 {{ groupIndex + 1 }}</span>
|
||
<el-button
|
||
v-if="selectedNode.approverGroups.length > 1"
|
||
type="danger"
|
||
size="small"
|
||
icon="Delete"
|
||
@click="removeApproverGroup(groupIndex)"
|
||
circle
|
||
/>
|
||
</div>
|
||
<div class="group-content">
|
||
<el-form-item label="类型" size="small">
|
||
<el-select
|
||
v-model="group.type"
|
||
placeholder="请选择审批类型"
|
||
@change="onGroupTypeChange(group)"
|
||
>
|
||
<el-option
|
||
v-for="item in approvalRole"
|
||
:key="item.item_value"
|
||
:label="item.item_label"
|
||
:value="item.item_value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="选项" size="small">
|
||
<el-select
|
||
v-model="group.users"
|
||
multiple
|
||
placeholder="请选择"
|
||
:loading="group.loading"
|
||
ollapse-tags
|
||
filterable
|
||
:disabled="group.type === 'initiator_leader'"
|
||
>
|
||
<el-option
|
||
v-for="opt in group.options"
|
||
:key="opt.id"
|
||
:label="opt.name"
|
||
:value="opt.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
icon="Plus"
|
||
@click="addApproverGroup"
|
||
class="add-group-btn"
|
||
>
|
||
添加审批组
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="config-section">
|
||
<div class="cc-header">
|
||
<h4>是否抄送</h4>
|
||
<el-switch v-model="selectedNode.isCC" />
|
||
</div>
|
||
<div v-if="selectedNode.isCC" class="cc-config">
|
||
<h4>抄送人</h4>
|
||
<el-checkbox-group v-model="selectedNode.ccUsers">
|
||
<!-- <el-checkbox label="上级领导">上级领导</el-checkbox> -->
|
||
</el-checkbox-group>
|
||
<div class="cc-groups">
|
||
<div
|
||
v-for="(group, groupIndex) in selectedNode.ccGroups"
|
||
:key="groupIndex"
|
||
class="cc-group"
|
||
>
|
||
<div class="group-header">
|
||
<span class="group-title">抄送组 {{ groupIndex + 1 }}</span>
|
||
<el-button
|
||
v-if="selectedNode.ccGroups.length > 1"
|
||
type="danger"
|
||
size="small"
|
||
icon="Delete"
|
||
@click="removeCCGroup(groupIndex)"
|
||
>
|
||
</el-button>
|
||
</div>
|
||
<div class="group-content">
|
||
<el-form-item label="类型" size="small">
|
||
<el-select
|
||
v-model="group.type"
|
||
placeholder="请选择类型"
|
||
style="width: 100%"
|
||
@change="onGroupTypeChange(group)"
|
||
>
|
||
<el-option
|
||
v-for="item in approvalRole"
|
||
:key="item.item_value"
|
||
:label="item.item_label"
|
||
:value="item.item_value"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="选项" size="small">
|
||
<el-select
|
||
v-model="group.users"
|
||
multiple
|
||
placeholder="请选择"
|
||
:loading="group.loading"
|
||
:disabled="group.type === 'initiator_leader'"
|
||
>
|
||
<el-option
|
||
v-for="opt in group.options"
|
||
:key="opt.id"
|
||
:label="opt.name"
|
||
:value="opt.id"
|
||
/>
|
||
</el-select>
|
||
</el-form-item>
|
||
</div>
|
||
</div>
|
||
<el-button
|
||
type="primary"
|
||
size="small"
|
||
icon="Plus"
|
||
@click="addCCGroup"
|
||
class="add-group-btn"
|
||
>
|
||
添加抄送组
|
||
</el-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- <div v-else class="no-selection">
|
||
<p>请选择一个节点进行配置</p>
|
||
</div> -->
|
||
</div>
|
||
</div>
|
||
<!-- 底部操作按钮 -->
|
||
<div class="bottom-actions">
|
||
<el-button @click="cancelFlow">取消</el-button>
|
||
<el-button type="primary" @click="saveFlow">保存</el-button>
|
||
</div>
|
||
<!-- 节点编辑弹框 -->
|
||
<el-dialog
|
||
v-model="editDialogVisible"
|
||
:title="editingNode.id === null ? '添加新节点' : '编辑节点名称'"
|
||
width="400px"
|
||
:close-on-click-modal="false"
|
||
>
|
||
<el-form :model="editingNode" label-width="80px">
|
||
<el-form-item label="节点名称">
|
||
<el-input v-model="editingNode.label" placeholder="请输入节点名称" />
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<el-button @click="cancelEdit">取消</el-button>
|
||
<el-button type="primary" @click="saveNode">确定</el-button>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { reactive, ref, onMounted } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage } from 'element-plus'
|
||
import { commonService } from '@/api/commonApi'
|
||
import { manageService } from '@/api/adminApi'
|
||
|
||
//审批类型 AWF_STEP_TYPE
|
||
const approvalType = ref([])
|
||
//角色类型
|
||
const approvalRole = ref([])
|
||
const loadDictOptions = async () => {
|
||
const response = await commonService.getDictSelect({
|
||
scope: 'awf_step_type,awf_approvers_type',
|
||
scope_type: '1'
|
||
})
|
||
approvalType.value = response.awf_step_type
|
||
approvalRole.value = response.awf_approvers_type
|
||
}
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
|
||
onMounted(async () => {
|
||
await loadDictOptions()
|
||
await loadDetail()
|
||
})
|
||
|
||
// 审批流程数据
|
||
const form = reactive({
|
||
id: '',
|
||
name: '',
|
||
type: '',
|
||
timeout_hours: '',
|
||
timeout_action: '',
|
||
is_active: 1,
|
||
nodes: [
|
||
{
|
||
id: 1,
|
||
label: '申请人',
|
||
title: '申请人',
|
||
subtitle: '本人',
|
||
type: '',
|
||
condition: '',
|
||
required: true,
|
||
approverGroups: [
|
||
{
|
||
type: '',
|
||
users: [],
|
||
loading: false,
|
||
options: []
|
||
}
|
||
],
|
||
approvers: [],
|
||
businessApprovers: [],
|
||
approverUsers: [],
|
||
ccGroups: [
|
||
{
|
||
type: '',
|
||
users: [],
|
||
loading: false,
|
||
options: []
|
||
}
|
||
],
|
||
ccUsers: [],
|
||
isCC: false
|
||
},
|
||
{
|
||
id: 2,
|
||
label: '审批人',
|
||
title: '审批人',
|
||
subtitle: '上级',
|
||
type: '',
|
||
condition: '',
|
||
required: true,
|
||
approverGroups: [
|
||
{
|
||
type: '',
|
||
users: [],
|
||
loading: false,
|
||
options: []
|
||
}
|
||
],
|
||
approvers: [],
|
||
businessApprovers: [],
|
||
approverUsers: [],
|
||
ccGroups: [
|
||
{
|
||
type: '',
|
||
users: [],
|
||
loading: false,
|
||
options: []
|
||
}
|
||
],
|
||
ccUsers: [],
|
||
isCC: false
|
||
}
|
||
]
|
||
})
|
||
|
||
// 编辑弹框状态
|
||
const editDialogVisible = ref(false)
|
||
const editingNode = reactive({
|
||
id: null,
|
||
label: '',
|
||
afterNodeId: null // 用于记录在哪个节点后添加新节点
|
||
})
|
||
|
||
// 选中的节点
|
||
const selectedNodeId = ref(null)
|
||
const selectedNode = ref(null)
|
||
|
||
// 业务部用户数据(如需部门静态项,可在此填充)
|
||
// const businessUsers = ref([])
|
||
|
||
// 选项缓存,避免重复请求
|
||
const optionsCache = reactive({ role: [], user: [], department: [] })
|
||
|
||
// 懒加载并缓存不同类型的选项
|
||
const ensureOptionsLoaded = async (group) => {
|
||
const type = group.type
|
||
if (!type || type === 'initiator_leader') {
|
||
group.options = []
|
||
return
|
||
}
|
||
group.loading = true
|
||
try {
|
||
if (type === 'role') {
|
||
if (!optionsCache.role || optionsCache.role.length === 0) {
|
||
const response = await commonService.getRoleSelect()
|
||
optionsCache.role = response || []
|
||
}
|
||
console.log('optionsCache.role', optionsCache.role)
|
||
group.options = optionsCache.role
|
||
} else if (type === 'user') {
|
||
if (!optionsCache.user || optionsCache.user.length === 0) {
|
||
const response = await commonService.getUserSelect()
|
||
optionsCache.user = response || []
|
||
}
|
||
group.options = optionsCache.user
|
||
} else {
|
||
group.options = []
|
||
}
|
||
} finally {
|
||
group.loading = false
|
||
}
|
||
}
|
||
|
||
// 节点ID计数器
|
||
let nodeIdCounter = 3
|
||
|
||
// 选择节点
|
||
const selectNode = (node) => {
|
||
// 第一个节点(申请人)不可选择
|
||
const nodeIndex = form.nodes.findIndex((n) => n.id === node.id)
|
||
if (nodeIndex === 0) {
|
||
return
|
||
}
|
||
selectedNodeId.value = node.id
|
||
selectedNode.value = node
|
||
}
|
||
|
||
// 更新节点标签(当名称改变时)
|
||
const updateNodeLabel = () => {
|
||
if (selectedNode.value) {
|
||
selectedNode.value.label = selectedNode.value.title
|
||
}
|
||
}
|
||
|
||
// 节点类型改变
|
||
const onNodeTypeChange = (type) => {
|
||
if (selectedNode.value) {
|
||
// 根据类型设置默认值
|
||
if (type === 'applicant') {
|
||
selectedNode.value.subtitle = '本人'
|
||
} else if (type === 'approver') {
|
||
selectedNode.value.subtitle = '请选择'
|
||
} else if (type === 'cc') {
|
||
selectedNode.value.subtitle = '请选择'
|
||
selectedNode.value.isCC = true
|
||
}
|
||
}
|
||
}
|
||
|
||
// 添加审批组
|
||
const addApproverGroup = () => {
|
||
if (selectedNode.value) {
|
||
selectedNode.value.approverGroups.push({
|
||
type: '',
|
||
users: [],
|
||
loading: false,
|
||
options: optionsCache.department
|
||
})
|
||
}
|
||
}
|
||
|
||
// 删除审批组
|
||
const removeApproverGroup = (groupIndex) => {
|
||
if (selectedNode.value && selectedNode.value.approverGroups.length > 1) {
|
||
selectedNode.value.approverGroups.splice(groupIndex, 1)
|
||
}
|
||
}
|
||
|
||
// 添加抄送组
|
||
const addCCGroup = () => {
|
||
if (selectedNode.value) {
|
||
selectedNode.value.ccGroups.push({
|
||
type: '',
|
||
users: [],
|
||
loading: false,
|
||
options: optionsCache.department
|
||
})
|
||
}
|
||
}
|
||
|
||
// 删除抄送组
|
||
const removeCCGroup = (groupIndex) => {
|
||
if (selectedNode.value && selectedNode.value.ccGroups.length > 1) {
|
||
selectedNode.value.ccGroups.splice(groupIndex, 1)
|
||
}
|
||
}
|
||
|
||
// (已由 ensureOptionsLoaded 替代)
|
||
|
||
// 类型变化时,清空选项(如发起人上级则禁用)
|
||
const onGroupTypeChange = async (group) => {
|
||
group.users = []
|
||
group.options = []
|
||
await ensureOptionsLoaded(group)
|
||
}
|
||
|
||
// 加载详情并回显
|
||
const loadDetail = async () => {
|
||
const id = route.params.id || route.query.id
|
||
let data = {}
|
||
if (!id) return
|
||
await manageService.stockapprovaldetail({ id }).then((res) => {
|
||
data = res
|
||
})
|
||
|
||
console.log('data', data)
|
||
if (!data) return
|
||
form.id = data.id
|
||
form.name = data.name || ''
|
||
form.type = data.type || ''
|
||
form.timeout_hours = data.timeout_hours ?? ''
|
||
form.timeout_action = data.timeout_action || ''
|
||
form.is_active = data.is_active ? 1 : 0
|
||
data.steps = JSON.parse(data.steps)
|
||
console.log('data', data.steps)
|
||
// 重建节点:第一个默认申请人 + 每个步骤一个节点
|
||
const nodes = [form.nodes[0]]
|
||
if (Array.isArray(data.steps)) {
|
||
for (const step of data.steps) {
|
||
const node = {
|
||
id: ++nodeIdCounter,
|
||
label: step.name || '审批节点',
|
||
title: step.name || '审批节点',
|
||
subtitle: '上级',
|
||
type: step.type || 'any',
|
||
condition: step.condition || '',
|
||
required: !!step.required,
|
||
approverGroups: [],
|
||
approverUsers: [],
|
||
ccGroups: [],
|
||
ccUsers: [],
|
||
isCC: Array.isArray(step.cc) && step.cc.length > 0
|
||
}
|
||
|
||
// 审批组
|
||
if (Array.isArray(step.approvers)) {
|
||
for (const ap of step.approvers) {
|
||
const group = { type: ap.type || '', users: [], loading: false, options: [] }
|
||
await ensureOptionsLoaded(group)
|
||
if (ap.type === 'initiator_leader') {
|
||
group.users = []
|
||
} else if (ap.value) {
|
||
group.users = String(ap.value)
|
||
.split(',')
|
||
.map((v) => Number((v || '').trim()))
|
||
.filter((n) => !Number.isNaN(n))
|
||
}
|
||
node.approverGroups.push(group)
|
||
}
|
||
} else {
|
||
// 至少保留一个空组方便编辑
|
||
const group = { type: '', users: [], loading: false, options: [] }
|
||
await ensureOptionsLoaded(group)
|
||
node.approverGroups.push(group)
|
||
}
|
||
|
||
// 抄送组
|
||
if (Array.isArray(step.cc)) {
|
||
for (const c of step.cc) {
|
||
const group = { type: c.type || '', users: [], loading: false, options: [] }
|
||
await ensureOptionsLoaded(group)
|
||
if (c.type === 'initiator_leader') {
|
||
group.users = []
|
||
} else if (c.value) {
|
||
group.users = String(c.value)
|
||
.split(',')
|
||
.map((v) => Number((v || '').trim()))
|
||
.filter((n) => !Number.isNaN(n))
|
||
}
|
||
node.ccGroups.push(group)
|
||
}
|
||
} else {
|
||
const group = { type: '', users: [], loading: false, options: [] }
|
||
await ensureOptionsLoaded(group)
|
||
node.ccGroups.push(group)
|
||
}
|
||
|
||
nodes.push(node)
|
||
}
|
||
}
|
||
|
||
form.nodes = nodes
|
||
}
|
||
|
||
// 保存节点
|
||
const saveNode = () => {
|
||
if (editingNode.id === null) {
|
||
// 添加新节点
|
||
if (editingNode.afterNodeId) {
|
||
const afterIndex = form.nodes.findIndex((n) => n.id === editingNode.afterNodeId)
|
||
if (afterIndex !== -1) {
|
||
const newNode = {
|
||
id: ++nodeIdCounter,
|
||
label: editingNode.label,
|
||
title: editingNode.label,
|
||
subtitle: '请选择',
|
||
type: '',
|
||
condition: '',
|
||
required: true,
|
||
approverGroups: [
|
||
{
|
||
type: '',
|
||
users: [],
|
||
loading: false
|
||
}
|
||
],
|
||
approvers: [],
|
||
businessApprovers: [],
|
||
ccUsers: [],
|
||
businessCCUsers: [],
|
||
isCC: false
|
||
}
|
||
form.nodes.splice(afterIndex + 1, 0, newNode)
|
||
}
|
||
}
|
||
} else {
|
||
// 编辑现有节点
|
||
const nodeIndex = form.nodes.findIndex((n) => n.id === editingNode.id)
|
||
if (nodeIndex !== -1) {
|
||
form.nodes[nodeIndex].label = editingNode.label
|
||
form.nodes[nodeIndex].title = editingNode.label
|
||
}
|
||
}
|
||
// 重置编辑状态
|
||
editingNode.id = null
|
||
editingNode.label = ''
|
||
editingNode.afterNodeId = null
|
||
editDialogVisible.value = false
|
||
}
|
||
|
||
// 在指定节点后添加新节点
|
||
const addNodeAfter = (afterNodeId) => {
|
||
// 先打开编辑弹框,让用户输入节点名称
|
||
editingNode.id = null // 设置为null表示这是新节点
|
||
editingNode.label = ''
|
||
editingNode.afterNodeId = afterNodeId
|
||
editDialogVisible.value = true
|
||
}
|
||
|
||
// 取消编辑
|
||
const cancelEdit = () => {
|
||
// 重置编辑状态
|
||
editingNode.id = null
|
||
editingNode.label = ''
|
||
editingNode.afterNodeId = null
|
||
editDialogVisible.value = false
|
||
}
|
||
|
||
// 删除节点
|
||
const removeNode = (nodeId) => {
|
||
const nodeIndex = form.nodes.findIndex((n) => n.id === nodeId)
|
||
if (nodeIndex !== -1 && nodeIndex > 0) {
|
||
form.nodes.splice(nodeIndex, 1)
|
||
}
|
||
}
|
||
|
||
// 保存流程 -> 组装为后端需要的结构
|
||
const saveFlow = async () => {
|
||
const timeout = Number(form.timeout_hours || 0)
|
||
if (timeout > 0 && !form.timeout_action) {
|
||
ElMessage.error('当超时时间大于0时,必须选择超时动作')
|
||
return
|
||
}
|
||
const payload = {
|
||
id: form.id,
|
||
name: form.name,
|
||
type: form.type,
|
||
timeout_hours: timeout,
|
||
timeout_action: form.timeout_action,
|
||
is_active: !!form.is_active,
|
||
steps: []
|
||
}
|
||
|
||
const steps = []
|
||
form.nodes.forEach((node, idx) => {
|
||
// 跳过第一个默认的申请人节点
|
||
if (idx === 0) return
|
||
|
||
const approvers = []
|
||
// 组审批
|
||
if (Array.isArray(node.approverGroups)) {
|
||
node.approverGroups.forEach((group) => {
|
||
if (group && group.type === 'initiator_leader') {
|
||
approvers.push({ type: 'initiator_leader' })
|
||
} else if (group && group.type && Array.isArray(group.users) && group.users.length > 0) {
|
||
approvers.push({ type: group.type, value: group.users.join(',') })
|
||
}
|
||
})
|
||
}
|
||
// // 单选“上级领导”
|
||
// if (Array.isArray(node.approverUsers) && node.approverUsers.includes('上级领导')) {
|
||
// approvers.push({ type: 'initiator_leader' })
|
||
// }
|
||
|
||
const cc = []
|
||
if (node.isCC && Array.isArray(node.ccGroups)) {
|
||
node.ccGroups.forEach((group) => {
|
||
if (group && group.type === 'initiator_leader') {
|
||
cc.push({ type: 'initiator_leader' })
|
||
} else if (group && group.type && Array.isArray(group.users) && group.users.length > 0) {
|
||
cc.push({ type: group.type, value: group.users.join(',') })
|
||
}
|
||
})
|
||
}
|
||
// if (node.isCC && Array.isArray(node.ccUsers) && node.ccUsers.includes('上级领导')) {
|
||
// cc.push({ type: 'initiator_leader' })
|
||
// }
|
||
|
||
steps.push({
|
||
name: node.title || node.label || '',
|
||
type: node.type || 'any',
|
||
approvers,
|
||
cc,
|
||
condition: node.condition || '',
|
||
required: !!node.required
|
||
})
|
||
})
|
||
payload.steps = steps
|
||
const { message } = await manageService.stockapprovalupdate(payload)
|
||
ElMessage.success(message)
|
||
console.log('保存的流程数据:', payload)
|
||
setTimeout(() => {
|
||
router.back()
|
||
}, 300)
|
||
}
|
||
|
||
// 取消流程
|
||
const cancelFlow = () => {
|
||
console.log('取消流程编辑')
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.approval-flow-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 88vh;
|
||
/* padding: 20px; */
|
||
/* gap: 20px; */
|
||
}
|
||
|
||
.main-content {
|
||
display: flex;
|
||
flex: 1;
|
||
gap: 20px;
|
||
}
|
||
|
||
.left-panel,
|
||
.middle-panel,
|
||
.right-panel {
|
||
background: #fff;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
height: 75vh;
|
||
}
|
||
|
||
.left-panel {
|
||
width: 300px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.middle-panel {
|
||
flex: 1;
|
||
min-width: 400px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.right-panel {
|
||
width: 350px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 20px;
|
||
padding-bottom: 10px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.panel-header .panel-title {
|
||
margin-bottom: 0;
|
||
border-bottom: none;
|
||
}
|
||
|
||
.unit {
|
||
margin-left: 8px;
|
||
color: #909399;
|
||
}
|
||
|
||
.flow-diagram {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
gap: 0;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 20px 10px;
|
||
max-height: calc(100vh - 200px);
|
||
}
|
||
|
||
.node-container {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
gap: 15px;
|
||
position: relative;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.node-btn {
|
||
width: 50px;
|
||
height: 50px !important;
|
||
border-radius: 8px;
|
||
font-size: 16px;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.3);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.node-btn:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.4);
|
||
}
|
||
|
||
.node-btn.selected {
|
||
background-color: #67c23a;
|
||
border-color: #67c23a;
|
||
}
|
||
|
||
.node-btn.selected:hover {
|
||
background-color: #85ce61;
|
||
border-color: #85ce61;
|
||
}
|
||
|
||
.node-btn.disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
background-color: #6ca3f5;
|
||
border-color: #6ca3f5;
|
||
color: #fff;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.node-btn.disabled:hover {
|
||
background-color: #f5f7fa;
|
||
border-color: #dcdfe6;
|
||
color: #909399;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
.cc-node {
|
||
background-color: #67c23a;
|
||
border-color: #67c23a;
|
||
}
|
||
|
||
.cc-node:hover {
|
||
background-color: #85ce61;
|
||
border-color: #85ce61;
|
||
}
|
||
|
||
.node-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
/* min-width: 120px; */
|
||
text-align: left;
|
||
}
|
||
|
||
.node-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.node-subtitle {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.danger-color {
|
||
position: absolute;
|
||
right: -30px;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
color: #f56c6c;
|
||
}
|
||
|
||
.connection-line {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
margin: 5px 0;
|
||
}
|
||
|
||
.dotted-line {
|
||
width: 2px;
|
||
height: 80px;
|
||
background: repeating-linear-gradient(
|
||
to bottom,
|
||
#409eff 0px,
|
||
#409eff 4px,
|
||
transparent 4px,
|
||
transparent 8px
|
||
);
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.add-connection-btn {
|
||
width: 24px;
|
||
height: 24px;
|
||
padding: 0;
|
||
background-color: #409eff;
|
||
border-color: #409eff;
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
z-index: 1;
|
||
}
|
||
|
||
.add-connection-btn:hover {
|
||
background-color: #66b1ff;
|
||
border-color: #66b1ff;
|
||
}
|
||
|
||
.approver-config {
|
||
margin-top: 20px;
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding-right: 10px;
|
||
}
|
||
|
||
/* 滚动条样式 */
|
||
.flow-diagram::-webkit-scrollbar,
|
||
.approver-config::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
.flow-diagram::-webkit-scrollbar-track,
|
||
.approver-config::-webkit-scrollbar-track {
|
||
background: #f1f1f1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.flow-diagram::-webkit-scrollbar-thumb,
|
||
.approver-config::-webkit-scrollbar-thumb {
|
||
background: #c1c1c1;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.flow-diagram::-webkit-scrollbar-thumb:hover,
|
||
.approver-config::-webkit-scrollbar-thumb:hover {
|
||
background: #a8a8a8;
|
||
}
|
||
|
||
/* 确保滚动区域有足够的最小高度 */
|
||
.flow-diagram {
|
||
min-height: 300px;
|
||
}
|
||
|
||
.config-section {
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.config-section h4 {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.config-section:first-child {
|
||
margin-bottom: 20px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.no-selection {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
height: 200px;
|
||
}
|
||
|
||
.no-selection-content {
|
||
text-align: center;
|
||
color: #909399;
|
||
}
|
||
|
||
.no-selection-content i {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
display: block;
|
||
}
|
||
|
||
.no-selection-content p {
|
||
margin: 0;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.approver-list {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.config-section .el-checkbox-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.approver-groups {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.approver-group {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 6px;
|
||
margin-bottom: 16px;
|
||
padding: 16px;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.group-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.group-title {
|
||
font-weight: 600;
|
||
color: #303133;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.group-content {
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: flex-end;
|
||
}
|
||
|
||
.group-content .el-form-item {
|
||
flex: 1;
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.add-group-btn {
|
||
width: 100%;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.cc-config .el-checkbox-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.cc-groups {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.cc-group {
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 6px;
|
||
margin-bottom: 16px;
|
||
padding: 16px;
|
||
background: #fafafa;
|
||
}
|
||
|
||
.department-group {
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.cc-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.cc-header h4 {
|
||
margin: 0;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.cc-config {
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.no-selection {
|
||
text-align: center;
|
||
color: #909399;
|
||
margin-top: 50px;
|
||
}
|
||
|
||
.no-selection p {
|
||
margin: 0;
|
||
}
|
||
|
||
.bottom-actions {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
padding: 20px 0;
|
||
}
|
||
|
||
/* 弹框样式优化 */
|
||
:deep(.el-dialog__body) {
|
||
padding: 20px;
|
||
}
|
||
|
||
:deep(.el-form-item) {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
:deep(.el-form-item__label) {
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
:deep(.el-input__inner) {
|
||
border-radius: 6px;
|
||
}
|
||
|
||
:deep(.el-select) {
|
||
width: 100%;
|
||
}
|
||
|
||
:deep(.el-select .el-input__inner) {
|
||
border-radius: 6px;
|
||
}
|
||
|
||
/* 移动端适配 */
|
||
@media (max-width: 768px) {
|
||
.approval-flow-container {
|
||
height: auto;
|
||
min-height: 100vh;
|
||
padding: 10px;
|
||
}
|
||
|
||
.main-content {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.left-panel,
|
||
.middle-panel,
|
||
.right-panel {
|
||
width: 100%;
|
||
height: auto;
|
||
min-height: auto;
|
||
padding: 15px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.left-panel {
|
||
order: 1;
|
||
}
|
||
|
||
.middle-panel {
|
||
order: 2;
|
||
min-width: auto;
|
||
}
|
||
|
||
.right-panel {
|
||
order: 3;
|
||
}
|
||
|
||
.flow-diagram {
|
||
max-height: 400px;
|
||
padding: 15px 5px;
|
||
}
|
||
|
||
.node-container {
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin: 15px 0;
|
||
}
|
||
|
||
.node-btn {
|
||
width: 60px;
|
||
height: 60px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.node-info {
|
||
min-width: auto;
|
||
text-align: center;
|
||
}
|
||
|
||
.node-title {
|
||
font-size: 14px;
|
||
}
|
||
|
||
.node-subtitle {
|
||
font-size: 12px;
|
||
}
|
||
|
||
.connection-line {
|
||
margin: 0px 0;
|
||
}
|
||
|
||
.dotted-line {
|
||
height: 75px;
|
||
}
|
||
|
||
.add-node-btn {
|
||
width: 40px;
|
||
height: 40px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.approver-config {
|
||
max-height: 300px;
|
||
}
|
||
|
||
.approver-group,
|
||
.cc-group {
|
||
padding: 12px;
|
||
}
|
||
|
||
.group-content {
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.group-content .el-form-item {
|
||
flex: none;
|
||
}
|
||
|
||
.bottom-actions {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
padding: 15px 0;
|
||
}
|
||
|
||
.bottom-actions .el-button {
|
||
width: 100%;
|
||
}
|
||
|
||
.cc-header {
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
gap: 10px;
|
||
}
|
||
|
||
.cc-header h4 {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.panel-title {
|
||
font-size: 14px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-row {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.form-row .el-form-item {
|
||
flex: none;
|
||
}
|
||
}
|
||
|
||
/* 超小屏幕适配 */
|
||
@media (max-width: 480px) {
|
||
.approval-flow-container {
|
||
padding: 5px;
|
||
}
|
||
|
||
.left-panel,
|
||
.middle-panel,
|
||
.right-panel {
|
||
padding: 10px;
|
||
}
|
||
|
||
.node-btn {
|
||
width: 50px;
|
||
height: 50px;
|
||
font-size: 16px;
|
||
}
|
||
|
||
.node-title {
|
||
font-size: 13px;
|
||
}
|
||
|
||
.node-subtitle {
|
||
font-size: 11px;
|
||
}
|
||
|
||
.flow-diagram {
|
||
padding: 10px 5px;
|
||
}
|
||
|
||
.approver-config {
|
||
max-height: 250px;
|
||
}
|
||
|
||
.approver-group,
|
||
.cc-group {
|
||
padding: 10px;
|
||
}
|
||
}
|
||
</style>
|