init
This commit is contained in:
154
src/views/workflow/about/index.vue
Normal file
154
src/views/workflow/about/index.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="queryParams" inline class="query-form" ref="queryForm" @submit.prevent="getList">
|
||||
<el-form-item label="部署名称" prop="deploymentName">
|
||||
<el-input v-model="queryParams.deploymentName" placeholder="请输入部署名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="状态" prop="enable">-->
|
||||
<!-- <el-select v-model="queryParams.enable" placeholder="请选择状态" clearable filterable >-->
|
||||
<!-- <el-option-->
|
||||
<!-- v-for="dict in cacheStore.getDict('regular_enable')"-->
|
||||
<!-- :key="dict.value"-->
|
||||
<!-- :label="dict.label"-->
|
||||
<!-- :value="dict.value"-->
|
||||
<!-- />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getList" :icon="Search">搜索</el-button>
|
||||
<el-button type="primary" @click="handleReset" :icon="Refresh" plain>重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="table">
|
||||
<el-table
|
||||
:data="list"
|
||||
row-key="id"
|
||||
:lazy="true"
|
||||
ref="singleTable"
|
||||
@cell-click="showDetails"
|
||||
v-loading="loading"
|
||||
v-tabh
|
||||
>
|
||||
<el-table-column type="selection" width="30"/>
|
||||
<el-table-column label="序号" type="index" width="60" align="center"/>
|
||||
<el-table-column prop="deploymentName" label="审批类型" align="center"/>
|
||||
<el-table-column prop="initiator" label="发起人" align="center"/>
|
||||
<el-table-column prop="submitTime" label="提交时间" align="center"/>
|
||||
<el-table-column prop="taskName" label="当前节点" align="center"/>
|
||||
<el-table-column prop="state" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<point-tag dict-type="process_state" :value="scope.row.state"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="处理耗时" align="center">
|
||||
<template #default="scope">
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.state === '4'">
|
||||
<el-button size="min" type="primary" @click="resubmitHander(scope.row.taskId)" link>再次提交</el-button>
|
||||
</template>
|
||||
<template v-if="scope.row.state === '2' || scope.row.state === '3'">
|
||||
<el-button size="min" type="primary" @click="resubmitHander(scope.row.taskId)" link>重新提交
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<paging :current-page="pageInfo.pageNum" :page-size="pageInfo.pageSize" :page-sizes="[10, 20, 30, 40,50]"
|
||||
:total="total" @changeSize="handleSizeChange" @goPage="handleCurrentChange"/>
|
||||
<instance-details ref="instanceDetails" :instance="selectProcessInstance"/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {getAboutInstanceList} from "@/api/workflow/process-instance.js";
|
||||
import {Search, Refresh, Delete, Edit} from '@element-plus/icons-vue'
|
||||
import InstanceDetails from '@/views/workflow/common/InstanceDetails.vue'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import {useCacheStore} from '@/stores/cache.js'
|
||||
import Paging from "@/components/pagination/index.vue";
|
||||
import PointTag from "@/components/PointTag.vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const dictStore = useCacheStore()
|
||||
dictStore.setCacheKey(['process_state'])
|
||||
const processStore = useProcessStore()
|
||||
//查询参数
|
||||
const queryParams = reactive({
|
||||
deploymentName: '',
|
||||
// state: 1
|
||||
})
|
||||
|
||||
//页面信息
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const list = ref([])
|
||||
const selectProcessInstance = ref()
|
||||
const total = ref()
|
||||
const loading = ref()
|
||||
const instanceDetails = ref()
|
||||
const queryForm = ref()
|
||||
|
||||
const showDetails = (row, column) => {
|
||||
if (column.label !== '操作') {
|
||||
selectProcessInstance.value = row
|
||||
nextTick(() => {
|
||||
instanceDetails.value.init()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//重置搜索
|
||||
const handleReset = () => {
|
||||
queryForm.value.resetFields()
|
||||
getList()
|
||||
}
|
||||
const getList = async () => {
|
||||
let params = {
|
||||
...queryParams,
|
||||
...pageInfo
|
||||
}
|
||||
loading.value = true
|
||||
getAboutInstanceList(params).then(res => {
|
||||
if (res.code === 1000) {
|
||||
list.value = res.data.rows
|
||||
total.value = res.data.total
|
||||
loading.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
//切换每页显示条数
|
||||
const handleSizeChange = async (val) => {
|
||||
pageInfo.value.pageSize = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
//点击页码进行分页功能
|
||||
const handleCurrentChange = async (val) => {
|
||||
pageInfo.value.pageNum = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
const getTimeConsuming = async (instance) => {
|
||||
if (instance.state != 1) {
|
||||
//dateFormat(开始时间,结束时间)
|
||||
// return timeLength(instance.submitTime, instance.endTime);
|
||||
}
|
||||
}
|
||||
|
||||
getList()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
114
src/views/workflow/approve/InitiateProcess.vue
Normal file
114
src/views/workflow/approve/InitiateProcess.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div v-loading="loading" class="initiate_process">
|
||||
<div v-if="!loading" style="min-width:30%">
|
||||
<!--渲染表单-->
|
||||
<form-render class="process-form" ref="initiateForm" :form-items="processDefinition.formItems"
|
||||
v-model:value="formData" mode="E"/>
|
||||
</div>
|
||||
<div v-if="!loading" id="approveTree"
|
||||
style="display: flex;justify-content: center;flex-direction: column;min-width:60%">
|
||||
<span style="font-size: 18px;text-align: center;padding-top: 20px;">审批流程</span>
|
||||
<process-tree ref="processTree" mode="view" id-name="approveTree"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {getInitiateInfo} from "@/api/workflow/process-definition.js";
|
||||
import ProcessTree from '@/views/workflow/process/ProcessTree.vue'
|
||||
import FormRender from '@/views/workflow/form/FormRender.vue'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
const processStore = useProcessStore()
|
||||
import {defineProps,defineExpose} from 'vue'
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
code: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const scale = ref({})
|
||||
const initiateForm = ref()
|
||||
const processTree = ref()
|
||||
const formData = ref({})
|
||||
const loading = ref({})
|
||||
const processDefinition = ref({
|
||||
processDefinitionKey: "",
|
||||
deploymentName: "",
|
||||
logo: {},
|
||||
formItems: [],
|
||||
process: {},
|
||||
remark: ""
|
||||
})
|
||||
|
||||
const loadProcessDefinitionInfo = (processDefinitionKey) => {
|
||||
loading.value = true
|
||||
getInitiateInfo(processDefinitionKey).then(res => {
|
||||
processDefinition.value = res.data;
|
||||
//构建表单及校验规则
|
||||
processStore.setDesign(res.data)
|
||||
loading.value = false;
|
||||
nextTick(() => {
|
||||
processTree.value.init()
|
||||
})
|
||||
}).catch(err => {
|
||||
ElMessage.error(err);
|
||||
});
|
||||
}
|
||||
const validate = () => {
|
||||
let formValidate
|
||||
initiateForm.value().validate(formCall => {
|
||||
formValidate = formCall;
|
||||
})
|
||||
let proValidate = processTree.value().validate()
|
||||
if (!formValidate) {
|
||||
return false;
|
||||
}
|
||||
return (Array.isArray(proValidate) && proValidate.length === 0);
|
||||
}
|
||||
|
||||
|
||||
loadProcessDefinitionInfo(props.code)
|
||||
|
||||
defineExpose({
|
||||
formData,
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@media screen and (max-width: 1545px) {
|
||||
.initiate_process {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
.initiate_process {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.process-form {
|
||||
:deep .el-form-item__label {
|
||||
padding: 0 0;
|
||||
}
|
||||
|
||||
:deep .el-divider--horizontal {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:deep .el-divider__text {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
:deep .node-footer {
|
||||
z-index: 0 !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
169
src/views/workflow/approve/index.vue
Normal file
169
src/views/workflow/approve/index.vue
Normal file
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="queryParams" inline class="query-form" ref="queryForm" @submit.prevent="getList">
|
||||
<el-form-item label="部署名称" prop="deploymentName">
|
||||
<el-input v-model="queryParams.deploymentName" placeholder="请输入部署名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="状态" prop="enable">-->
|
||||
<!-- <el-select v-model="queryParams.enable" placeholder="请选择状态" clearable filterable>-->
|
||||
<!-- <el-option-->
|
||||
<!-- v-for="dict in cacheStore.getDict('regular_enable')"-->
|
||||
<!-- :key="dict.value"-->
|
||||
<!-- :label="dict.label"-->
|
||||
<!-- :value="dict.value"-->
|
||||
<!-- />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getList" :icon="Search">搜索</el-button>
|
||||
<el-button type="primary" @click="handleReset" :icon="Refresh" plain>重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="table">
|
||||
<el-table
|
||||
:data="list"
|
||||
row-key="id"
|
||||
:lazy="true"
|
||||
ref="singleTable"
|
||||
v-loading="loading"
|
||||
v-tabh
|
||||
>
|
||||
<el-table-column type="selection" width="30"/>
|
||||
<el-table-column label="序号" type="index" width="60" align="center"/>
|
||||
<el-table-column prop="deploymentName" label="部署名称" align="center"/>
|
||||
<el-table-column prop="version" label="版本" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag type="success">{{ scope.row.version }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注说明" align="center"/>
|
||||
<el-table-column prop="updateTime" label="更新时间" align="center"/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="mini" v-perm="['rapid:regular:edit']"
|
||||
@click="handleApprove(scope.row)" link>发起流程
|
||||
</el-button>
|
||||
<popover-delete :name="scope.row.deploymentName" :type="'流程'" :perm="['rapid:regular:del']"
|
||||
@delete="handleDelete(scope.row.id)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<paging :current-page="pageInfo.pageNum" :page-size="pageInfo.pageSize" :page-sizes="[10, 20, 30, 40,50]"
|
||||
:total="total" @changeSize="handleSizeChange" @goPage="handleCurrentChange"/>
|
||||
<el-dialog title="发起审批" v-model="openItemDl" :close-on-click-modal="true" width="1200px">
|
||||
<initiate-process ref="processInstance" :code="selectItem.processDefinitionKey"
|
||||
v-if="openItemDl"></initiate-process>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button size="mini" @click="openItemDl = false">取 消</el-button>
|
||||
<el-button size="mini" type="primary" @click="submitForm">提 交</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {getProcessDefinitionList} from "@/api/workflow/process-definition.js";
|
||||
import {startProcessInstance} from "@/api/workflow/process-instance.js";
|
||||
import InitiateProcess from './InitiateProcess.vue'
|
||||
import {Search, Refresh, Delete, Plus, Edit, Download} from '@element-plus/icons-vue'
|
||||
import {ElMessage} from "element-plus";
|
||||
import Paging from "@/components/pagination/index.vue";
|
||||
|
||||
//查询参数
|
||||
const queryParams = reactive({
|
||||
deploymentName: '',
|
||||
// startTime: null,
|
||||
// endTime: null,
|
||||
// state: 1,
|
||||
})
|
||||
|
||||
//页面信息
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const openItemDl = ref(false)
|
||||
const selectItem = ref({})
|
||||
const loading = ref(true)
|
||||
const list = ref([])
|
||||
const queryForm = ref()
|
||||
const total = ref()
|
||||
const singleTable = ref()
|
||||
const processInstance = ref()
|
||||
|
||||
|
||||
|
||||
//重置搜索
|
||||
const handleReset = () => {
|
||||
queryForm.value.resetFields()
|
||||
getList()
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
let formData = processInstance.value.formData
|
||||
let paramsData = {
|
||||
processDefinitionId: selectItem.value.processDefinitionId,
|
||||
formData: JSON.stringify(formData),
|
||||
optionalUser: {}
|
||||
}
|
||||
// let valid = processInstance.value.validate(() => {
|
||||
// })
|
||||
console.log(paramsData)
|
||||
if (true) {
|
||||
startProcessInstance(paramsData).then(res => {
|
||||
if (res.code === 1000) {
|
||||
openItemDl.value = false
|
||||
ElMessage.success(res.msg)
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ElMessage.error("请完善表单😥")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//获取数据
|
||||
const handleApprove = async (node) => {
|
||||
selectItem.value = node
|
||||
openItemDl.value = true
|
||||
}
|
||||
const getList = async () => {
|
||||
let params = {
|
||||
...queryParams,
|
||||
...pageInfo
|
||||
}
|
||||
loading.value = true
|
||||
getProcessDefinitionList(params).then(res => {
|
||||
if (res.code === 1000) {
|
||||
list.value = res.data.rows
|
||||
total.value = res.data.total
|
||||
loading.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
//切换每页显示条数
|
||||
const handleSizeChange = async (val) => {
|
||||
pageInfo.value.pageSize = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
//点击页码进行分页功能
|
||||
const handleCurrentChange = async (val) => {
|
||||
pageInfo.value.pageNum = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
getList()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
380
src/views/workflow/common/Comment.vue
Normal file
380
src/views/workflow/common/Comment.vue
Normal file
@@ -0,0 +1,380 @@
|
||||
<template>
|
||||
<el-dialog custom-class="custom-dialog" class="border" width="500px" :title="title"
|
||||
v-model="visible">
|
||||
<!--显示转签弹出框-->
|
||||
<div v-if="type === 3" style="margin-bottom: 10px;">
|
||||
<span>转交给谁:</span>
|
||||
<el-button size="mini" icon="User" @click="chooseUser">选择人员</el-button>
|
||||
<div class="userStyle" v-for="(user, i) in userList" :key="i">
|
||||
<span @click="delDept(i)">×</span>
|
||||
<el-avatar :src="user.avatar"/>
|
||||
<el-tooltip class="item" effect="dark" :content="user.name" placement="bottom-start">
|
||||
<span>{{ user.name }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<!--显示退回节点弹出框-->
|
||||
<div v-if="type === 4" style="margin-bottom: 10px">
|
||||
<span>回退节点:</span>
|
||||
<el-select v-if="userTaskOption.length >0 " v-model="rollBackId" slot="prepend" placeholder="选择要回退到的节点" filterable>
|
||||
<el-option v-for="(option,index) in userTaskOption" :key="index"
|
||||
:label="option.label" :value="option.value"/>
|
||||
</el-select>
|
||||
<span v-if="userTaskOption.length <=0 " style="margin-left: 10px;color: rgb(140, 140, 140);">暂无可回退节点</span>
|
||||
</div>
|
||||
<!--显示增加审批人弹出框-->
|
||||
<div v-if="type === 5" style="margin-bottom: 10px;">
|
||||
<span>给谁加签:</span>
|
||||
<el-button size="mini" icon="User" @click="chooseUser">选择人员</el-button>
|
||||
<div class="userStyle" v-for="(user, i) in userList" :key="i">
|
||||
<span @click="delDept(i)">×</span>
|
||||
<el-avatar :src="user.avatar"/>
|
||||
<el-tooltip class="item" effect="dark" :content="user.name" placement="bottom-start">
|
||||
<span>{{ user.name }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="el-textarea">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
maxlength="255"
|
||||
show-word-limit
|
||||
placeholder="评论内容"
|
||||
v-model="context">
|
||||
</el-input>
|
||||
</div>
|
||||
<div style="margin: 20px 0;" v-if="imageList && imageList.length > 0">
|
||||
<el-upload :file-list="imageList"
|
||||
:limit="imageSize" with-credentials
|
||||
:multiple="imageSize > 0"
|
||||
:data="uploadParams"
|
||||
list-type="picture-card"
|
||||
:action="uploadFileUrl"
|
||||
:headers="headers"
|
||||
:auto-upload="true"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="beforeUpload">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<!-- <i slot="default" class="el-icon-plus"></i>-->
|
||||
<div slot="file" slot-scope="{file}">
|
||||
<img style="width: 100%;height: 100%" :src="file.url" alt="">
|
||||
<label class="el-upload-list__item-status-label">
|
||||
<i class="el-icon-upload-success el-icon-check"></i>
|
||||
</label>
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
<i class="el-icon-zoom-in"></i>
|
||||
</span>
|
||||
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleDownload(file)">
|
||||
<i class="el-icon-download"></i>
|
||||
</span>
|
||||
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
|
||||
<i class="el-icon-delete"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div slot="tip">添加图片 {{ sizeTip }}</div>
|
||||
</el-upload>
|
||||
<el-dialog :visible.sync="dialogVisible" center append-to-body>
|
||||
<div>
|
||||
<img width="100%" :src="dialogImageUrl" alt="" style="z-index: 3435">
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
<el-upload style="margin-top: 10px" :file-list="attachmentList"
|
||||
:limit="attachmentMaxSize" with-credentials
|
||||
:multiple="attachmentMaxSize > 0"
|
||||
:action="uploadFileUrl"
|
||||
:headers="headers"
|
||||
:auto-upload="true"
|
||||
:on-success="handleAttachmentUploadSuccess"
|
||||
:before-upload="beforeAttachmentUpload"
|
||||
:on-remove="handleRemoveAttachment"
|
||||
>
|
||||
<el-button size="small" round >
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span>选择文件</span>
|
||||
</el-button>
|
||||
<div slot="tip" style="margin-left: 5px"> 添加附件 {{ attachmentTip }}</div>
|
||||
</el-upload>
|
||||
|
||||
<div slot="footer">
|
||||
<el-button size="mini" @click="visible = false; emit('cancel')">取 消</el-button>
|
||||
<el-button size="mini" type="primary" @click="selectConfirm">确 定</el-button>
|
||||
</div>
|
||||
<user-picker :multiple="false" ref="userPicker" title="请选择人员" v-model:value="userList" @ok="selected"/>
|
||||
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {deleteFile} from '@/api/workflow/process-file.js'
|
||||
const baseURL = import.meta.env.VITE_BASE_URL
|
||||
import {computed, defineProps,defineEmits} from "vue";
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import {ElMessage} from "element-plus";
|
||||
import UserPicker from "../process/common/UserPicker.vue";
|
||||
const processStore = useProcessStore()
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: "添加评论"
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "评论内容"
|
||||
},
|
||||
imageSize: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
type: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
attachmentMaxSize: {
|
||||
type: Number,
|
||||
default: 100
|
||||
}
|
||||
});
|
||||
|
||||
const sizeTip = computed(() => {
|
||||
return props.imageSize > 0 ? `| 每张图不超过${props.imageSize}MB` : "";
|
||||
});
|
||||
|
||||
const attachmentTip = computed(() => {
|
||||
return props.attachmentMaxSize > 0 ? `| 单个附件不超过${props.attachmentMaxSize}MB` : "";
|
||||
});
|
||||
|
||||
const userTaskOption = computed(() => {
|
||||
let optionList = processStore.userTaskOption.value;
|
||||
if (optionList) {
|
||||
return optionList;
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const userList = ref([])
|
||||
const userPicker = ref()
|
||||
const visible = ref(false);
|
||||
const disabled = ref(false);
|
||||
const imageList = ref([]);
|
||||
const attachmentList = ref([]);
|
||||
const uploadParams = ref({});
|
||||
const uploadFileUrl = ref(baseURL + "/workflow/process/file");
|
||||
const headers = ref({
|
||||
authorization: getToken()
|
||||
});
|
||||
const context = ref(null);
|
||||
const rollBackId = ref(null);
|
||||
const dialogImageUrl = ref("");
|
||||
const dialogVisible = ref(false);
|
||||
const chooseUser=()=>{
|
||||
userPicker.value.showUserPicker()
|
||||
}
|
||||
const show = () => {
|
||||
visible.value = true;
|
||||
};
|
||||
|
||||
const handleChange = (file, imageList) => {
|
||||
imageList.value = imageList.slice(-3);
|
||||
};
|
||||
|
||||
const handleUploadSuccess = (res, file) => {
|
||||
if (res.code !== 1000) {
|
||||
ElMessage.error("上传失败");
|
||||
}
|
||||
let data = res.data;
|
||||
imageList.value.push(data);
|
||||
};
|
||||
|
||||
const handleAttachmentUploadSuccess = (res, file) => {
|
||||
if (res.code !== 1000) {
|
||||
ElMessage.error("上传失败");
|
||||
}
|
||||
let data = res.data;
|
||||
attachmentList.value.push(data);
|
||||
};
|
||||
|
||||
const beforeAttachmentUpload = (file) => {
|
||||
if (props.attachmentMaxSize > 0 && file.size / 1024 / 1024 > props.attachmentMaxSize) {
|
||||
ElMessage.warning(`单张图片最大不超过 ${props.attachmentMaxSize}MB`);
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
const alows = ["image/jpeg", "image/png", "image/gif", "image/jpg"];
|
||||
if (alows.indexOf(file.type) === -1) {
|
||||
ElMessage.warning("存在不支持的图片格式");
|
||||
} else if (props.imageSize > 0 && file.size / 1024 / 1024 > props.imageSize) {
|
||||
ElMessage.warning(`单张图片最大不超过 ${props.imageSize}MB`);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handlePictureCardPreview = (file) => {
|
||||
dialogVisible.value = true;
|
||||
dialogImageUrl.value = file.url;
|
||||
};
|
||||
|
||||
const handleDownload = (file) => {
|
||||
console.log(file);
|
||||
};
|
||||
const handleRemoveAttachment=(file)=>{
|
||||
handleRemove(file.response.data)
|
||||
}
|
||||
const handleRemove = (file) => {
|
||||
deleteFile(file.id).then(res => {
|
||||
if (res.code === 1000) {
|
||||
ElMessage.success("删除成功");
|
||||
this.fileList.splice(this.fileList.findIndex((item) => item.id === file.id), 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const selectConfirm = () => {
|
||||
let fileList = [];
|
||||
attachmentList.value.forEach((attachment) => {
|
||||
fileList.push({
|
||||
id: attachment.id,
|
||||
name: attachment.name,
|
||||
isImage: attachment.isImage,
|
||||
size: attachment.size,
|
||||
url: attachment.url
|
||||
});
|
||||
});
|
||||
imageList.value.forEach((image) => {
|
||||
fileList.push({
|
||||
id: image.id,
|
||||
name: image.name,
|
||||
isImage: image.isImage,
|
||||
size: image.size,
|
||||
url: image.url
|
||||
});
|
||||
});
|
||||
let data = {
|
||||
context: context.value,
|
||||
attachments: fileList,
|
||||
rollBackId: rollBackId.value
|
||||
};
|
||||
console.log(data)
|
||||
emit("ok", data, props.type);
|
||||
visible.value = false;
|
||||
};
|
||||
|
||||
const selected = (select) => {
|
||||
let userInfoList = []
|
||||
for (let val of select) {
|
||||
let userInfo = {
|
||||
id: val.id,
|
||||
name: val.name,
|
||||
avatar: val.avatar,
|
||||
}
|
||||
userInfoList.push(userInfo)
|
||||
}
|
||||
userList.value = userInfoList
|
||||
console.log('select',userList.value)
|
||||
}
|
||||
const delDept = (i) => {
|
||||
userList.value.splice(i, 1)
|
||||
}
|
||||
defineExpose({
|
||||
show
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.userStyle {
|
||||
width: 45px;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
|
||||
span:first-child {
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: -11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden
|
||||
}
|
||||
}
|
||||
|
||||
.custom-dialog {
|
||||
:deep .el-dialog__header {
|
||||
padding: 10px 20px;
|
||||
|
||||
.el-dialog__title {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 15px;
|
||||
|
||||
.i {
|
||||
font-size: large;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep .el-dialog__footer {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.border {
|
||||
:deep .el-dialog__header {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
:deep .el-dialog__footer {
|
||||
border-top: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
:deep .el-select .el-input__inner {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
:deep .el-select .el-input .el-select__caret {
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
:deep .el-upload--picture-card {
|
||||
width: 65px;
|
||||
height: 65px;
|
||||
line-height: 70px;
|
||||
}
|
||||
|
||||
:deep .el-upload-list--picture-card {
|
||||
.el-upload-list__item {
|
||||
width: 65px !important;
|
||||
height: 65px !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep .el-upload-list__item-status-label {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
98
src/views/workflow/common/InstanceDetails.vue
Normal file
98
src/views/workflow/common/InstanceDetails.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
title="审批详情"
|
||||
size="500px"
|
||||
v-model="approveOpen"
|
||||
direction="rtl">
|
||||
<div v-loading="loadingDetails">
|
||||
<div v-if="!loadingDetails">
|
||||
<div class="top">
|
||||
<div class="top_left">
|
||||
<el-avatar size="large" :src="processInstanceData.userInfo.avatar"></el-avatar>
|
||||
<span
|
||||
style="text-align: center;color: #19191a;font-size: 14px;">{{processInstanceData.userInfo.name}}</span>
|
||||
</div>
|
||||
<div class="top_right" v-if="!loadingDetails">
|
||||
<div style="margin-bottom: 12px">
|
||||
<span style="font-size: 15px;margin-right: 15px;">{{ instance.deploymentName }}</span>
|
||||
<tag dict-type="process_state" :value="instance.state"/>
|
||||
<el-tooltip class="item" effect="dark" content="查看详细流程" placement="top-start">
|
||||
<el-icon style="float: right;font-size: 20px;cursor: pointer"
|
||||
@click.native="processDiagramViewer = true">
|
||||
<View/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color: rgb(108, 108, 108);">编号: {{ instance.processInstanceId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 15px;background:#f5f5f5;"></div>
|
||||
<form-render ref="taskViewForm" :form-items="processInstanceData.formItems"
|
||||
v-model:value="processInstanceData.formData"/>
|
||||
<div style="height: 15px;background:#f5f5f5;"></div>
|
||||
<operation-render :operation-list="processInstanceData.operationList"
|
||||
:state="instance.state"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
<el-dialog title="流程详情" v-model="processDiagramViewer" :close-on-click-modal="true">
|
||||
<process-diagram-viewer v-if="processDiagramViewer"/>
|
||||
<div style="height: 70px;"></div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Tag from '@/components/Tag.vue'
|
||||
import FormRender from '@/views/workflow/form/FormRender.vue'
|
||||
import OperationRender from '@/views/workflow/common/OperationRender.vue'
|
||||
import ProcessDiagramViewer from '@/views/workflow/common/ProcessDiagramViewer.vue'
|
||||
import {getInitiatedInstanceInfo} from "@/api/workflow/process-instance.js";
|
||||
import {defineProps, defineExpose} from 'vue'
|
||||
import {useCacheStore} from '@/stores/cache.js'
|
||||
|
||||
useCacheStore().setCacheKey(['process_state'])
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
|
||||
const processStore = useProcessStore()
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
const approveOpen = ref()
|
||||
const loadingDetails = ref(true)
|
||||
const processDiagramViewer = ref(false)
|
||||
const processInstanceData = ref()
|
||||
|
||||
|
||||
const loadProcessInstance = async (processInstanceId) => {
|
||||
getInitiatedInstanceInfo(processInstanceId).then(res => {
|
||||
let data = res.data
|
||||
processInstanceData.value = data
|
||||
processStore.setDesign(data)
|
||||
processStore.runningList.value = data.runningList;
|
||||
processStore.endList.value = data.endList;
|
||||
processStore.noTakeList.value = data.noTakeList;
|
||||
processStore.refuseList.value = data.refuseList;
|
||||
processStore.passList.value = data.passList;
|
||||
nextTick(() => {
|
||||
loadingDetails.value = false;
|
||||
})
|
||||
})
|
||||
}
|
||||
const init = () => {
|
||||
approveOpen.value = true
|
||||
loadingDetails.value = true
|
||||
loadProcessInstance(props.instance.processInstanceId)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
})
|
||||
|
||||
</script>
|
||||
294
src/views/workflow/common/OperationRender.vue
Normal file
294
src/views/workflow/common/OperationRender.vue
Normal file
@@ -0,0 +1,294 @@
|
||||
<template>
|
||||
<div style="margin: 10px">
|
||||
<el-timeline>
|
||||
<el-timeline-item v-for="(operation,index) in operationList"
|
||||
:key="index" :timestamp="operation.startTime"
|
||||
:icon="operation.icon"
|
||||
:color="operation.color"
|
||||
size="large"
|
||||
placement="top">
|
||||
<el-card>
|
||||
<div style="display: flex;">
|
||||
<div v-for="(user,index) in operation.userInfo" :key="index" class="avatar_name">
|
||||
<el-avatar size="large" :src="user.avatar"></el-avatar>
|
||||
<div v-if="!$slots.dot && operation.userInfo.length > 1"
|
||||
class="el-timeline-item__node" :style="{
|
||||
backgroundColor: user.color
|
||||
}">
|
||||
<el-icon v-if="user.icon" :class="user.class">
|
||||
<component :is="user.icon"/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-tooltip effect="dark" :content="user.name" placement="bottom-start">
|
||||
<span class="username">{{ user.name }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div style="margin-left: 10px;">
|
||||
<div style="color: #c0bebe">{{ operation.operationName }}</div>
|
||||
<div style="font-size: 14px; font-weight: bold;">{{ operation.remark }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="operation.comment">
|
||||
<div style="margin-top: 10px;background:#f5f5f5;padding: 10px;">
|
||||
<div>
|
||||
{{ operation.comment.context }}
|
||||
</div>
|
||||
<div style="margin-top: 10px;" v-if="operation.comment.attachments && operation.comment.attachments.length > 0">
|
||||
<template v-for="(item) in getAttachmentList(operation.comment.attachments,true)">
|
||||
<el-image
|
||||
style="width: 100px; height: 100px"
|
||||
:src="item.url"
|
||||
:preview-src-list="[item.url]">
|
||||
</el-image>
|
||||
</template>
|
||||
<div v-for="(file) in getAttachmentList(operation.comment.attachments,false)">
|
||||
<el-link style="color: #2a99ff" :href="file.url" icon="el-icon-document">{{ file.name }}</el-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
<el-timeline-item :color="timeline.color" :icon="timeline.icon" size="large">
|
||||
<el-card style="font-size: 16px;font-weight: bold;">
|
||||
{{ timeline.context }}
|
||||
</el-card>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {CircleCheckFilled, Close, Loading, MoreFilled} from "@element-plus/icons-vue";
|
||||
import {ref,defineProps} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
operationList: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
state: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return '1'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const timeline = ref()
|
||||
|
||||
const init = () => {
|
||||
switch (props.state) {
|
||||
case '1':
|
||||
timeline.value = {
|
||||
color: '#f78f5f',
|
||||
icon: 'MoreFilled',
|
||||
context: '审批进行中'
|
||||
}
|
||||
break
|
||||
case '2':
|
||||
timeline.value = {
|
||||
color: '#0bbd87',
|
||||
icon: 'CircleCheckFilled',
|
||||
context: '审批通过'
|
||||
}
|
||||
break
|
||||
case '3':
|
||||
timeline.value = {
|
||||
color: '#f56c6c',
|
||||
icon: 'CircleCloseFilled',
|
||||
context: '审核已驳回'
|
||||
}
|
||||
break
|
||||
case '4':
|
||||
timeline.value = {
|
||||
color: '#0bbd87',
|
||||
icon: 'CircleCheckFilled',
|
||||
context: '审批通过'
|
||||
}
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
// let operationListNew = []
|
||||
for (let i = 0;i<props.operationList.length;i++) {
|
||||
let operationNew = initOperationFun(props.operationList[i])
|
||||
let userList = []
|
||||
if (operationNew.userInfo){
|
||||
for (let user of operationNew.userInfo) {
|
||||
let userNew = initUser(user,operationNew.operation)
|
||||
userList.push(userNew)
|
||||
}
|
||||
operationNew.userInfo = userList
|
||||
}
|
||||
// operationListNew.push(operationNew)
|
||||
// this.operationList.push(operationNew)
|
||||
props.operationList[i] = operationNew
|
||||
}
|
||||
}
|
||||
//获取到对应附件的地址
|
||||
|
||||
const getAttachmentList = (attachments, image) => {
|
||||
let result = [];
|
||||
if (attachments){
|
||||
for (let attachment of attachments) {
|
||||
if (attachment.isImage === image) {
|
||||
result.push(attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const initUser = (user,type) => {
|
||||
let state = user.state
|
||||
//创建节点
|
||||
if (state === 'CREATE') {
|
||||
user["icon"] = 'CircleCheckFilled'
|
||||
user["color"] = "#0bbd87"
|
||||
}
|
||||
//审批通过
|
||||
if (state === 'AGREE' || state === 'AUTO_PASS') {
|
||||
user["icon"] = 'CircleCheckFilled'
|
||||
user["color"] = "#0bbd87"
|
||||
}
|
||||
if (type === "CC"){
|
||||
user["icon"] = "Promotion"
|
||||
user["color"] = "#3395f8"
|
||||
}
|
||||
//审批处理中
|
||||
if (state === 'RUNNING') {
|
||||
user["icon"] = 'Loading'
|
||||
user["color"] = "#f78f5f"
|
||||
user["class"] = 'is-loading'
|
||||
}
|
||||
//拒绝后评论
|
||||
if (state === 'REFUSE') {
|
||||
user["icon"] = 'Close'
|
||||
user["color"] = "#f56c6c"
|
||||
}
|
||||
if (state === 'PASS'){
|
||||
user["icon"] = 'MoreFilled'
|
||||
user["color"] = "#c0c4cc"
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
const initOperationFun = (operation) => {
|
||||
let state = operation.state
|
||||
let type = operation.operation
|
||||
//创建节点
|
||||
if (type === 'CREATE') {
|
||||
operation["icon"] = "CircleCheckFilled"
|
||||
operation["color"] = "#0bbd87"
|
||||
}
|
||||
if (type === 'OPINION') {
|
||||
//审批通过
|
||||
if (state === 'AGREE' || state === 'AUTO_PASS') {
|
||||
operation["icon"] = "CircleCheckFilled"
|
||||
operation["color"] = "#0bbd87"
|
||||
operation["remark"] = " (已同意)"
|
||||
}
|
||||
if (state === 'PASS') {
|
||||
operation["icon"] = "SemiSelect"
|
||||
operation["color"] = "#c0c4cc"
|
||||
}
|
||||
//审批处理中
|
||||
if (state === 'RUNNING') {
|
||||
operation["icon"] = "Loading"
|
||||
operation["color"] = "#f78f5f"
|
||||
operation["remark"] = " (处理中)"
|
||||
}
|
||||
//回退
|
||||
if (state === 'ROLLBACK') {
|
||||
operation["icon"] = "RefreshLeft"
|
||||
operation["color"] = "#f78f5f"
|
||||
operation["remark"] = " (回退成功)"
|
||||
}
|
||||
//拒绝操作
|
||||
if (state === 'REFUSE' || state === 'AUTO_REFUSE') {
|
||||
operation["icon"] = "CircleCloseFilled"
|
||||
operation["color"] = "#f56c6c"
|
||||
operation["remark"] = " (拒绝)"
|
||||
}
|
||||
}
|
||||
//抄送
|
||||
if (type === 'CC') {
|
||||
operation["icon"] = "Promotion"
|
||||
operation["color"] = "#3395f8"
|
||||
operation["remark"] = " (抄送成功)"
|
||||
}
|
||||
//评论
|
||||
if (type === 'COMMENT') {
|
||||
//评论
|
||||
if (state === 'COMMENT') {
|
||||
operation["icon"] = "ChatDotRound"
|
||||
operation["color"] = "#0bbd87"
|
||||
operation["remark"] = " (添加了评论)"
|
||||
}
|
||||
}
|
||||
//触发器发送http请求
|
||||
if (type === 'TRIGGER_WEBHOOK') {
|
||||
operation["icon"] = "Share"
|
||||
if (state === 'SUCCESS') {
|
||||
operation["color"] = "#0bbd87"
|
||||
operation["remark"] = " (成功)"
|
||||
} else if (state === 'RUNNING') {
|
||||
operation["color"] = "#f78f5f"
|
||||
operation["remark"] = " (请求中)"
|
||||
} else {
|
||||
operation["color"] = "#f56c6c"
|
||||
operation["remark"] = " (失败)"
|
||||
}
|
||||
}
|
||||
|
||||
//触发器发送邮件
|
||||
if (type === 'TRIGGER_EMAIL') {
|
||||
operation["icon"] = "Message"
|
||||
if (state === 'SUCCESS') {
|
||||
operation["color"] = "#0bbd87"
|
||||
operation["remark"] = " (成功)"
|
||||
} else if (state === 'RUNNING') {
|
||||
operation["color"] = "#f78f5f"
|
||||
operation["remark"] = " (发送中)"
|
||||
} else {
|
||||
operation["color"] = "#f78f5f"
|
||||
operation["remark"] = " (发送中)"
|
||||
}
|
||||
}
|
||||
return operation;
|
||||
}
|
||||
init()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
:deep .el-card__body, .el-main {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.avatar_name {
|
||||
width: 45px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.el-timeline-item__node{
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
.username {
|
||||
width: 45px;
|
||||
padding-top: 2px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden
|
||||
}
|
||||
</style>
|
||||
27
src/views/workflow/common/ProcessDiagramViewer.vue
Normal file
27
src/views/workflow/common/ProcessDiagramViewer.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="scale">
|
||||
<el-button icon="Plus" size="mini" @click="scale += 10" :disabled="scale >= 150" circle></el-button>
|
||||
<span>{{ scale }}%</span>
|
||||
<el-button icon="Minus" size="mini" @click="scale -= 10" :disabled="scale <= 40" circle></el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 40px">
|
||||
<div :style="'transform: scale('+ scale / 100 +');'">
|
||||
<div id="previewProcess">
|
||||
<process-tree mode="preview" ref="processTreePreview" id-name="previewProcess"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProcessTree from '@/views/workflow/process/ProcessTree.vue'
|
||||
|
||||
const processTreePreview = ref()
|
||||
const scale = ref(100)
|
||||
|
||||
nextTick(()=>{
|
||||
processTreePreview.value.init()
|
||||
})
|
||||
</script>
|
||||
256
src/views/workflow/common/TaskDetails.vue
Normal file
256
src/views/workflow/common/TaskDetails.vue
Normal file
@@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
title="审批详情"
|
||||
size="500px"
|
||||
v-model="approveOpen"
|
||||
direction="rtl">
|
||||
<div v-loading="loadingDetails">
|
||||
<div v-if="!loadingDetails">
|
||||
<div class="top">
|
||||
<div class="top_left">
|
||||
<el-avatar size="large" :src="processTask.userInfo.avatar"></el-avatar>
|
||||
<span
|
||||
style="text-align: center;color: #19191a;font-size: 14px;">{{ processTask.userInfo.name }}</span>
|
||||
</div>
|
||||
<div class="top_right">
|
||||
<div style="margin-bottom: 12px">
|
||||
<span style="font-size: 15px;margin-right: 15px;">{{ task.processName }}</span>
|
||||
<tag dict-type="process_state" :value="task.state"/>
|
||||
</div>
|
||||
<div>
|
||||
<span style="color: rgb(108, 108, 108);">编号: {{ task.taskId }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="height: 15px;background:#f5f5f5;"></div>
|
||||
<form-render ref="taskViewForm" :form-items="processTask.formItems"
|
||||
v-model:value="processTask.formData"/>
|
||||
<div style="height: 15px;background:#f5f5f5;"></div>
|
||||
<operation-render :operation-list="processTask.operationList"
|
||||
:state="task.state"/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-footer style="display: flex;justify-content: right;align-items: center ">
|
||||
<div style="margin-right: 20px;">
|
||||
<el-dropdown>
|
||||
<span style="color: #2a99ff;cursor: pointer;margin-top: 8px;">
|
||||
更多<el-icon style="margin-left: 5px"><ArrowDown/></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item icon="Avatar" @click.native="showTransferBox()">转交</el-dropdown-item>
|
||||
<el-dropdown-item icon="Failed" @click.native="showBackNodeBox()">退回</el-dropdown-item>
|
||||
<el-dropdown-item icon="Stamp" @click.native="showAddApprovalBox()">加签</el-dropdown-item>
|
||||
<el-dropdown-item icon="RefreshLeft" @click.native="showRevocationBox()">撤销</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button type="danger" size="mini" plain round style="margin-left: 20px"
|
||||
@click="showRefuseBox()">拒绝
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" round @click="submitTask()">同意</el-button>
|
||||
</div>
|
||||
<div @click="showCommentBox" style="display: flex;align-items: center">
|
||||
<el-icon size="18"><ChatLineRound /></el-icon>
|
||||
<span style="font-size: 14px;margin-left: 5px">评论</span>
|
||||
</div>
|
||||
</el-footer>
|
||||
</template>
|
||||
</el-drawer>
|
||||
|
||||
<comment ref="comment"
|
||||
:title="commentInfo.title"
|
||||
:type="commentInfo.type"
|
||||
:max-size="5"
|
||||
:attachment-max-size="20" @ok="commentConfirm"/>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FormRender from '@/views/workflow/form/FormRender.vue'
|
||||
import OperationRender from '@/views/workflow/common/OperationRender.vue'
|
||||
import Comment from '@/views/workflow/common/Comment.vue'
|
||||
import Tag from '@/components/Tag.vue'
|
||||
import {getTaskInfo, refuseTask, rollBackTask, addComment, completeTask} from "@/api/workflow/process-task.js";
|
||||
import {defineProps, defineExpose, defineEmits} from 'vue'
|
||||
import {ElMessage} from "element-plus";
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
|
||||
const processStore = useProcessStore()
|
||||
import {useCacheStore} from '@/stores/cache.js'
|
||||
|
||||
useCacheStore().setCacheKey(['process_state'])
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
const comment = ref()
|
||||
const approveOpen = ref()
|
||||
const commentInfo = ref({
|
||||
title: null,
|
||||
type: null, //1 添加评论 2 拒绝
|
||||
})
|
||||
const loadingDetails = ref(true)
|
||||
const processTask = ref()
|
||||
const taskViewForm = ref()
|
||||
|
||||
|
||||
const submitTask = () => {
|
||||
taskViewForm.value.validate(valid => {
|
||||
if (valid) {
|
||||
let params = {
|
||||
taskId: props.task.taskId,
|
||||
formData: JSON.stringify(processTask.value.formData)
|
||||
};
|
||||
completeTask(params).then(res => {
|
||||
approveOpen.value = false;
|
||||
emit("refresh")
|
||||
ElMessage.success(res.msg);
|
||||
});
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const loadProcessTask = async (taskId) => {
|
||||
getTaskInfo(taskId).then(res => {
|
||||
processTask.value = res.data
|
||||
processStore.setDesign(res.data)
|
||||
processStore.userTaskOption.value = res.data.userTaskOption;
|
||||
nextTick(() => {
|
||||
loadingDetails.value = false;
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const showCommentBox = () => {
|
||||
commentInfo.value = {
|
||||
title: "添加评论",
|
||||
type: 1
|
||||
}
|
||||
commentShow()
|
||||
}
|
||||
|
||||
const showRefuseBox = () => {
|
||||
commentInfo.value = {
|
||||
title: "拒绝任务",
|
||||
type: 2
|
||||
}
|
||||
commentShow()
|
||||
}
|
||||
|
||||
const showTransferBox = () => {
|
||||
commentInfo.value = {
|
||||
title: "转交给其他人审批",
|
||||
type: 3
|
||||
}
|
||||
commentShow()
|
||||
}
|
||||
const showBackNodeBox = () => {
|
||||
commentInfo.value = {
|
||||
title: "退回到之前节点",
|
||||
type: 4
|
||||
}
|
||||
commentShow()
|
||||
}
|
||||
const showAddApprovalBox = () => {
|
||||
commentInfo.value = {
|
||||
title: "加签",
|
||||
type: 5
|
||||
}
|
||||
commentShow()
|
||||
}
|
||||
const showRevocationBox = () => {
|
||||
commentInfo.value = {
|
||||
title: "撤销当前流程",
|
||||
type: 6
|
||||
}
|
||||
commentShow()
|
||||
}
|
||||
const commentShow = () => {
|
||||
nextTick(() => {
|
||||
comment.value.show()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论回调
|
||||
* @param data
|
||||
* @param type
|
||||
*/
|
||||
const commentConfirm = (data, type) => {
|
||||
console.log(data, type)
|
||||
switch (type) {
|
||||
case 1:
|
||||
submitComment(data);
|
||||
break;
|
||||
case 2:
|
||||
submitRefuse(data);
|
||||
break;
|
||||
case 4:
|
||||
submitRollBack(data);
|
||||
break;
|
||||
default :
|
||||
break;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 拒绝任务
|
||||
* @param data
|
||||
*/
|
||||
const submitRefuse = (data) => {
|
||||
let params = {
|
||||
taskId: props.task.taskId,
|
||||
comment: data
|
||||
};
|
||||
refuseTask(params).then(res => {
|
||||
approveOpen.value = false;
|
||||
emit("refresh")
|
||||
ElMessage.success(res.msg);
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 回退
|
||||
*/
|
||||
const submitRollBack = (data) => {
|
||||
let params = {
|
||||
taskId: props.task.taskId,
|
||||
rollBackId: data.rollBackId,
|
||||
comment: data,
|
||||
};
|
||||
rollBackTask(params).then(res => {
|
||||
approveOpen.value = false;
|
||||
emit("refresh")
|
||||
ElMessage.success(res.msg);
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 提交评论
|
||||
* @param data
|
||||
*/
|
||||
const submitComment = (data) => {
|
||||
data['taskId'] = props.task.taskId
|
||||
addComment(data).then(res => {
|
||||
approveOpen.value = false;
|
||||
ElMessage.success(res.msg);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const init = () => {
|
||||
approveOpen.value = true
|
||||
loadingDetails.value = true
|
||||
loadProcessTask(props.task.taskId)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
init
|
||||
})
|
||||
|
||||
</script>
|
||||
255
src/views/workflow/form/ComponentsConfigExport.js
Normal file
255
src/views/workflow/form/ComponentsConfigExport.js
Normal file
@@ -0,0 +1,255 @@
|
||||
import {ScaleToOriginal,EditPen,More,Edit,Tickets,Warning,CircleCheck,Money,FolderChecked,Calendar,Picture,User,SetUp,Star,FolderOpened } from '@element-plus/icons-vue'
|
||||
|
||||
|
||||
export const ValueType = {
|
||||
string: 'String',
|
||||
object: 'Object',
|
||||
array: 'Array',
|
||||
number: 'Number',
|
||||
date: 'Date',
|
||||
user: 'User',
|
||||
dept: 'Dept',
|
||||
star: 'star',
|
||||
dateRange: 'DateRange'
|
||||
}
|
||||
|
||||
export const baseComponents = [
|
||||
{
|
||||
name: '布局',
|
||||
components: [
|
||||
{
|
||||
title: '分栏布局',
|
||||
name: 'SpanLayout',
|
||||
icon: 'ScaleToOriginal',
|
||||
value: [],
|
||||
valueType: ValueType.array,
|
||||
props: {
|
||||
items: []
|
||||
}
|
||||
}
|
||||
]
|
||||
}, {
|
||||
name: '基础组件',
|
||||
components: [
|
||||
{
|
||||
title: '单行文本输入',
|
||||
name: 'TextInput',
|
||||
icon: 'EditPen',
|
||||
value: '',
|
||||
valueType: ValueType.string,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '多行文本输入',
|
||||
name: 'TextareaInput',
|
||||
icon: 'More',
|
||||
value: '',
|
||||
valueType: ValueType.string,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '数字输入框',
|
||||
name: 'NumberInput',
|
||||
icon: 'Edit',
|
||||
value: '',
|
||||
valueType: ValueType.number,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '金额输入框',
|
||||
name: 'AmountInput',
|
||||
icon: 'Money',
|
||||
value: '',
|
||||
valueType: ValueType.number,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
showChinese: true
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '单选框',
|
||||
name: 'SelectInput',
|
||||
icon: 'CircleCheck',
|
||||
value: '',
|
||||
valueType: ValueType.string,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
expanding: false,
|
||||
options: ['选项1', '选项2']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '多选框',
|
||||
name: 'MultipleSelect',
|
||||
icon: 'FolderChecked',
|
||||
value: [],
|
||||
valueType: ValueType.array,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
expanding: false,
|
||||
options: ['选项1', '选项2']
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '日期时间点',
|
||||
name: 'DateTime',
|
||||
icon: 'Calendar',
|
||||
value: '',
|
||||
valueType: ValueType.date,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
format: 'YYYY-MM-DD HH:mm',
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '日期时间区间',
|
||||
name: 'DateTimeRange',
|
||||
icon: 'Calendar',
|
||||
valueType: ValueType.dateRange,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
placeholder: ['开始时间', '结束时间'],
|
||||
format: 'YYYY-MM-DD HH:mm',
|
||||
showLength: false,
|
||||
length: 0
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '上传图片',
|
||||
name: 'ImageUpload',
|
||||
icon: 'Picture',
|
||||
value: [],
|
||||
valueType: ValueType.array,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
maxSize: 5, //图片最大大小MB
|
||||
maxNumber: 10, //最大上传数量
|
||||
enableZip: true, //图片压缩后再上传
|
||||
placeholder: '请选择图片',
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '上传附件',
|
||||
name: 'FileUpload',
|
||||
icon: 'FolderOpened',
|
||||
value: [],
|
||||
valueType: ValueType.array,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
onlyRead: false, //是否只读,false只能在线预览,true可以下载
|
||||
maxSize: 100, //文件最大大小MB
|
||||
maxNumber: 10, //最大上传数量
|
||||
fileTypes: [], //限制文件上传类型,
|
||||
placeholder: '请选择附件',
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '人员选择',
|
||||
name: 'UserPicker',
|
||||
icon: 'User' ,
|
||||
value: [],
|
||||
valueType: ValueType.user,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
multiple: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '部门选择',
|
||||
name: 'DeptPicker',
|
||||
icon: 'SetUp',
|
||||
value: [],
|
||||
valueType: ValueType.dept,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
multiple: false
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '评分',
|
||||
name: 'RatePicker',
|
||||
icon: 'Star',
|
||||
value: '',
|
||||
valueType: ValueType.star,
|
||||
props: {
|
||||
color: '#f0a732',
|
||||
max: 5,
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
showScore: true,
|
||||
enableHalf: true,
|
||||
placeholder: undefined,
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '说明文字',
|
||||
name: 'Description',
|
||||
icon: 'Warning',
|
||||
value: '',
|
||||
valueType: ValueType.string,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true
|
||||
}
|
||||
},
|
||||
]
|
||||
}, {
|
||||
name: '扩展组件',
|
||||
components: [
|
||||
{
|
||||
title: '明细表',
|
||||
name: 'TableList',
|
||||
icon: 'Tickets',
|
||||
value: [],
|
||||
valueType: ValueType.array,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
showBorder: true,
|
||||
rowLayout: true,
|
||||
showSummary: false,
|
||||
summaryColumns: [],
|
||||
maxSize: 0, //最大条数,为0则不限制
|
||||
columns: [] //列设置
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '签名',
|
||||
name: 'SignPanel',
|
||||
icon: 'EditPen',
|
||||
value: [],
|
||||
valueType: ValueType.string,
|
||||
props: {
|
||||
required: false,
|
||||
enablePrint: true,
|
||||
isCrop: true,
|
||||
lineColor: '#ff0000',
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
export default {
|
||||
baseComponents
|
||||
}
|
||||
|
||||
89
src/views/workflow/form/FormComponentConfig.vue
Normal file
89
src/views/workflow/form/FormComponentConfig.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form label-width="90px" v-if="selectFormItem.name !== 'SpanLayout'">
|
||||
<el-form-item label="表单ID">
|
||||
<el-input size="small" clearable v-model="selectFormItem.id" disabled/>
|
||||
</el-form-item>
|
||||
<el-form-item label="表单名称">
|
||||
<el-input size="small" clearable v-model="selectFormItem.title"/>
|
||||
</el-form-item>
|
||||
<component :is="selectFormItem.name" v-model="selectFormItem.props"/>
|
||||
<el-form-item label="必填项">
|
||||
<el-switch v-model="selectFormItem.props.required"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="可打印">
|
||||
<el-switch v-model="selectFormItem.props.enablePrint"></el-switch>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-empty v-else description="当前组件不支持配置"></el-empty>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TextInput from './config/TextInputConfig.vue'
|
||||
import NumberInput from './config/NumberInputConfig.vue'
|
||||
import AmountInput from './config/AmountInputConfig.vue'
|
||||
import TextareaInput from './config/TextareaInputConfig.vue'
|
||||
import MultipleSelect from './config/SelectInputConfig.vue'
|
||||
import SelectInput from './config/SelectInputConfig.vue'
|
||||
import DateTime from './config/DateTimeConfig.vue'
|
||||
import DateTimeRange from './config/DateTimeRangeConfig.vue'
|
||||
import ImageUpload from './config/ImageUploadConfig.vue'
|
||||
import FileUpload from './config/FileUploadConfig.vue'
|
||||
import Description from './config/DescriptionConfig.vue'
|
||||
import MoneyInput from './config/MoneyInputConfig.vue'
|
||||
import DeptPicker from './config/OrgPickerConfig.vue'
|
||||
import UserPicker from './config/OrgPickerConfig.vue'
|
||||
import TableList from './config/TableListConfig.vue'
|
||||
import SignPanel from './config/SignPanelConfig.vue'
|
||||
import RatePicker from './config/RatePickerConfig.vue'
|
||||
import {computed,ref} from "vue";
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
const processStore = useProcessStore()
|
||||
// const component = ref()
|
||||
|
||||
const selectFormItem = computed(()=>{
|
||||
return processStore.getSelectedFormItem()
|
||||
})
|
||||
|
||||
// const init = () => {
|
||||
// console.log(processStore.getSelectedFormItem().name)
|
||||
// switch (processStore.getSelectedFormItem().name) {
|
||||
// case 'TextInput':
|
||||
// component.value = TextInput;
|
||||
// break;
|
||||
// case 'TextareaInput':
|
||||
// component.value = TextareaInput;
|
||||
// break;
|
||||
// case 'NumberInput':
|
||||
// component.value = NumberInput;
|
||||
// break;
|
||||
// case 'AmountInput':
|
||||
// component.value = AmountInput;
|
||||
// break;
|
||||
// case 'SelectInput':
|
||||
// component.value = SelectInput;
|
||||
// break;
|
||||
// case 'MultipleSelect':
|
||||
// component.value = MultipleSelect;
|
||||
// break;
|
||||
// case 'DateTime':
|
||||
// component.value = DateTime;
|
||||
// break;
|
||||
// case 'DateTimeRange':
|
||||
// component.value = DateTimeRange;
|
||||
// break;
|
||||
// case 'ImageUpload':
|
||||
// component.value = ImageUpload;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
// init()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
596
src/views/workflow/form/FormDesign.vue
Normal file
596
src/views/workflow/form/FormDesign.vue
Normal file
@@ -0,0 +1,596 @@
|
||||
<template>
|
||||
<div class="form-design">
|
||||
<!--左侧组件库-->
|
||||
<div style="width: 350px">
|
||||
<div class="components-nav">
|
||||
<span @click="libSelect = 0">组件库</span>
|
||||
</div>
|
||||
<div style="overflow-y: auto;height:calc(100vh - 172px);">
|
||||
<div class="components" v-for="(item, i) in baseComponents" :key="i">
|
||||
<p>{{ item.name }}</p>
|
||||
<ul>
|
||||
<draggable
|
||||
class="drag"
|
||||
:list="item.components"
|
||||
:group="{ name: 'form', pull: 'clone', put: false }"
|
||||
itemKey="name"
|
||||
@start="isStart = true" @end="isStart = false" :clone="clone"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div class="list-group-item">
|
||||
<li :key="id">
|
||||
<el-icon>
|
||||
<component :is="element.icon"/>
|
||||
</el-icon>
|
||||
<span>{{ element.title }}</span>
|
||||
</li>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--中间编辑区-->
|
||||
<div class="layout-main">
|
||||
<div class="tool-nav">
|
||||
<div>
|
||||
<el-tooltip class="item" effect="dark" content="撤销" placement="bottom-start">
|
||||
<el-icon>
|
||||
<RefreshLeft/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" effect="dark" content="恢复" placement="bottom-start">
|
||||
<el-icon>
|
||||
<RefreshRight/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-tooltip class="item" effect="dark" content="预览表单" placement="bottom-start">
|
||||
<el-icon @click="viewForms">
|
||||
<View/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" effect="dark" content="移动端" placement="bottom-start">
|
||||
<el-icon :class="{'select': showMobile}" @click="showMobile = true">
|
||||
<Cellphone/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" effect="dark" content="PC端" placement="bottom-start">
|
||||
<el-icon :class="{'select': !showMobile}" @click="showMobile = false">
|
||||
<Monitor/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="work-form">
|
||||
<div :class="{'mobile': showMobile, 'pc': !showMobile}">
|
||||
<div :class="{'bd': showMobile}">
|
||||
<div :class="{'form-content': showMobile}">
|
||||
<div class="form">
|
||||
<div class="tip" v-show="formItems.length === 0 && !isStart">👈 请在左侧选择控件并拖至此处</div>
|
||||
<draggable class="drag-from" :list="formItems" group="form"
|
||||
:options="{animation: 300, chosenClass:'choose', sort:true}"
|
||||
@start="drag = true; selectFormItem = null" @end="drag = false">
|
||||
<template #item="{ element, index }">
|
||||
<div class="form-item" @click="selectItem(element)"
|
||||
:style="getSelectedClass(element)">
|
||||
<div class="form-header">
|
||||
<p><span v-if="element.props.required">*</span>{{ element.title }}</p>
|
||||
<div class="option">
|
||||
<!--<i class="el-icon-copy-document" @click="copy"></i>-->
|
||||
<!-- <el-icon @click="copy">-->
|
||||
<!-- <CopyDocument/>-->
|
||||
<!-- </el-icon>-->
|
||||
<el-icon size="25px" @click="delItem(index)">
|
||||
<Close/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<form-design-render :config="element"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="layout-param" v-if="isShow">
|
||||
<div class="tool-nav-r" v-if="selectFormItem">
|
||||
<i :class="selectFormItem.icon" style="margin-right: 5px; font-size: medium"></i>
|
||||
<span>{{ selectFormItem.title }}</span>
|
||||
</div>
|
||||
<div v-if="!selectFormItem || forms.length === 0" class="tip">
|
||||
😀 选中控件后在这里进行编辑
|
||||
</div>
|
||||
<div style="text-align:left; padding: 10px" v-else>
|
||||
<form-component-config/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="viewFormVisible" title="表单预览" width="800px">
|
||||
<el-radio-group v-model="formPreviewModel" @change="formPreviewModelChange">
|
||||
<el-radio-button label="E">编辑</el-radio-button>
|
||||
<el-radio-button label="R">查看</el-radio-button>
|
||||
</el-radio-group>
|
||||
<form-render v-if="viewFormVisibleRender" ref="form" :form-items="forms" v-model:value="formValue"
|
||||
:is-preview="true" :mode="formPreviewModel"/>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import draggable from 'vuedraggable';
|
||||
import FormRender from './FormRender.vue'
|
||||
import FormDesignRender from './FormDesignRender.vue'
|
||||
import FormComponentConfig from './FormComponentConfig.vue'
|
||||
import {ValueType} from "./ComponentsConfigExport";
|
||||
import {CopyDocument, Close} from '@element-plus/icons-vue'
|
||||
import {ref, computed, defineExpose} from 'vue'
|
||||
import {baseComponents} from './ComponentsConfigExport'
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
|
||||
const processStore = useProcessStore()
|
||||
|
||||
const form = ref()
|
||||
const isStart = ref(false)
|
||||
const isShow = ref(true)
|
||||
const showMobile = ref(false)
|
||||
const formValue = ref({})
|
||||
const formPreviewModel = ref("E")
|
||||
const viewFormVisible = ref(false)
|
||||
const viewFormVisibleRender = ref(true)
|
||||
const forms = ref(processStore.getDesign().formItems)
|
||||
const formItems = computed(() => {
|
||||
return processStore.getDesign().formItems
|
||||
})
|
||||
const selectFormItem = computed({
|
||||
get() {
|
||||
return processStore.getSelectedFormItem()
|
||||
},
|
||||
set(val) {
|
||||
processStore.setSelectedFormItem(val)
|
||||
},
|
||||
})
|
||||
|
||||
const clone = (obj) => {
|
||||
obj.id = getId()
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
}
|
||||
// const copy = (node, index) => {
|
||||
// console.log(node)
|
||||
// processStore.getDesign().formItems.push(index + 1, 0,{...node})
|
||||
// }
|
||||
|
||||
const formPreviewModelChange = () => {
|
||||
// 卸载
|
||||
// viewFormVisibleRender.value= false
|
||||
// nextTick(()=>{
|
||||
// // 加载
|
||||
// viewFormVisibleRender.value= true
|
||||
// })
|
||||
}
|
||||
const delItem = (index) => {
|
||||
ElMessageBox.confirm('删除组件将会连带删除包含该组件的条件以及相关设置,是否继续?', '提示', {
|
||||
confirmButtonText: '确 定',
|
||||
cancelButtonText: '取 消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
if (processStore.getDesign().formItems[index].name === 'SpanLayout') {
|
||||
//删除的是分栏则遍历删除分栏内所有子组件
|
||||
processStore.getDesign().formItems[index].props.items.forEach(item => {
|
||||
removeFormItemAbout(item)
|
||||
})
|
||||
processStore.getDesign().formItems[index].props.items.length = 0
|
||||
} else {
|
||||
removeFormItemAbout(processStore.getDesign().formItems[index])
|
||||
}
|
||||
processStore.getDesign().formItems.splice(index, 1)
|
||||
})
|
||||
}
|
||||
|
||||
const viewForms = () => {
|
||||
viewFormVisible.value = true
|
||||
formValue.value = {}
|
||||
}
|
||||
|
||||
const removeFormItemAbout = async (item) => {
|
||||
processStore.nodeMap.forEach(node => {
|
||||
//搜寻条件,进行移除
|
||||
if (node.type === 'CONDITION') {
|
||||
node.props.groups.forEach(group => {
|
||||
let i = group.cids.remove(item.id)
|
||||
if (i > -1) {
|
||||
//从子条件移除
|
||||
group.conditions.splice(i, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
//搜寻权限,进行移除
|
||||
if (node.type === 'ROOT' || node.type === 'APPROVAL' || node.type === 'CC') {
|
||||
node.props.formPerms.removeByKey('id', item.id)
|
||||
if (node.props.formUser === item.id) {
|
||||
node.props.formUser = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getId = () => {
|
||||
return 'field' + (Math.floor(Math.random() * (99999 - 10000)) + 10000).toString()
|
||||
+ new Date().getTime().toString().substring(5);
|
||||
}
|
||||
const getSelectedClass = (element) => {
|
||||
return processStore.getSelectedFormItem() && processStore.getSelectedFormItem().id === element.id ?
|
||||
'border-left: 4px solid #409eff' : ''
|
||||
}
|
||||
|
||||
const selectItem = (element) => {
|
||||
isShow.value = false
|
||||
selectFormItem.value = element
|
||||
nextTick(() => {
|
||||
isShow.value = true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const validateItem = (err, titleSet, item) => {
|
||||
if (titleSet.has(item.title) && item.name !== 'SpanLayout') {
|
||||
err.push(`表单 ${item.title} 名称重复`)
|
||||
}
|
||||
titleSet.add(item.title)
|
||||
if (item.name === 'SelectInput' || item.name === 'MultipleSelect') {
|
||||
if (item.props.options.length === 0) {
|
||||
err.push(`${item.title} 未设置选项`)
|
||||
}
|
||||
} else if (item.name === 'TableList') {
|
||||
if (item.props.columns.length === 0) {
|
||||
err.push(`明细表 ${item.title} 内未添加组件`)
|
||||
}
|
||||
} else if (item.name === 'SpanLayout') {
|
||||
if (item.props.items.length === 0) {
|
||||
err.push('分栏内未添加组件')
|
||||
} else {
|
||||
item.props.items.forEach(sub => validateItem(err, titleSet, sub))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
let err = []
|
||||
if (processStore.getDesign().formItems.length > 0) {
|
||||
let titleSet = new Set()
|
||||
processStore.getDesign().formItems.forEach(item => {
|
||||
//主要校验表格及分栏/选择器/表单名称/是否设置
|
||||
validateItem(err, titleSet, item)
|
||||
})
|
||||
} else {
|
||||
err.push('表单为空,请添加组件')
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.form-design {
|
||||
display: flex;
|
||||
|
||||
}
|
||||
|
||||
.choose {
|
||||
border: 1px dashed #1890FF !important;
|
||||
}
|
||||
|
||||
.process-form {
|
||||
//.el-form-item__label {
|
||||
// padding: 0 0;
|
||||
//}
|
||||
}
|
||||
|
||||
.components-nav {
|
||||
box-sizing: content-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 12px 12px 0;
|
||||
height: 28px;
|
||||
box-shadow: 0 2px 4px 0 rgba(17, 31, 44, 0.04);
|
||||
border: 1px solid #ecedef;
|
||||
border-radius: 16px;
|
||||
background-color: #fff;
|
||||
|
||||
.selected {
|
||||
color: #1890FF;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-left: 1px solid #f5f6f6;
|
||||
border-right: 1px solid #f5f6f6;
|
||||
}
|
||||
|
||||
span {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 12px;
|
||||
color: rgba(17, 31, 44, 0.72);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #1890FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.components {
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
//margin-top: 20px;
|
||||
//padding: 0 20px;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
color: rgba(17, 31, 44, 0.85);
|
||||
|
||||
& > p {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.drag {
|
||||
margin-left: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
li {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 124px;
|
||||
height: 38px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 8px;
|
||||
cursor: grab;
|
||||
background-color: #fff;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid #1890FF;
|
||||
color: #1890FF;
|
||||
}
|
||||
|
||||
i {
|
||||
margin: 0 12px;
|
||||
}
|
||||
}
|
||||
|
||||
li:nth-child(odd) {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
width: 80%;
|
||||
background-color: #feffff;
|
||||
|
||||
.tool-nav {
|
||||
font-size: medium;
|
||||
padding: 8px 20px;
|
||||
background: #fafafb;
|
||||
border-bottom: 1px solid #ebecee;
|
||||
|
||||
div:first-child {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
|
||||
i {
|
||||
margin-right: 10px
|
||||
}
|
||||
}
|
||||
|
||||
div:last-child {
|
||||
float: right;
|
||||
|
||||
i {
|
||||
margin-left: 10px
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
color: #7a7a7a;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #4b4b4b;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.work-form {
|
||||
margin: 0 auto;
|
||||
//height: calc(100% - 38px);
|
||||
overflow-y: auto;
|
||||
background: rgb(245, 246, 246);
|
||||
border-left: 1px solid rgb(235, 236, 238);
|
||||
border-right: 1px solid rgb(235, 236, 238);
|
||||
margin-right: -1px;
|
||||
|
||||
.pc {
|
||||
margin-top: 4%;
|
||||
padding: 0 150px;
|
||||
|
||||
.drag-from {
|
||||
height: calc(100vh - 190px);
|
||||
background-color: rgb(245, 246, 246);
|
||||
|
||||
.form-item, li {
|
||||
cursor: grab;
|
||||
background: #ffffff;
|
||||
padding: 10px;
|
||||
border: 1px solid #ebecee;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mobile {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: 360px;
|
||||
max-height: 640px;
|
||||
margin-top: 4%;
|
||||
border-radius: 24px;
|
||||
box-shadow: 0 8px 40px 0 rgba(17, 31, 44, 0.12);
|
||||
|
||||
.bd {
|
||||
border: 1px solid rgba(17, 31, 44, 0.08);
|
||||
border-radius: 24px;
|
||||
padding: 10px 10px;
|
||||
background-color: #ffffff;
|
||||
|
||||
.form-content {
|
||||
padding: 3px 2px;
|
||||
border-radius: 14px;
|
||||
background-color: #f2f4f5;
|
||||
|
||||
.drag-from {
|
||||
width: 100%;
|
||||
height: calc(100vh - 190px);
|
||||
min-height: 200px;
|
||||
max-height: 600px;
|
||||
}
|
||||
|
||||
.form {
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
max-height: 640px;
|
||||
|
||||
.form-item, li {
|
||||
border: 1px solid #ffffff;
|
||||
list-style: none;
|
||||
background: #ffffff;
|
||||
padding: 10px;
|
||||
margin: 5px 0;
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tip {
|
||||
//float: left;
|
||||
margin: 0 auto;
|
||||
width: 65%;
|
||||
max-width: 400px;
|
||||
padding: 35px 20px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed rgba(25, 31, 37, 0.12);
|
||||
margin-top: 50px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: rgb(122, 122, 122);
|
||||
z-index: 9999;
|
||||
|
||||
&:hover {
|
||||
border: 1px dashed #1890FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.layout-param {
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: rgb(122, 122, 122);
|
||||
|
||||
.tool-nav-r {
|
||||
text-align: left;
|
||||
font-size: small;
|
||||
border-left: 1px solid #ebecee;
|
||||
padding: 8px 20px;
|
||||
background: #fafafb;
|
||||
border-bottom: 1px solid #ebecee;
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 150px;
|
||||
}
|
||||
}
|
||||
|
||||
.flip-list-move {
|
||||
transition: transform 0.5s;
|
||||
}
|
||||
|
||||
.no-move {
|
||||
transition: transform 0s;
|
||||
}
|
||||
|
||||
.select {
|
||||
color: #4b4b4b !important;
|
||||
}
|
||||
|
||||
.form-header {
|
||||
font-size: small;
|
||||
color: #818181;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
margin: 0 0 10px 0;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 3px;
|
||||
color: rgb(217, 0, 19);
|
||||
}
|
||||
}
|
||||
|
||||
.option {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
|
||||
i {
|
||||
font-size: large;
|
||||
cursor: pointer;
|
||||
color: #8c8c8c;
|
||||
padding: 5px;
|
||||
|
||||
&:hover {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
</style>
|
||||
135
src/views/workflow/form/FormDesignRender.vue
Normal file
135
src/views/workflow/form/FormDesignRender.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<component ref="form" :is="com" :config="config" :mode="mode" :perm="perm" @update="update" v-model:value="_value"
|
||||
v-bind="config.props"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import TextInput from './components/TextInput.vue'
|
||||
import TextareaInput from './components/TextareaInput.vue'
|
||||
import NumberInput from './components/NumberInput.vue'
|
||||
import AmountInput from './components/AmountInput.vue'
|
||||
import SelectInput from './components/SelectInput.vue'
|
||||
import MultipleSelect from './components/MultipleSelect.vue'
|
||||
import DateTime from './components/DateTime.vue'
|
||||
import DateTimeRange from './components/DateTimeRange.vue'
|
||||
import ImageUpload from './components/ImageUpload.vue'
|
||||
import FileUpload from './components/FileUpload.vue'
|
||||
import SpanLayout from './components/SpanLayout.vue'
|
||||
import UserPicker from './components/UserPicker.vue'
|
||||
import DeptPicker from './components/DeptPicker.vue'
|
||||
import RatePicker from './components/RatePicker.vue'
|
||||
import Description from './components/Description.vue'
|
||||
import TableList from './components/TableList.vue'
|
||||
import SignPanel from './components/SignPanel.vue'
|
||||
import {defineProps, defineEmits, computed} from 'vue'
|
||||
|
||||
const emit = defineEmits()
|
||||
const com = ref()
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: undefined
|
||||
},
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
})
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
|
||||
// const _value = ref(props.value)
|
||||
watch(() => props.value, (value) => {
|
||||
_value.value = value
|
||||
})
|
||||
watch(() => props.config, (value) => {
|
||||
init(value.name)
|
||||
})
|
||||
watch(() => props.perm, (value) => {
|
||||
props.perm=value
|
||||
})
|
||||
const init = (value) => {
|
||||
switch (value) {
|
||||
case 'TextInput':
|
||||
com.value = TextInput;
|
||||
break;
|
||||
case 'TextareaInput':
|
||||
com.value = TextareaInput;
|
||||
break;
|
||||
case 'NumberInput':
|
||||
com.value = NumberInput;
|
||||
break;
|
||||
case 'AmountInput':
|
||||
com.value = AmountInput;
|
||||
break;
|
||||
case 'SelectInput':
|
||||
com.value = SelectInput;
|
||||
break;
|
||||
case 'MultipleSelect':
|
||||
com.value = MultipleSelect;
|
||||
break;
|
||||
case 'DateTime':
|
||||
com.value = DateTime;
|
||||
break;
|
||||
case 'DateTimeRange':
|
||||
com.value = DateTimeRange;
|
||||
break;
|
||||
case 'ImageUpload':
|
||||
com.value = ImageUpload;
|
||||
break;
|
||||
case 'FileUpload':
|
||||
com.value = FileUpload;
|
||||
break;
|
||||
case 'SpanLayout':
|
||||
com.value = SpanLayout;
|
||||
break;
|
||||
case 'UserPicker':
|
||||
com.value = UserPicker;
|
||||
break;
|
||||
case 'DeptPicker':
|
||||
com.value = DeptPicker;
|
||||
break;
|
||||
case 'RatePicker':
|
||||
com.value = RatePicker;
|
||||
break;
|
||||
case 'Description':
|
||||
com.value = Description;
|
||||
break;
|
||||
case 'TableList':
|
||||
com.value = TableList;
|
||||
break;
|
||||
case 'SignPanel':
|
||||
com.value = SignPanel;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
init(props.config.name)
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
130
src/views/workflow/form/FormRender.vue
Normal file
130
src/views/workflow/form/FormRender.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<el-form v-if="showForm" ref="formRenderView" class="process-form" label-position="top" :rules="rules"
|
||||
:model="_value">
|
||||
<div v-for="(item, index) in formItems" :key="item.name + index">
|
||||
<el-form-item v-if="item.name !== 'SpanLayout' && item.name !== 'Description'"
|
||||
:prop="item.id" :label="item.title">
|
||||
<!-- {{_value[item.id]}}-->
|
||||
<form-design-render :ref="`sub-item_${item.id}`" :perm="mode" v-model:value="_value[item.id]" mode="PC"
|
||||
:config="item"/>
|
||||
</el-form-item>
|
||||
<form-design-render ref="span-layout" v-else :perm="mode" v-model:value="_value" mode="PC" :config="item"/>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FormDesignRender from './FormDesignRender.vue'
|
||||
import {getCurrentInstance} from '@vue/runtime-core';
|
||||
import {defineProps, defineEmits} from 'vue'
|
||||
|
||||
const currentInstance = getCurrentInstance()
|
||||
|
||||
const emit = defineEmits()
|
||||
const showForm = ref(false)
|
||||
const formRenderView = ref()
|
||||
const rules = ref({})
|
||||
|
||||
const props = defineProps({
|
||||
formItems: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: () => {
|
||||
return "R"
|
||||
}
|
||||
},
|
||||
isPreview: {
|
||||
type: Boolean,
|
||||
default: () => {
|
||||
return false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
// const _value = ref(props.value)
|
||||
watch(() => props.value, (value) => {
|
||||
_value.value = value
|
||||
})
|
||||
watch(() => props.mode, (value) => {
|
||||
props.mode = value
|
||||
})
|
||||
|
||||
const validate = (call) => {
|
||||
let success = true
|
||||
formRenderView.value.validate(valid => {
|
||||
success = valid
|
||||
if (valid) {
|
||||
//校验成功再校验内部
|
||||
for (let i = 0; i < props.formItems.length; i++) {
|
||||
if (props.formItems[i].name === 'TableList') {
|
||||
// let formRef = currentInstance.ctx.$refs[`sub-item_${this.formItems[i].id}`]
|
||||
// if (formRef && Array.isArray(formRef) && formRef.length > 0) {
|
||||
// formRef[0].validate(subValid => {
|
||||
// success = subValid
|
||||
// })
|
||||
// if (!success) {
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
call(success)
|
||||
});
|
||||
}
|
||||
|
||||
const loadFormConfig = (formItems) => {
|
||||
formItems.forEach(item => {
|
||||
if (item.name === 'SpanLayout') {
|
||||
loadFormConfig(item.props.items)
|
||||
} else {
|
||||
_value.value[item.id] = props.value[item.id]
|
||||
// this.$set(_value, item.id, props.value[item.id])
|
||||
if (props.isPreview) {
|
||||
item['perm'] = props.mode
|
||||
}
|
||||
if (item.perm === 'E') {
|
||||
if (item.props.required) {
|
||||
rules.value[item.id] = [{
|
||||
type: item.valueType === 'Array' ? 'array' : undefined,
|
||||
required: true,
|
||||
message: `请填写${item.title}`, trigger: 'blur'
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
loadFormConfig(props.formItems)
|
||||
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
179
src/views/workflow/form/components/AmountInput.vue
Normal file
179
src/views/workflow/form/components/AmountInput.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-input size="medium" disabled :placeholder="placeholder"/>
|
||||
<div style="margin-top: 15px" v-show="showChinese">
|
||||
<span>大写:</span>
|
||||
<span class="chinese">{{chinese}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-input-number style="width: 100%;" :min="0" controls-position="right" :precision="precision" size="medium" clearable v-model="_value" :placeholder="placeholder"/>
|
||||
<div v-show="showChinese">
|
||||
<span>大写:</span>
|
||||
<span class="chinese">{{chinese}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<div style="display: flex;">
|
||||
<span>{{_value}}</span>
|
||||
<span v-show="showChinese" style="margin-left: 30px;">
|
||||
<span>大写:</span>
|
||||
<span class="chinese">{{chinese}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入金额'
|
||||
},
|
||||
showChinese: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
precision: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
// emit('update:value', value)
|
||||
})
|
||||
|
||||
const chinese = computed(()=>{
|
||||
return convertCurrency(_value.value)
|
||||
})
|
||||
|
||||
const convertCurrency = (money) => {
|
||||
//汉字的数字
|
||||
const cnNums = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
|
||||
//基本单位
|
||||
const cnIntRadice = ['', '拾', '佰', '仟'];
|
||||
//对应整数部分扩展单位
|
||||
const cnIntUnits = ['', '万', '亿', '兆'];
|
||||
//对应小数部分单位
|
||||
const cnDecUnits = ['角', '分', '毫', '厘'];
|
||||
//整数金额时后面跟的字符
|
||||
const cnInteger = '整';
|
||||
//整型完以后的单位
|
||||
const cnIntLast = '元';
|
||||
//最大处理的数字
|
||||
let maxNum = 999999999999999.9999;
|
||||
//金额整数部分
|
||||
let integerNum;
|
||||
//金额小数部分
|
||||
let decimalNum;
|
||||
//输出的中文金额字符串
|
||||
let chineseStr = '';
|
||||
//分离金额后用的数组,预定义
|
||||
let parts;
|
||||
if (money === '') {
|
||||
return '';
|
||||
}
|
||||
money = parseFloat(money);
|
||||
if (money >= maxNum) {
|
||||
//超出最大处理数字
|
||||
return '';
|
||||
}
|
||||
if (money === 0) {
|
||||
chineseStr = cnNums[0] + cnIntLast + cnInteger;
|
||||
return chineseStr;
|
||||
}
|
||||
//转换为字符串
|
||||
money = money.toString();
|
||||
if (money.indexOf('.') === -1) {
|
||||
integerNum = money;
|
||||
decimalNum = '';
|
||||
} else {
|
||||
parts = money.split('.');
|
||||
integerNum = parts[0];
|
||||
decimalNum = parts[1].substr(0, 4);
|
||||
}
|
||||
//获取整型部分转换
|
||||
if (parseInt(integerNum, 10) > 0) {
|
||||
var zeroCount = 0;
|
||||
var IntLen = integerNum.length;
|
||||
for (let i = 0; i < IntLen; i++) {
|
||||
let n = integerNum.substr(i, 1);
|
||||
let p = IntLen - i - 1;
|
||||
let q = p / 4;
|
||||
let m = p % 4;
|
||||
if (n == '0') {
|
||||
zeroCount++;
|
||||
} else {
|
||||
if (zeroCount > 0) {
|
||||
chineseStr += cnNums[0];
|
||||
}
|
||||
//归零
|
||||
zeroCount = 0;
|
||||
chineseStr += cnNums[parseInt(n)] + cnIntRadice[m];
|
||||
}
|
||||
if (m == 0 && zeroCount < 4) {
|
||||
chineseStr += cnIntUnits[q];
|
||||
}
|
||||
}
|
||||
chineseStr += cnIntLast;
|
||||
}
|
||||
//小数部分
|
||||
if (decimalNum !== '') {
|
||||
let decLen = decimalNum.length;
|
||||
for (let i = 0; i < decLen; i++) {
|
||||
let n = decimalNum.substr(i, 1);
|
||||
if (n !== '0') {
|
||||
chineseStr += cnNums[Number(n)] + cnDecUnits[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chineseStr === '') {
|
||||
chineseStr += cnNums[0] + cnIntLast + cnInteger;
|
||||
} else if (decimalNum === '') {
|
||||
chineseStr += cnInteger;
|
||||
}
|
||||
return chineseStr;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chinese{
|
||||
color: #afadad;
|
||||
font-size: smaller;
|
||||
}
|
||||
.el-input__inner{
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
176
src/views/workflow/form/components/AmountInputback.vue
Normal file
176
src/views/workflow/form/components/AmountInputback.vue
Normal file
@@ -0,0 +1,176 @@
|
||||
<template>
|
||||
{{value}}-----------------
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-input size="medium" disabled :placeholder="placeholder"/>
|
||||
<div style="margin-top: 15px" v-show="showChinese">
|
||||
<span>大写:</span>
|
||||
<span class="chinese">{{chinese}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-input-number style="width: 100%;" :min="0" controls-position="right" :precision="precision" size="medium" clearable v-model="_value" :placeholder="placeholder"/>
|
||||
<div v-show="showChinese">
|
||||
<span>大写:</span>
|
||||
<span class="chinese">{{chinese}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<div style="display: flex;">
|
||||
<span>{{_value}}</span>
|
||||
<span v-show="showChinese" style="margin-left: 30px;">
|
||||
<span>大写:</span>
|
||||
<span class="chinese">{{chinese}}</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入金额'
|
||||
},
|
||||
showChinese: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
precision: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
console.log(val,"组件")
|
||||
emit("update:value", val);
|
||||
}
|
||||
})
|
||||
|
||||
const chinese = computed(()=>{
|
||||
return convertCurrency(props.value)
|
||||
})
|
||||
|
||||
const convertCurrency = (money) => {
|
||||
console.log("zhuanhuan ",money)
|
||||
//汉字的数字
|
||||
const cnNums = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
|
||||
//基本单位
|
||||
const cnIntRadice = ['', '拾', '佰', '仟'];
|
||||
//对应整数部分扩展单位
|
||||
const cnIntUnits = ['', '万', '亿', '兆'];
|
||||
//对应小数部分单位
|
||||
const cnDecUnits = ['角', '分', '毫', '厘'];
|
||||
//整数金额时后面跟的字符
|
||||
const cnInteger = '整';
|
||||
//整型完以后的单位
|
||||
const cnIntLast = '元';
|
||||
//最大处理的数字
|
||||
let maxNum = 999999999999999.9999;
|
||||
//金额整数部分
|
||||
let integerNum;
|
||||
//金额小数部分
|
||||
let decimalNum;
|
||||
//输出的中文金额字符串
|
||||
let chineseStr = '';
|
||||
//分离金额后用的数组,预定义
|
||||
let parts;
|
||||
if (money === '') {
|
||||
return '';
|
||||
}
|
||||
money = parseFloat(money);
|
||||
if (money >= maxNum) {
|
||||
//超出最大处理数字
|
||||
return '';
|
||||
}
|
||||
if (money === 0) {
|
||||
chineseStr = cnNums[0] + cnIntLast + cnInteger;
|
||||
return chineseStr;
|
||||
}
|
||||
//转换为字符串
|
||||
money = money.toString();
|
||||
if (money.indexOf('.') === -1) {
|
||||
integerNum = money;
|
||||
decimalNum = '';
|
||||
} else {
|
||||
parts = money.split('.');
|
||||
integerNum = parts[0];
|
||||
decimalNum = parts[1].substr(0, 4);
|
||||
}
|
||||
//获取整型部分转换
|
||||
if (parseInt(integerNum, 10) > 0) {
|
||||
var zeroCount = 0;
|
||||
var IntLen = integerNum.length;
|
||||
for (let i = 0; i < IntLen; i++) {
|
||||
let n = integerNum.substr(i, 1);
|
||||
let p = IntLen - i - 1;
|
||||
let q = p / 4;
|
||||
let m = p % 4;
|
||||
if (n == '0') {
|
||||
zeroCount++;
|
||||
} else {
|
||||
if (zeroCount > 0) {
|
||||
chineseStr += cnNums[0];
|
||||
}
|
||||
//归零
|
||||
zeroCount = 0;
|
||||
chineseStr += cnNums[parseInt(n)] + cnIntRadice[m];
|
||||
}
|
||||
if (m == 0 && zeroCount < 4) {
|
||||
chineseStr += cnIntUnits[q];
|
||||
}
|
||||
}
|
||||
chineseStr += cnIntLast;
|
||||
}
|
||||
//小数部分
|
||||
if (decimalNum !== '') {
|
||||
let decLen = decimalNum.length;
|
||||
for (let i = 0; i < decLen; i++) {
|
||||
let n = decimalNum.substr(i, 1);
|
||||
if (n !== '0') {
|
||||
chineseStr += cnNums[Number(n)] + cnDecUnits[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chineseStr === '') {
|
||||
chineseStr += cnNums[0] + cnIntLast + cnInteger;
|
||||
} else if (decimalNum === '') {
|
||||
chineseStr += cnInteger;
|
||||
}
|
||||
return chineseStr;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chinese{
|
||||
color: #afadad;
|
||||
font-size: smaller;
|
||||
}
|
||||
.el-input__inner{
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
69
src/views/workflow/form/components/DateTime.vue
Normal file
69
src/views/workflow/form/components/DateTime.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'" >
|
||||
<el-date-picker size="medium" disabled :type="type" :placeholder="placeholder"></el-date-picker>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-date-picker v-model="_value" :value-format="format" size="medium" clearable :type="type" :placeholder="placeholder"></el-date-picker>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
{{_value}}
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
format:{
|
||||
type: String,
|
||||
default: 'YYYY-MM-DD HH:mm'
|
||||
},
|
||||
placeholder:{
|
||||
type: String,
|
||||
default: '请选择日期时间'
|
||||
}
|
||||
})
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
})
|
||||
|
||||
const type = computed(()=>{
|
||||
switch (props.format){
|
||||
case 'YYYY': return 'year';
|
||||
case 'YYYY-MM': return 'month';
|
||||
case 'YYYY-MM-DD': return 'date';
|
||||
case 'YYYY-MM-DD HH:mm': return 'datetime';
|
||||
default: return 'datetime';
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
136
src/views/workflow/form/components/DateTimeRange.vue
Normal file
136
src/views/workflow/form/components/DateTimeRange.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-date-picker size="medium" v-model="_value" disabled :type="type" :start-placeholder="placeholder[0]"
|
||||
:end-placeholder="placeholder[1]"/>
|
||||
<div v-if="showLength" class="length">
|
||||
<span>时长:</span>
|
||||
<span>{{ timeLength }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-date-picker v-model="_value" size="medium" clearable :value-format="format" :type="type"
|
||||
:start-placeholder="placeholder[0]" :end-placeholder="placeholder[1]"/>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<!-- <div v-if="type==='daterange'">-->
|
||||
<!-- <span style="float:left;">{{_value[0]}}</span>-->
|
||||
<!-- <span style="padding:0 9px">至</span>-->
|
||||
<!-- <span>{{ _value[1] }}</span>-->
|
||||
<!-- </div>-->
|
||||
<div>
|
||||
<span style="float:left;">{{ _value[0] }}</span>
|
||||
<span style="padding:0 9px">至</span>
|
||||
<span>{{ _value[1] }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="showLength" class="length">
|
||||
<span>时长:</span>
|
||||
<span>{{timeLengthFiled}}</span>
|
||||
<!-- <span>{{type==='daterange'? parseInt(timeLength)+1: timeLength }}</span>-->
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
// import {timeLength} from '../utils/date'
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: "E"
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: "YYYY-MM-DD HH:mm"
|
||||
},
|
||||
placeholder: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return ["开始时间", "结束时间"];
|
||||
}
|
||||
},
|
||||
showLength: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
length: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const type = computed(()=>{
|
||||
switch (props.format) {
|
||||
case "YYYY-MM-DD":
|
||||
return "daterange";
|
||||
case "YYYY-MM-DD HH:mm":
|
||||
return "datetimerange";
|
||||
default:
|
||||
return "daterange";
|
||||
}
|
||||
})
|
||||
|
||||
const timeLengthFiled = computed(()=>{
|
||||
//求时长算法
|
||||
if (Array.isArray(this.value)) {
|
||||
// let start = moment(this.value[0]).format(this.format.replaceAll("dd", "DD"));
|
||||
// let end = moment(this.value[1]).format(this.format.replaceAll("dd", "DD"));
|
||||
if (this.value[0] === this.value[1]) {
|
||||
return "0 (时长为0,请确认)";
|
||||
}
|
||||
return "先选择时间哦";
|
||||
// return timeLength(this.value[0],this.value[1])
|
||||
} else {
|
||||
return "先选择时间哦";
|
||||
}
|
||||
})
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.length {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.length:nth-child(2) {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.el-date-editor--datetimerange.el-input__inner {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
99
src/views/workflow/form/components/DeptPicker.vue
Normal file
99
src/views/workflow/form/components/DeptPicker.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div style="max-width: 350px">
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-button disabled icon="SetUp" type="primary" size="mini" round> 选择部门</el-button>
|
||||
<span class="placeholder"> {{ placeholder }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-button icon="SetUp" type="primary" size="mini" round @click="selectDept"> 选择部门</el-button>
|
||||
<org-picker type="dept" :multiple="multiple" ref="deptPicker" :v-model="deptList" @ok="selected"/>
|
||||
<span class="placeholder"> {{ placeholder }}</span>
|
||||
<div style="margin-top: 5px">
|
||||
<el-tag size="mini" style="margin: 5px" closable v-for="(dept, i) in deptList" :key="i" @close="delDept(i)">
|
||||
{{ dept.label }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<el-tag size="mini" style="margin: 5px" v-for="(dept, i) in deptList" :key="i" >
|
||||
{{ dept.label }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OrgPicker from "../../process/common/DeptPicker.vue";
|
||||
import {computed, defineProps} from "vue";
|
||||
|
||||
const showOrgSelect = ref(false)
|
||||
const deptPicker = ref()
|
||||
const deptList = ref([])
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择部门'
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
})
|
||||
const emit = defineEmits(["input"])
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emit("input", val);
|
||||
}
|
||||
})
|
||||
|
||||
const selectDept = () => {
|
||||
//弹出部门选择器,选择部门
|
||||
deptPicker.value.showDeptPicker()
|
||||
}
|
||||
const selected = (select) => {
|
||||
let userInfoList = []
|
||||
for (let val of select) {
|
||||
let userInfo = {
|
||||
value: val.value,
|
||||
label: val.label
|
||||
}
|
||||
userInfoList.push(userInfo)
|
||||
}
|
||||
deptList.value = userInfoList
|
||||
}
|
||||
const delDept = (i) => {
|
||||
deptList.value.splice(i, 1)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.placeholder {
|
||||
margin-left: 10px;
|
||||
color: #adabab;
|
||||
font-size: smaller;
|
||||
}
|
||||
</style>
|
||||
29
src/views/workflow/form/components/Description.vue
Normal file
29
src/views/workflow/form/components/Description.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div :style="{'color': color}">
|
||||
<el-icon><Warning/></el-icon>
|
||||
<span> {{ placeholder }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps} from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#868686'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '只是一段说明文字'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
104
src/views/workflow/form/components/FileUpload.vue
Normal file
104
src/views/workflow/form/components/FileUpload.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-button size="small" icon="Link" round>选择文件</el-button>
|
||||
<ellipsis :row="1" :content="placeholder + sizeTip" hoverTip slot="tip"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-upload :file-list="_value" action="#" :limit="maxSize" with-credentials :multiple="maxSize > 0"
|
||||
:data="uploadParams"
|
||||
:auto-upload="false" :before-upload="beforeUpload">
|
||||
<el-button size="small" icon="Link" round>选择文件</el-button>
|
||||
<ellipsis :row="1" :content="placeholder + sizeTip" hoverTip slot="tip"/>
|
||||
</el-upload>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
{{ _value }}
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps,computed,ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
import Ellipsis from '../../process/common/Ellipsis.vue'
|
||||
const disabled = ref(false)
|
||||
const uploadParams = ref({})
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择附件'
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
maxNumber: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
fileTypes: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(["input"])
|
||||
const sizeTip = computed(() => {
|
||||
if (props.fileTypes.length > 0) {
|
||||
return ` | 只允许上传[${String(props.fileTypes).replaceAll(",", "、")}]格式的文件,且单个附件不超过${props.maxSize}MB`
|
||||
}
|
||||
return props.maxSize > 0 ? ` | 单个附件不超过${props.maxSize}MB` : ''
|
||||
|
||||
})
|
||||
const _value = computed( {
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emit("input", val);
|
||||
}
|
||||
})
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
const alows = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
|
||||
if (alows.indexOf(file.type) === -1) {
|
||||
ElMessage.warning("存在不支持的图片格式")
|
||||
} else if (this.maxSize > 0 && file.size / 1024 / 1024 > this.maxSize) {
|
||||
ElMessage.warning(`单张图片最大不超过 ${this.maxSize}MB`)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
const handleRemove = (file, fileList) => {
|
||||
console.log(file, fileList);
|
||||
}
|
||||
const handlePictureCardPreview = (file) => {
|
||||
console.log(file)
|
||||
}
|
||||
const handleDownload = (file) => {
|
||||
console.log(file)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
207
src/views/workflow/form/components/ImageUpload.vue
Normal file
207
src/views/workflow/form/components/ImageUpload.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<div class="design">
|
||||
<el-icon><Plus /></el-icon>
|
||||
</div>
|
||||
<p>{{ placeholder }} {{ sizeTip }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-upload :file-list="_value"
|
||||
:limit="maxSize" with-credentials
|
||||
:multiple="maxSize > 0"
|
||||
:data="uploadParams"
|
||||
list-type="picture-card"
|
||||
:action="uploadFileUrl"
|
||||
:headers="headers"
|
||||
:auto-upload="true"
|
||||
:on-success="handleUploadSuccess"
|
||||
:before-upload="beforeUpload">
|
||||
<i slot="default" class="el-icon-plus"></i>
|
||||
<div slot="file" slot-scope="{file}">
|
||||
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="">
|
||||
<label class="el-upload-list__item-status-label">
|
||||
<i class="el-icon-upload-success el-icon-check"></i>
|
||||
</label>
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
<i class="el-icon-zoom-in"></i>
|
||||
</span>
|
||||
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleDownload(file)">
|
||||
<i class="el-icon-download"></i>
|
||||
</span>
|
||||
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
|
||||
<i class="el-icon-delete"></i>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div slot="tip">{{ placeholder }} {{ sizeTip }}</div>
|
||||
</el-upload>
|
||||
<el-dialog :visible.sync="dialogVisible" center append-to-body>
|
||||
<div>
|
||||
<img width="100%" :src="dialogImageUrl" alt="" style="z-index: 3435">
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<div v-for="(item , index) in _value" :key="index">
|
||||
<el-image
|
||||
style="width: 100px; height: 100px"
|
||||
:src="item.url"
|
||||
:preview-src-list="[item.url]">
|
||||
</el-image>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {deleteFile} from '@/api/workflow/process-file.js'
|
||||
import {defineProps, defineEmits} from 'vue'
|
||||
import { getToken } from '@/utils/auth'
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
|
||||
const baseURL = import.meta.env.VITE_BASE_URL
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择图片'
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
maxNumber: {
|
||||
type: Number,
|
||||
default: 10
|
||||
},
|
||||
enableZip: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const fileList = ref([])
|
||||
const disabled = ref(false)
|
||||
const uploadFileUrl = ref(baseURL + "/workflow/process/file")
|
||||
const headers = reactive({
|
||||
authorization: getToken()
|
||||
})
|
||||
|
||||
const uploadParams = ref([])
|
||||
const dialogImageUrl = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const imageUrl = ref([])
|
||||
|
||||
|
||||
const sizeTip = computed(() => {
|
||||
return props.maxSize > 0 ? `| 每张图不超过${props.maxSize}MB` : ''
|
||||
})
|
||||
|
||||
|
||||
const init = () => {
|
||||
fileList.value = _value
|
||||
}
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
})
|
||||
|
||||
const beforeUpload = (file) => {
|
||||
const alows = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
|
||||
if (alows.indexOf(file.type) === -1) {
|
||||
ElMessage.warning("存在不支持的图片格式")
|
||||
} else if (props.maxSize > 0 && file.size / 1024 / 1024 > props.maxSize) {
|
||||
ElMessage.warning(`单张图片最大不超过 ${props.maxSize}MB`)
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleUploadSuccess = (res, file) => {
|
||||
if (res.code !== 1000) {
|
||||
ElMessage.error("上传失败")
|
||||
}
|
||||
let data = res.data
|
||||
fileList.value.push(data)
|
||||
emit("input", fileList)
|
||||
console.log(res, file, fileList)
|
||||
}
|
||||
|
||||
const handleRemove = (file) => {
|
||||
deleteFile(file.id).then(res => {
|
||||
if (res.code === 1000) {
|
||||
ElMessage.success("删除成功")
|
||||
fileList.value.splice(fileList.value().findIndex((item) => item.id === file.id), 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handlePictureCardPreview = (file) => {
|
||||
dialogVisible.value = true;
|
||||
dialogImageUrl.value = file.url;
|
||||
}
|
||||
|
||||
const handleDownload = (file) => {
|
||||
console.log(file);
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.design {
|
||||
i {
|
||||
padding: 10px;
|
||||
font-size: xx-large;
|
||||
background: white;
|
||||
border: 1px dashed #8c8c8c;
|
||||
}
|
||||
}
|
||||
|
||||
.el-upload--picture-card {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
line-height: 87px;
|
||||
}
|
||||
|
||||
.el-upload-list__item {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
.el-upload-list__item-actions {
|
||||
& > span + span {
|
||||
margin: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
80
src/views/workflow/form/components/MultipleSelect.vue
Normal file
80
src/views/workflow/form/components/MultipleSelect.vue
Normal file
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-select class="max-fill" v-if="!expanding" size="medium" multiple v-model="_value" disabled :placeholder="placeholder" filterable/>
|
||||
<el-checkbox-group v-else v-model="_value">
|
||||
<el-checkbox disabled v-for="(op, index) in options" :key="index" :label="op">{{op}}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-select class="max-fill" v-if="!expanding" v-model="_value" multiple size="medium" clearable :placeholder="placeholder" filterable>
|
||||
<el-option v-for="(op, index) in options" :key="index" :value="op" :label="op"></el-option>
|
||||
</el-select>
|
||||
<el-checkbox-group v-else v-model="_value">
|
||||
<el-checkbox v-for="(op, index) in options" :key="index" :label="op">{{op}}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<div v-for="(item,index) in _value" :key="index" >
|
||||
<span style="float:left;padding-right: 9px">{{item}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
placeholder:{
|
||||
type: String,
|
||||
default: '请选择选项'
|
||||
},
|
||||
value:{
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
expanding:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options:{
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
61
src/views/workflow/form/components/NumberInput.vue
Normal file
61
src/views/workflow/form/components/NumberInput.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-input-number size="medium" disabled :placeholder="placeholder"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-input-number style="width: 100%;" controls-position="right" v-model="_value" size="medium" clearable :placeholder="placeholder"/>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<span>{{_value}}</span>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value:{
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder:{
|
||||
type: String,
|
||||
default: '请输入数值'
|
||||
}
|
||||
})
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-input__inner{
|
||||
text-align: left;
|
||||
}
|
||||
</style>
|
||||
90
src/views/workflow/form/components/RatePicker.vue
Normal file
90
src/views/workflow/form/components/RatePicker.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-rate
|
||||
disabled
|
||||
:show-score="showScore"
|
||||
:allow-half="enableHalf"
|
||||
:text-color="color"
|
||||
text-color="#ff9900"
|
||||
score-template="{value}">
|
||||
</el-rate>
|
||||
<p>{{ placeholder }}</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-rate
|
||||
v-model="star"
|
||||
:show-score="showScore"
|
||||
:allow-half="enableHalf"
|
||||
:text-color="color"
|
||||
score-template="{value}">
|
||||
</el-rate>
|
||||
<p>{{ placeholder }}</p>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<el-rate
|
||||
v-model="star"
|
||||
disabled
|
||||
:show-score="showScore"
|
||||
:allow-half="enableHalf"
|
||||
:text-color="color"
|
||||
score-template="{value}">
|
||||
</el-rate>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, defineEmits, defineProps} from "vue";
|
||||
const emit = defineEmits(["input"])
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#f0a732'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请打分!'
|
||||
},
|
||||
enableHalf: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
showScore: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
})
|
||||
const star=ref(null)
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emit("input", val);
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
p {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
</style>
|
||||
75
src/views/workflow/form/components/SelectInput.vue
Normal file
75
src/views/workflow/form/components/SelectInput.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-select class="max-fill" v-if="!expanding" size="medium" v-model="_value" disabled :placeholder="placeholder" filterable/>
|
||||
<el-radio-group v-model="_value" v-else>
|
||||
<el-radio disabled v-for="(op, index) in options" :key="index" :label="op">{{op}}</el-radio>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-select class="max-fill" v-if="!expanding" v-model="_value" size="medium" clearable :placeholder="placeholder" filterable>
|
||||
<el-option v-for="(op, index) in options" :key="index" :value="op" :label="op"></el-option>
|
||||
</el-select>
|
||||
<el-radio-group v-model="_value" v-else>
|
||||
<el-radio v-for="(op, index) in options" :key="index" :label="op">{{op}}</el-radio>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<span>{{ _value }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}, value:{
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder:{
|
||||
type: String,
|
||||
default: '请选择选项'
|
||||
},
|
||||
expanding:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
options:{
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
118
src/views/workflow/form/components/SignPanel.vue
Normal file
118
src/views/workflow/form/components/SignPanel.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-icon ><EditPen/></el-icon>
|
||||
请签名
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<div v-if="!_value">
|
||||
<el-button icon="EditPen" @click="doSign">请签字</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div>
|
||||
<el-image fit="contain" class="sign-border-preview" style="width: 200px; height: 100px" @click="doSign"
|
||||
:src="_value"/>
|
||||
</div>
|
||||
<div>
|
||||
点击签名重签
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<div v-if="_value">
|
||||
<el-image fit="contain" style="width: 200px; height: 100px" :src="_value"/>
|
||||
</div>
|
||||
<div>
|
||||
请签名
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<el-dialog title="请使用鼠标签字" width="800px" :visible.sync="sign.open" @close="signClose" append-to-body>
|
||||
<div class="sign-border">
|
||||
<!-- <vue-esign v-if="sign.open" ref="sign" :width="800" :height="300" :isCrop="isCrop"-->
|
||||
<!-- :lineWidth="sign.lineWidth" :lineColor="sign.lineColor"-->
|
||||
<!-- :bgColor.sync="sign.bgColor"/>-->
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<el-button size="mini" @click="signClose">取消</el-button>
|
||||
<el-button size="mini" type="primary" @click="handleGenerate">确认</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// import vueEsign from 'vue-esign'
|
||||
|
||||
import {computed, defineProps,ref} from "vue";
|
||||
// import EditTable from "../../../rapid/gen/editTable";
|
||||
|
||||
const emit = defineEmits(["input"])
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
isCrop: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
lineColor: {
|
||||
type: String,
|
||||
default: '#000000'
|
||||
}
|
||||
})
|
||||
const sign = ref({
|
||||
open: false,
|
||||
lineWidth: 6,
|
||||
bgColor: ''
|
||||
})
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emit("input", val);
|
||||
}
|
||||
})
|
||||
const signClose = () => {
|
||||
sign.value.open = false
|
||||
sign.value.reset()
|
||||
}
|
||||
const doSign = () => {
|
||||
sign.value.open = true
|
||||
}
|
||||
const handleGenerate = () => {
|
||||
sign.value.generate().then(res => {
|
||||
_value.value = res
|
||||
sign.value.open = false
|
||||
}).catch(err => {
|
||||
alert(err) // 画布没有签字时会执行这里 'Not Signned'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.sign-border {
|
||||
border: 1px dashed #2b2b2b;
|
||||
}
|
||||
|
||||
.sign-border-preview {
|
||||
border: 1px solid #ffffff;
|
||||
}
|
||||
|
||||
.sign-border-preview:hover {
|
||||
border: 1px dashed #1989fa;
|
||||
}
|
||||
</style>
|
||||
238
src/views/workflow/form/components/SpanLayout.vue
Normal file
238
src/views/workflow/form/components/SpanLayout.vue
Normal file
@@ -0,0 +1,238 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<draggable class="l-drag-from" :list="_items" group="form"
|
||||
:options="{animation: 300, chosenClass:'choose', sort:true}"
|
||||
@start="spanLayoutStart" @end="drag = false">
|
||||
<template #item="{ element, index }">
|
||||
<div class="l-form-item" @click.stop="selectItem(element)"
|
||||
:style="getSelectedClass(element)">
|
||||
<div class="l-form-header">
|
||||
<p><span v-if="element.props.required">*</span>{{ element.title }}</p>
|
||||
<div class="l-option">
|
||||
<!--<i class="el-icon-copy-document" @click="copy"></i>-->
|
||||
<i class="el-icon-close" @click="delItem(index)"></i>
|
||||
</div>
|
||||
<form-design-render :config="element"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div style="color: #c0bebe;text-align: center; width: 90%; padding: 5px;">☝ 拖拽控件到布局容器内部</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-row :gutter="20" v-for="(rows, rsi) in __items" :key="rsi + '_rows'">
|
||||
<el-col :span="24 / rows.length" v-for="(item, ri) in rows" :key="ri + '_row'">
|
||||
<el-form-item v-if="item.name !== 'SpanLayout' && item.name !== 'Description'" :prop="item.id"
|
||||
:label="item.title" :key="item.name + ri">
|
||||
<form-design-render v-model:value="_value[item.id]" :perm="item.perm" :mode="mode" :config="item"/>
|
||||
</el-form-item>
|
||||
<form-design-render v-else v-model:value="_value" :perm="item.perm" :mode="mode" :config="item"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import draggable from "vuedraggable";
|
||||
import FormDesignRender from '../FormDesignRender.vue'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
|
||||
const processStore = useProcessStore()
|
||||
import {defineProps, defineEmits, computed} from 'vue'
|
||||
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
default: null
|
||||
},
|
||||
items: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const select = ref();
|
||||
const drag = ref(false);
|
||||
const formConfig = ref({
|
||||
//数据字段
|
||||
data: {},
|
||||
//校验规则
|
||||
rules: {}
|
||||
});
|
||||
const form = ref({
|
||||
processDefinitionKey: '',
|
||||
deploymentName: "",
|
||||
logo: {},
|
||||
formItems: [],
|
||||
process: {},
|
||||
remark: ""
|
||||
});
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(() => props.value, (value) => {
|
||||
_value.value = value
|
||||
})
|
||||
|
||||
const _items = computed({
|
||||
get() {
|
||||
return props.items;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(() => props.items, (value) => {
|
||||
_items.value = value
|
||||
})
|
||||
|
||||
const __items = computed(() => {
|
||||
let result = []
|
||||
for (let i = 0; i < props.items.length; i++) {
|
||||
if (i > 0 && i % 2 > 0) {
|
||||
result.push([props.items[i - 1], props.items[i]])
|
||||
}
|
||||
}
|
||||
if (result.length * 2 < props.items.length) {
|
||||
result.push([props.items[props.items.length - 1]])
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
|
||||
const selectFormItem = computed({
|
||||
get() {
|
||||
return processStore.getSelectedFormItem()
|
||||
},
|
||||
set(val) {
|
||||
processStore.setSelectedFormItem(val)
|
||||
},
|
||||
})
|
||||
|
||||
const spanLayoutStart = () => {
|
||||
drag.value = true;
|
||||
selectFormItem.value = null
|
||||
}
|
||||
const selectItem = (cp) => {
|
||||
selectFormItem.value = cp
|
||||
}
|
||||
const getSelectedClass = (cp) => {
|
||||
return processStore.getSelectedFormItem() && processStore.getSelectedFormItem().id === cp.id ?
|
||||
'border-left: 4px solid #f56c6c' : ''
|
||||
}
|
||||
|
||||
const delItem = (index) => {
|
||||
ElMessageBox.confirm('删除组件将会连带删除包含该组件的条件以及相关设置,是否继续?', '提示', {
|
||||
confirmButtonText: '确 定',
|
||||
cancelButtonText: '取 消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
if (_items.value[index].name === 'SpanLayout') {
|
||||
//删除的是分栏则遍历删除分栏内所有子组件
|
||||
_items.value[index].props.items.forEach(item => {
|
||||
removeFormItemAbout(item)
|
||||
})
|
||||
_items.value[index].props.items.length = 0
|
||||
} else {
|
||||
removeFormItemAbout(_items[index])
|
||||
}
|
||||
_items.value.splice(index, 1)
|
||||
})
|
||||
}
|
||||
const removeFormItemAbout = async (item) => {
|
||||
processStore.nodeMap.forEach(node => {
|
||||
//搜寻条件,进行移除
|
||||
if (node.type === 'CONDITION') {
|
||||
node.props.groups.forEach(group => {
|
||||
let i = group.cids.remove(item.id)
|
||||
if (i > -1) {
|
||||
//从子条件移除
|
||||
group.conditions.splice(i, 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
//搜寻权限,进行移除
|
||||
if (node.type === 'ROOT' || node.type === 'APPROVAL' || node.type === 'CC') {
|
||||
node.props.formPerms.removeByKey('id', item.id)
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
|
||||
.choose {
|
||||
border: 1px dashed #1890FF !important;
|
||||
}
|
||||
|
||||
.l-drag-from {
|
||||
min-height: 50px;
|
||||
background-color: rgb(245, 246, 246);
|
||||
|
||||
.l-form-item, li {
|
||||
cursor: grab;
|
||||
background: #ffffff;
|
||||
padding: 10px;
|
||||
border: 1px solid #ebecee;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.l-form-header {
|
||||
font-size: small;
|
||||
color: #818181;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
margin: 0 0 10px 0;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 3px;
|
||||
color: rgb(217, 0, 19);
|
||||
}
|
||||
}
|
||||
|
||||
.l-option {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
|
||||
i {
|
||||
font-size: large;
|
||||
cursor: pointer;
|
||||
color: #8c8c8c;
|
||||
padding: 5px;
|
||||
|
||||
&:hover {
|
||||
color: #1890FF;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
373
src/views/workflow/form/components/TableList.vue
Normal file
373
src/views/workflow/form/components/TableList.vue
Normal file
@@ -0,0 +1,373 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<draggable class="l-drag-from" :list="_columns" group="form"
|
||||
:options="{animation: 300, chosenClass:'choose', sort:true}"
|
||||
@start="drag = true; selectFormItem = null" @end="drag = false">
|
||||
<template #item="{ element, index }">
|
||||
<div class="l-form-item" @click.stop="selectItem(element)"
|
||||
:style="getSelectedClass(element)">
|
||||
<div class="l-form-header">
|
||||
<p><span v-if="element.props.required">*</span>{{ element.title }}</p>
|
||||
<div class="l-option">
|
||||
<el-icon size="25px" @click="delItem(index)">
|
||||
<Close/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<form-design-render :config="element"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<div style="color: #c0bebe;text-align: center; width: 90%; padding: 5px;">☝ 拖拽控件到表格内部</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<div v-if="rowLayout">
|
||||
<el-table size="medium" :header-cell-style="{background:'#f5f7fa', padding:'3px 0'}" :border="showBorder"
|
||||
:data="_value" style="width: 763px" v-tabh>
|
||||
<el-table-column fixed type="index" label="序号" width="55"></el-table-column>
|
||||
<el-table-column :min-width="getMinWidth(column)" v-for="(column, index) in _columns" :key="index"
|
||||
:prop="column.id" :label="column.title" >
|
||||
<template #default="scope">
|
||||
<form-design-render :class="{'valid-error': showError(column, _value[scope.$index][column.id])}"
|
||||
v-model:value="_value[scope.$index][column.id]" :mode="mode" :config="column"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" min-width="90" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button size="mini" type="primary" @click="copyData(scope.$index, scope.row)" link>复制</el-button>
|
||||
<el-button size="mini" type="primary" @click="delRow(scope.$index, scope.row)" link>删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-button size="small" icon="Plus" @click="addRow">{{ placeholder }}</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form :rules="rules" :model="row" :ref="`table-form-${i}`" class="table-column" v-for="(row, i) in _value"
|
||||
:key="i">
|
||||
<div class="table-column-action">
|
||||
<span>第 {{ i + 1 }} 项</span>
|
||||
<el-icon size="25px" @click="delRow(i, row)">
|
||||
<Close/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-form-item v-for="(column, index) in _columns" :key="'column_' + index" :prop="column.id"
|
||||
:label="column.title">
|
||||
<form-design-render v-model="row[column.id]" :mode="mode" :config="column"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button size="small" icon="Plus" @click="addRow">{{ placeholder }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<div v-if="rowLayout">
|
||||
<el-table size="medium" :header-cell-style="{background:'#f5f7fa', padding:'3px 0'}" :border="showBorder"
|
||||
:data="_value" style="width: 763px" v-tabh>
|
||||
<el-table-column fixed type="index" label="序号" width="50"></el-table-column>
|
||||
<el-table-column :min-width="getMinWidth(column)" v-for="(column, index) in _columns" :key="index"
|
||||
:prop="column.id" :label="column.title">
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import draggable from "vuedraggable";
|
||||
import {ValueType} from "../ComponentsConfigExport";
|
||||
import FormDesignRender from '../FormDesignRender.vue'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import {computed, defineEmits, defineExpose, defineProps, ref} from "vue";
|
||||
import {ElMessage} from "element-plus";
|
||||
|
||||
const processStore = useProcessStore()
|
||||
const emit = defineEmits(["input"])
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '添加数据'
|
||||
},
|
||||
columns: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
showBorder: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
rowLayout: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
})
|
||||
const select = ref(null)
|
||||
const drag = ref(false)
|
||||
const _value=ref([])
|
||||
|
||||
const init = () => {
|
||||
if (!Array.isArray(props.value)) {
|
||||
_value.value = []
|
||||
}
|
||||
}
|
||||
// const _value = computed({
|
||||
// get() {
|
||||
// return props.value;
|
||||
// },
|
||||
// set(value) {
|
||||
// console.log('成都督导',value)
|
||||
// props.value=value
|
||||
// }
|
||||
// })
|
||||
// watch(() => _value.value, (value) => {
|
||||
// _value.value = value
|
||||
// })
|
||||
// const rules = computed({
|
||||
// const rules = {}
|
||||
// this.columns.forEach(col => {
|
||||
// if (col.props.required) {
|
||||
// rules[col.id] = [{
|
||||
// type: col.valueType === 'Array' ? 'array' : undefined,
|
||||
// required: true,
|
||||
// message: `请填写${col.title}`, trigger: 'blur'
|
||||
// }]
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
const _columns = computed({
|
||||
get() {
|
||||
return props.columns;
|
||||
},
|
||||
set(val) {
|
||||
this.columns = val;
|
||||
}
|
||||
})
|
||||
const selectFormItem = computed({
|
||||
get() {
|
||||
return processStore.selectFormItem
|
||||
},
|
||||
set(val) {
|
||||
processStore.selectFormItem = val
|
||||
},
|
||||
})
|
||||
const getMinWidth = (col) => {
|
||||
switch (col.name) {
|
||||
case 'DateTime':
|
||||
return '250px'
|
||||
case 'DateTimeRange':
|
||||
return '280px'
|
||||
case 'MultipleSelect':
|
||||
return '200px'
|
||||
default:
|
||||
return '150px'
|
||||
}
|
||||
}
|
||||
const showError = (col, val) => {
|
||||
console.log('showError', val)
|
||||
if (col.props.required) {
|
||||
switch (col.valueType) {
|
||||
case ValueType.dept:
|
||||
case ValueType.user:
|
||||
case ValueType.dateRange:
|
||||
case ValueType.array:
|
||||
return !(Array.isArray(val) && val.length > 0)
|
||||
default:
|
||||
return !this.isNotEmpty(val)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
const copyData = (i, row) => {
|
||||
// _value.value.push(this.deepCopy(row))
|
||||
}
|
||||
const delRow = (i) => {
|
||||
_value.value.splice(i, 1)
|
||||
}
|
||||
const addRow = () => {
|
||||
console.log('添加', props.maxSize, _value.value)
|
||||
if (props.maxSize > 0 && _value.value.length >= props.maxSize) {
|
||||
console.log('限制大小')
|
||||
ElMessage.warning(`最多只能添加${props.maxSize}行`)
|
||||
} else {
|
||||
let row = {}
|
||||
_columns.value.map(col => {
|
||||
row[col.id] = undefined
|
||||
})
|
||||
_value.value.push(row)
|
||||
console.log('_value', _value.value)
|
||||
}
|
||||
}
|
||||
const delItem = (id) => {
|
||||
_columns.value.splice(id, 1)
|
||||
}
|
||||
const selectItem = (cp) => {
|
||||
selectFormItem.value = cp
|
||||
}
|
||||
const getSelectedClass = (cp) => {
|
||||
return selectFormItem.value && selectFormItem.value.id === cp.id ? 'border-left: 4px solid #f56c6c' : ''
|
||||
}
|
||||
const validate = (call) => {
|
||||
if (props.rowLayout) {
|
||||
let result = true
|
||||
for (let i = 0; i < props.columns.length; i++) {
|
||||
if (props.columns[i].props.required) {
|
||||
for (let j = 0; j < _value.value.length; j++) {
|
||||
result = !showError(props.columns[i], _value.value[j][props.columns[i].id])
|
||||
if (!result) {
|
||||
call(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
call(result)
|
||||
} else {
|
||||
let success = 0
|
||||
_value.value.forEach((v, i) => {
|
||||
console.log("i", i);
|
||||
// let formRef = this.$refs[`table-form-${i}`]
|
||||
// if (formRef && Array.isArray(formRef) && formRef.length > 0) {
|
||||
// formRef[0].validate(valid => {
|
||||
// if (valid) {
|
||||
// success++;
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
})
|
||||
if (success === _value.value.length) {
|
||||
call(true)
|
||||
} else {
|
||||
call(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
init()
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.valid-error {
|
||||
.el-input__inner {
|
||||
border-color: #F56C6C;
|
||||
}
|
||||
}
|
||||
|
||||
.choose {
|
||||
border: 1px dashed var(--theme-primary) !important;
|
||||
}
|
||||
|
||||
.table-column {
|
||||
padding: 5px;
|
||||
margin-bottom: 10px;
|
||||
border-left: 3px solid #409eff;
|
||||
border-radius: 5px;
|
||||
background: #fafafa;
|
||||
|
||||
.el-form-item {
|
||||
margin-bottom: 0;
|
||||
|
||||
.el-form-item__label {
|
||||
height: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
.table-column-action {
|
||||
float: right;
|
||||
|
||||
span {
|
||||
color: #afafaf;
|
||||
margin-right: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
i {
|
||||
color: #afafaf;
|
||||
padding: 5px;
|
||||
font-size: large;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: #666666;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.l-drag-from {
|
||||
min-height: 50px;
|
||||
background-color: rgb(245, 246, 246);
|
||||
|
||||
.l-form-item, li {
|
||||
cursor: grab;
|
||||
background: #ffffff;
|
||||
padding: 10px;
|
||||
border: 1px solid #ebecee;
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.l-form-header {
|
||||
font-size: small;
|
||||
color: #818181;
|
||||
text-align: left;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
|
||||
p {
|
||||
position: relative;
|
||||
margin: 0 0 10px 0;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
left: -8px;
|
||||
top: 3px;
|
||||
color: rgb(217, 0, 19);
|
||||
}
|
||||
}
|
||||
|
||||
.l-option {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
|
||||
i {
|
||||
font-size: large;
|
||||
cursor: pointer;
|
||||
color: #8c8c8c;
|
||||
padding: 5px;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
57
src/views/workflow/form/components/TextInput.vue
Normal file
57
src/views/workflow/form/components/TextInput.vue
Normal file
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-input size="medium" disabled :placeholder="placeholder"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-input style="width: 100%;" size="medium" clearable v-model="_value" :placeholder="placeholder"/>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<span>{{ _value }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: "E"
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请输入内容"
|
||||
}
|
||||
})
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
58
src/views/workflow/form/components/TextareaInput.vue
Normal file
58
src/views/workflow/form/components/TextareaInput.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-input size="medium" disabled :placeholder="placeholder" show-word-limit :rows="2" type="textarea"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-input size="medium" v-model="_value" clearable :maxlength="255" :placeholder="placeholder" show-word-limit
|
||||
:rows="3" type="textarea"/>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<span>{{ _value }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode:{
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请输入内容'
|
||||
}
|
||||
})
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(value) {
|
||||
emit('update:value', value)
|
||||
}
|
||||
})
|
||||
watch(()=>props.value,(value)=>{
|
||||
_value.value = value
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
143
src/views/workflow/form/components/UserPicker.vue
Normal file
143
src/views/workflow/form/components/UserPicker.vue
Normal file
@@ -0,0 +1,143 @@
|
||||
<template>
|
||||
<div style="max-width: 350px">
|
||||
<template v-if="mode === 'DESIGN'">
|
||||
<el-button disabled icon="User" type="primary" size="mini" round>选择人员</el-button>
|
||||
<span class="placeholder"> {{ placeholder }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="perm === 'E'">
|
||||
<el-button icon="User" type="primary" size="mini" round @click="chooseUser">
|
||||
选择人员
|
||||
</el-button>
|
||||
<user-picker :multiple="multiple" ref="userPicker" title="请选择人员" v-model:value="userList" @ok="selected"/>
|
||||
<span class="placeholder"> {{ placeholder }}</span>
|
||||
<div style="display: flex;">
|
||||
<div class="userStyle" v-for="(user, i) in userList" :key="i">
|
||||
<span @click="delDept(i)">×</span>
|
||||
<el-avatar :src="user.avatar"/>
|
||||
<el-tooltip class="item" effect="dark" :content="user.name" placement="bottom-start">
|
||||
<span>{{ user.name }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="perm === 'R'">
|
||||
<div style="display: flex;">
|
||||
<div v-for="(user, i) in userList" :key="i" class="view_user">
|
||||
<el-avatar :src="user.avatar"/>
|
||||
<el-tooltip effect="dark" :content="user.name" placement="bottom-start">
|
||||
<span>{{ user.name }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import UserPicker from "../../process/common/UserPicker.vue";
|
||||
import {computed, defineProps} from "vue";
|
||||
const emit = defineEmits(["input"])
|
||||
const showPickerSelect = ref(false)
|
||||
const userPicker = ref()
|
||||
const userList = ref([])
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'DESIGN'
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
},
|
||||
perm: {
|
||||
type: String,
|
||||
default: 'E'
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '请选择人员'
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const chooseUser=()=>{
|
||||
userPicker.value.showUserPicker()
|
||||
}
|
||||
watch(() => props.perm, (newVal, oldVal) => {
|
||||
console.log('newVal',newVal,userPicker.value)
|
||||
});
|
||||
|
||||
const selected = (select) => {
|
||||
let userInfoList = []
|
||||
for (let val of select) {
|
||||
let userInfo = {
|
||||
id: val.id,
|
||||
name: val.name,
|
||||
avatar: val.avatar,
|
||||
}
|
||||
userInfoList.push(userInfo)
|
||||
}
|
||||
userList.value = userInfoList
|
||||
console.log('select',userList.value)
|
||||
}
|
||||
const delDept = (i) => {
|
||||
userList.value.splice(i, 1)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.placeholder {
|
||||
margin-left: 10px;
|
||||
color: #adabab;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
.userStyle {
|
||||
width: 45px;
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
margin-right: 10px;
|
||||
|
||||
span:first-child {
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: -11px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
span:last-child {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden
|
||||
}
|
||||
}
|
||||
|
||||
.view_user {
|
||||
width: 45px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-right: 10px;
|
||||
|
||||
span {
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden
|
||||
}
|
||||
}
|
||||
</style>
|
||||
37
src/views/workflow/form/config/AmountInputConfig.vue
Normal file
37
src/views/workflow/form/config/AmountInputConfig.vue
Normal file
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="保留小数">
|
||||
<el-input-number controls-position="right" :precision="0" :max="3" :min="0" size="small" v-model="value.precision" placeholder="小数位数"/>
|
||||
位
|
||||
</el-form-item>
|
||||
<el-form-item label="展示大写">
|
||||
<el-switch v-model="value.showChinese"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "AmountInputConfig",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
38
src/views/workflow/form/config/DateTimeConfig.vue
Normal file
38
src/views/workflow/form/config/DateTimeConfig.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置日期提示"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期格式">
|
||||
<el-select size="small" v-model="value.format" filterable>
|
||||
<el-option value="yyyy" label="年"></el-option>
|
||||
<el-option value="yyyy-MM" label="年-月"></el-option>
|
||||
<el-option value="yyyy-MM-dd" label="年-月-日"></el-option>
|
||||
<el-option value="yyyy-MM-dd HH:mm" label="年-月-日 时:分"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DateTime",
|
||||
components: {},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
58
src/views/workflow/form/config/DateTimeRangeConfig.vue
Normal file
58
src/views/workflow/form/config/DateTimeRangeConfig.vue
Normal file
@@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="placeholder[0]" @change="placeholderChange" placeholder="开始日期提示"/>
|
||||
<el-input size="small" v-model="placeholder[1]" @change="placeholderChange" placeholder="结束日期提示"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期格式">
|
||||
<el-select size="small" v-model="value.format" filterable>
|
||||
<el-option value="yyyy" label="年"></el-option>
|
||||
<el-option value="yyyy-MM" label="年-月"></el-option>
|
||||
<el-option value="yyyy-MM-dd" label="年-月-日"></el-option>
|
||||
<el-option value="yyyy-MM-dd HH:mm" label="年-月-日 时:分"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="展示时长">
|
||||
<el-switch v-model="value.showLength"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "DateTimeRangeConfig",
|
||||
components: {},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
placeholder:[
|
||||
"开始时间","结束时间"
|
||||
]
|
||||
}
|
||||
},
|
||||
created() {
|
||||
console.log("出发了",this.value.placeholder,!this.value.placeholder)
|
||||
if (undefined !== this.value.placeholder){
|
||||
this.placeholder = this.value.placeholder
|
||||
}else {
|
||||
this.$set(this.value,"placeholder",this.placeholder)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
placeholderChange(){
|
||||
this.value.placeholder = this.placeholder
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
33
src/views/workflow/form/config/DescriptionConfig.vue
Normal file
33
src/views/workflow/form/config/DescriptionConfig.vue
Normal file
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示内容">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示内容"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="文字颜色">
|
||||
<el-color-picker v-model="value.color" size="medium"></el-color-picker>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Description",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
48
src/views/workflow/form/config/FileUploadConfig.vue
Normal file
48
src/views/workflow/form/config/FileUploadConfig.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="数量限制">
|
||||
<tip slot="label" content="限制最大上传图片数量(为0则不限制)">数量限制</tip>
|
||||
<el-input-number class="max-fill" controls-position="right" :precision="0" size="small" v-model="value.maxNumber" placeholder="最多上传几张图片"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="大小限制">
|
||||
<tip slot="label" content="限制单个文件最大大小-MB(为0则不限制)">大小限制</tip>
|
||||
<el-input-number class="max-fill" controls-position="right" :precision="1" size="small" v-model="value.maxSize" placeholder="单个文件最大大小"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型限制">
|
||||
<tip slot="label" content="限制上传文件的后缀类型">类型限制</tip>
|
||||
<el-select size="small" style="width: 100%;" v-model="value.fileTypes" multiple
|
||||
filterable allow-create default-first-option clearable placeholder="允许上传文件的后缀格式,可设置多种"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="不可下载">
|
||||
<el-switch v-model="value.onlyRead"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "FileUploadConfig",
|
||||
components: {},
|
||||
props: {
|
||||
value: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-form-item__label{
|
||||
padding: 0 12px 0 0;
|
||||
}
|
||||
</style>
|
||||
43
src/views/workflow/form/config/ImageUploadConfig.vue
Normal file
43
src/views/workflow/form/config/ImageUploadConfig.vue
Normal file
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="数量限制">
|
||||
<tip slot="label" content="限制最大上传图片数量(为0则不限制)">数量限制</tip>
|
||||
<el-input-number class="max-fill" controls-position="right" :precision="0" size="small" v-model="value.maxNumber" placeholder="最多上传几张图片"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="大小限制">
|
||||
<tip slot="label" content="限制单个图片最大大小-MB(为0则不限制)">大小限制</tip>
|
||||
<el-input-number class="max-fill" controls-position="right" :precision="1" size="small" v-model="value.maxSize" placeholder="单个文件最大大小"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片压缩">
|
||||
<el-switch v-model="value.enableZip"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ImageUploadConfig",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-form-item__label{
|
||||
padding: 0 12px 0 0;
|
||||
}
|
||||
</style>
|
||||
18
src/views/workflow/form/config/LocationConfig.vue
Normal file
18
src/views/workflow/form/config/LocationConfig.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Location",
|
||||
components: {},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
18
src/views/workflow/form/config/MoneyInputConfig.vue
Normal file
18
src/views/workflow/form/config/MoneyInputConfig.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MoneyInput",
|
||||
components: {},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
30
src/views/workflow/form/config/NumberInputConfig.vue
Normal file
30
src/views/workflow/form/config/NumberInputConfig.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "NumberInput",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
34
src/views/workflow/form/config/OrgPickerConfig.vue
Normal file
34
src/views/workflow/form/config/OrgPickerConfig.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否多选">
|
||||
<el-switch v-model="value.multiple"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "OrgPicker",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
39
src/views/workflow/form/config/RatePickerConfig.vue
Normal file
39
src/views/workflow/form/config/RatePickerConfig.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大分值">
|
||||
<el-input-number size="small" :min="1" v-model="value.max" />
|
||||
</el-form-item>
|
||||
<el-form-item label="允许半分">
|
||||
<el-switch v-model="value.enableHalf"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示分值">
|
||||
<el-switch v-model="value.showScore"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "RatePickerConfig",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
94
src/views/workflow/form/config/SelectInputConfig.vue
Normal file
94
src/views/workflow/form/config/SelectInputConfig.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="选项设置" class="options">
|
||||
<div slot="label" class="option-item-label">
|
||||
<span>选项设置</span>
|
||||
<el-button icon="el-icon-plus" type="primary" size="mini"
|
||||
@click="value.options.push('新选项')" link>新增选项</el-button>
|
||||
</div>
|
||||
<draggable :list="value.options" group="option" handler=".el-icon-rank" :options="dragOption">
|
||||
<div v-for="(op, index) in value.options" :key="index" class="option-item">
|
||||
<i class="el-icon-rank"></i>
|
||||
<el-input v-model="value.options[index]" size="medium" placeholder="请设置选项值" clearable>
|
||||
<el-button icon="el-icon-delete" slot="append" type="danger" size="medium"
|
||||
@click="value.options.splice(index, 1)"></el-button>
|
||||
</el-input>
|
||||
</div>
|
||||
</draggable>
|
||||
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-form-item label="选项展开">
|
||||
<el-switch v-model="value.expanding"></el-switch>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import draggable from "vuedraggable";
|
||||
|
||||
export default {
|
||||
name: "SelectInputConfig",
|
||||
components: {draggable},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dragOption:{
|
||||
animation: 300,
|
||||
sort: true
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.options{
|
||||
.el-form-item__label{
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.el-icon-rank{
|
||||
padding-right: 5px;
|
||||
cursor: move;
|
||||
}
|
||||
.option-item{
|
||||
.el-input{
|
||||
width: 250px;
|
||||
float:right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option-item-label{
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
button{
|
||||
float:right;
|
||||
}
|
||||
}
|
||||
|
||||
/*/deep/ .el-form-item {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.el-form-item__label {
|
||||
padding: 0;
|
||||
}
|
||||
.options{
|
||||
.el-icon-rank{
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
</style>
|
||||
30
src/views/workflow/form/config/SignPanelConfig.vue
Normal file
30
src/views/workflow/form/config/SignPanelConfig.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="是否裁剪">
|
||||
<el-switch v-model="value.isCrop"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SignPanelConfig",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
52
src/views/workflow/form/config/TableListConfig.vue
Normal file
52
src/views/workflow/form/config/TableListConfig.vue
Normal file
@@ -0,0 +1,52 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="提醒添加记录的提示"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="最大行数">
|
||||
<tip slot="label" content="允许添加多少条记录(为0则不限制)">最大行数</tip>
|
||||
<el-input-number controls-position="right" :precision="0" :max="100" :min="0" size="small" v-model="value.maxSize" placeholder="限制条数"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="布局方式">
|
||||
<el-radio name="layout" :label="true" v-model="value.rowLayout">按表格</el-radio>
|
||||
<el-radio name="layout" :label="false" v-model="value.rowLayout">按表单</el-radio>
|
||||
</el-form-item>
|
||||
<el-form-item label="展示合计">
|
||||
<el-switch v-model="value.showSummary"></el-switch>
|
||||
<el-select v-if="value.showSummary" style="width: 100%;" size="small" v-model="value.summaryColumns" multiple clearable placeholder="请选择合计项" filterable>
|
||||
<el-option :label="column.title" :value="column.id" v-for="column in columns" :key="column.id"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="展示边框">
|
||||
<el-switch v-model="value.showBorder"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TableListConfig",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
computed:{
|
||||
columns(){
|
||||
return this.value.columns.filter(c => c.valueType === 'Number')
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
30
src/views/workflow/form/config/TextInputConfig.vue
Normal file
30
src/views/workflow/form/config/TextInputConfig.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TextInput",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
30
src/views/workflow/form/config/TextareaInputConfig.vue
Normal file
30
src/views/workflow/form/config/TextareaInputConfig.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form-item label="提示文字">
|
||||
<el-input size="small" v-model="value.placeholder" placeholder="请设置提示语"/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TextareaInput",
|
||||
components: {},
|
||||
props:{
|
||||
value:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
methods: {}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
27
src/views/workflow/form/utils/CustomUtil.js
Normal file
27
src/views/workflow/form/utils/CustomUtil.js
Normal file
@@ -0,0 +1,27 @@
|
||||
Array.prototype.remove = function (value) {
|
||||
let index = this.indexOf(value)
|
||||
if (index > -1) {
|
||||
this.splice(index, 1)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
//移除对象数组,匹配唯一key
|
||||
Array.prototype.removeByKey = function (key, val) {
|
||||
let index = this.findIndex(value => value[key] === val)
|
||||
if (index > -1) {
|
||||
this.splice(index, 1)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
//对象数组转map
|
||||
Array.prototype.toMap = function (key) {
|
||||
let map = new Map()
|
||||
this.forEach(v => map.set(v[key], v))
|
||||
return map
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
118
src/views/workflow/form/utils/date.js
Normal file
118
src/views/workflow/form/utils/date.js
Normal file
@@ -0,0 +1,118 @@
|
||||
//时间转换为String类型
|
||||
const moment = require("moment");
|
||||
|
||||
function simpleDateFormat(pattern) {
|
||||
var fmt = new Object();
|
||||
fmt.pattern = pattern;
|
||||
|
||||
fmt.parse = function (source) {
|
||||
try {
|
||||
return new Date(source);
|
||||
} catch (e) {
|
||||
console.log("字符串 " + source + " 转时间格式失败!");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
fmt.format = function (date) {
|
||||
if (typeof (date) == "undefined" || date == null || date == "") {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
date = new Date(date);
|
||||
} catch (e) {
|
||||
console.log("时间 " + date + " 格式化失败!");
|
||||
return "";
|
||||
}
|
||||
|
||||
var strTime = this.pattern;//时间表达式的正则
|
||||
|
||||
var o = {
|
||||
"M+": date.getMonth() + 1, //月份
|
||||
"d+": date.getDate(), //日
|
||||
"H+": date.getHours(), //小时
|
||||
"m+": date.getMinutes(), //分
|
||||
"s+": date.getSeconds(), //秒
|
||||
"q+": Math.floor((date.getMonth() + 3) / 3), //季度
|
||||
"S": date.getMilliseconds() //毫秒
|
||||
};
|
||||
|
||||
if (/(y+)/.test(strTime)) {
|
||||
strTime = strTime
|
||||
.replace(RegExp.$1, (date.getFullYear() + "")
|
||||
.substr(4 - RegExp.$1.length));
|
||||
}
|
||||
for (var k in o) {
|
||||
if (new RegExp("(" + k + ")").test(strTime)) {
|
||||
strTime = strTime.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length)));
|
||||
}
|
||||
}
|
||||
|
||||
return strTime;
|
||||
};
|
||||
return fmt;
|
||||
}
|
||||
|
||||
//时间格式化为yyyy-MM-dd
|
||||
function simpleDateFormatByMoreLine(date) {
|
||||
var fmt = simpleDateFormat("yyyy-MM-dd");
|
||||
date = fmt.parse(date)
|
||||
return fmt.format(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param dateBegin 开始时间
|
||||
* @param dateEnd 结束时间
|
||||
* @returns {string} 时间差 天 小时 分钟 秒
|
||||
*/
|
||||
function consumingTime(dateBegin, dateEnd) {
|
||||
// //如果时间格式是正确的,那下面这一步转化时间格式就可以不用了
|
||||
let submitTime = new Date(dateBegin)
|
||||
let endTime = new Date(dateEnd)
|
||||
var dateDiff = endTime - submitTime;//时间差的毫秒数
|
||||
var dayDiff = Math.floor(dateDiff / (24 * 3600 * 1000));//计算出相差天数
|
||||
var leave1 = dateDiff % (24 * 3600 * 1000) //计算天数后剩余的毫秒数
|
||||
var hours = Math.floor(leave1 / (3600 * 1000))//计算出小时数
|
||||
//计算相差分钟数
|
||||
var leave2 = leave1 % (3600 * 1000) //计算小时数后剩余的毫秒数
|
||||
var minutes = Math.floor(leave2 / (60 * 1000))//计算相差分钟数
|
||||
//计算相差秒数
|
||||
var leave3 = leave2 % (60 * 1000) //计算分钟数后剩余的毫秒数
|
||||
var seconds = Math.round(leave3 / 1000)
|
||||
return dayDiff + "天 " + hours + "小时 " + minutes + " 分钟" + seconds + " 秒";
|
||||
}
|
||||
|
||||
export function timeLength(start, dateEnd) {
|
||||
// //如果时间格式是正确的,那下面这一步转化时间格式就可以不用了
|
||||
let mstart = moment(start);
|
||||
let mend = moment(dateEnd);
|
||||
let years = mend.diff(start, "years");
|
||||
let months = mend.diff(start, "months");
|
||||
let days = mend.diff(start, "days");
|
||||
let hours = mend.diff(start, "hours");
|
||||
let minutes = mend.diff(start, "minutes");
|
||||
minutes = minutes % 60;
|
||||
hours = hours % 24;
|
||||
months = months % 12;
|
||||
//因为每月天不固定,所以天要特殊动态处理
|
||||
if (mstart.date() < mend.date()) {
|
||||
days = mend.date() - mstart.date();
|
||||
if (minutes > 0 || hours > 0) {
|
||||
days--;
|
||||
}
|
||||
}
|
||||
//处理超过俩月且天超过31
|
||||
if (days > 31 && mend.month() - mstart.month() >= 2) {
|
||||
//将日期推至上月求差
|
||||
days = mend.diff(mstart.add(mend.month() - mstart.month() - 1, "month"), "days");
|
||||
}
|
||||
return `${years > 0 ? years + "年 " : " "}` + `${months > 0 ? months + "个月 " : " "}` + `${days > 0 ? days + "天 " : " "}`
|
||||
+ `${hours > 0 ? hours + "小时 " : " "}` + `${minutes > 0 ? minutes + "分钟 " : " "}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatToYYYYMMDD: simpleDateFormatByMoreLine,
|
||||
consumingTime: consumingTime,
|
||||
timeLength: timeLength
|
||||
}
|
||||
155
src/views/workflow/initiated/index.vue
Normal file
155
src/views/workflow/initiated/index.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="queryParams" inline class="query-form" ref="queryForm" @submit.prevent="getList">
|
||||
<el-form-item label="部署名称" prop="departmentName">
|
||||
<el-input v-model="queryParams.departmentName" placeholder="请输入部署名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="状态" prop="enable">-->
|
||||
<!-- <el-select v-model="queryParams.enable" placeholder="请选择状态" clearable filterable>-->
|
||||
<!-- <el-option-->
|
||||
<!-- v-for="dict in cacheStore.getDict('regular_enable')"-->
|
||||
<!-- :key="dict.value"-->
|
||||
<!-- :label="dict.label"-->
|
||||
<!-- :value="dict.value"-->
|
||||
<!-- />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getList" :icon="Search">搜索</el-button>
|
||||
<el-button type="primary" @click="handleReset" :icon="Refresh" plain>重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="table">
|
||||
<el-table
|
||||
:data="list"
|
||||
row-key="id"
|
||||
:lazy="true"
|
||||
ref="singleTable"
|
||||
@cell-click="showDetails"
|
||||
v-loading="loading"
|
||||
v-tabh
|
||||
>
|
||||
<el-table-column type="selection" width="30"/>
|
||||
<el-table-column label="序号" type="index" width="60" align="center"/>
|
||||
<el-table-column prop="deploymentName" label="审批类型" align="center"/>
|
||||
<el-table-column prop="approveName" label="审批人" align="center"/>
|
||||
<el-table-column prop="submitTime" label="提交时间" align="center"/>
|
||||
<el-table-column prop="endTime" label="结束时间" align="center"/>
|
||||
<el-table-column prop="taskName" label="当前节点" align="center"/>
|
||||
<el-table-column label="处理耗时" align="center">
|
||||
<template #default="scope">
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="state" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<point-tag dict-type="process_state" :value="scope.row.state"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<template v-if="scope.row.state === '4'">
|
||||
<el-button size="min" type="primary" @click="resubmitHander(scope.row.taskId)" link>再次提交</el-button>
|
||||
</template>
|
||||
<template v-if="scope.row.state === '2' || scope.row.state === '3'">
|
||||
<el-button size="min" type="primary" @click="resubmitHander(scope.row.taskId)" link>重新提交
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<paging :current-page="pageInfo.pageNum" :page-size="pageInfo.pageSize" :page-sizes="[10, 20, 30, 40,50]"
|
||||
:total="total" @changeSize="handleSizeChange" @goPage="handleCurrentChange"/>
|
||||
<instance-details ref="instanceDetails" :instance="selectProcessInstance"/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {getInitiatedInstanceList} from "@/api/workflow/process-instance.js";
|
||||
import {Search, Refresh, Delete, Edit} from '@element-plus/icons-vue'
|
||||
import InstanceDetails from '@/views/workflow/common/InstanceDetails.vue'
|
||||
import {useCacheStore} from '@/stores/cache.js'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import {ElMessage} from "element-plus";
|
||||
import Paging from "@/components/pagination/index.vue";
|
||||
import PointTag from "@/components/PointTag.vue";
|
||||
|
||||
const dictStore = useCacheStore()
|
||||
dictStore.setCacheKey(['process_state'])
|
||||
|
||||
const processStore = useProcessStore()
|
||||
//查询参数
|
||||
const queryParams = reactive({
|
||||
departmentName: '',
|
||||
// state: 1
|
||||
})
|
||||
|
||||
//页面信息
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const list = ref([])
|
||||
const selectProcessInstance = ref()
|
||||
const total = ref()
|
||||
const loading = ref()
|
||||
const instanceDetails = ref()
|
||||
const queryForm = ref()
|
||||
|
||||
const showDetails = (row, column) => {
|
||||
if (column.label !== '操作') {
|
||||
selectProcessInstance.value = row
|
||||
nextTick(() => {
|
||||
instanceDetails.value.init()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//重置搜索
|
||||
const handleReset = () => {
|
||||
queryForm.value.resetFields()
|
||||
getList()
|
||||
}
|
||||
const getList = async () => {
|
||||
let params = {
|
||||
...queryParams,
|
||||
...pageInfo
|
||||
}
|
||||
loading.value = true
|
||||
getInitiatedInstanceList(params).then(res => {
|
||||
if (res.code === 1000) {
|
||||
list.value = res.data.rows
|
||||
total.value = res.data.total
|
||||
loading.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//切换每页显示条数
|
||||
const handleSizeChange = async (val) => {
|
||||
pageInfo.value.pageSize = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
//点击页码进行分页功能
|
||||
const handleCurrentChange = async (val) => {
|
||||
pageInfo.value.pageNum = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
const getTimeConsuming = async (instance) => {
|
||||
if (instance.state != 1) {
|
||||
//dateFormat(开始时间,结束时间)
|
||||
// return timeLength(instance.submitTime, instance.endTime);
|
||||
}
|
||||
}
|
||||
|
||||
getList()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
339
src/views/workflow/listen/index.vue
Normal file
339
src/views/workflow/listen/index.vue
Normal file
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="queryParams" inline class="query-form" ref="queryForm">
|
||||
<el-form-item label="监听器类型" prop="listenerType">
|
||||
<el-select v-model="queryParams.listenerType" placeholder="请选择监听器类型" filterable>
|
||||
<el-option
|
||||
v-for="item in cacheStore.getDict('process_listen_type')"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="监听器名称" prop="listenerName">
|
||||
<el-input v-model="queryParams.listenerName" placeholder="请输入监听器名称"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="监听器数据类型" prop="listenerValueType">
|
||||
<el-select v-model="queryParams.listenerValueType" placeholder="请选择监听器数据类型" filterable>
|
||||
<el-option
|
||||
v-for="item in cacheStore.getDict('process_data_type')"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch()" :icon="Search">搜索</el-button>
|
||||
<el-button type="primary" @click="handleReset" :icon="Refresh" plain>重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="query-btn">
|
||||
<el-button type="primary" v-perm="['code:listener:add']" @click="handleAdd" :icon="Plus" >新增</el-button>
|
||||
<el-button type="danger" v-perm="['code:listener:del']" @click="handleMoreDelete(listenId,listenNameList)" :icon="Delete" plain
|
||||
:disabled="disabled">删除
|
||||
</el-button>
|
||||
<el-button type="warning" v-perm="['code:listener:export']" @click="handleExport" :icon="Download" plain>导出
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="table">
|
||||
<el-table
|
||||
:data="list"
|
||||
row-key="id"
|
||||
:lazy="true"
|
||||
ref="singleTable"
|
||||
v-loading="loading"
|
||||
@select="handleSelect"
|
||||
v-tabh
|
||||
>
|
||||
<el-table-column type="selection" width="55"/>
|
||||
<el-table-column label="序号" type="index" width="60" align="center"/>
|
||||
<el-table-column prop="listenerType" label="监听器类型" align="center">
|
||||
<template #default="scope">
|
||||
<tag dict-type="process_listen_type" :value="scope.row.listenerType"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="eventType" label="监听类型" align="center"/>
|
||||
<el-table-column prop="listenerName" label="监听器名称" align="center"/>
|
||||
<el-table-column prop="listenerValue" label="监听器值" align="center"/>
|
||||
<el-table-column prop="listenerValueType" label="监听器数据类型" align="center">
|
||||
<template #default="scope">
|
||||
<tag dict-type="process_data_type" :value="scope.row.listenerValueType"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="mini" v-perm="['code:listener:edit']"
|
||||
@click="handleEdit(scope.row.id)" link>编辑
|
||||
</el-button>
|
||||
<popover-delete :name="scope.row.dsName" :type="'系统内置监听器'" :perm="['code:listener:del']"
|
||||
@delete="handleDelete(scope.row.id)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<paging :current-page="pageInfo.pageNum" :page-size="pageInfo.pageSize" :page-sizes="[10, 20, 30, 40,50]"
|
||||
:total="total" @changeSize="handleSizeChange" @goPage="handleCurrentChange"/>
|
||||
<el-dialog v-model="isVisited" :title="title" width="900px">
|
||||
<el-form :model="form" ref="formInstance" :rules="formRules" label-width="100px" class="dialog-form">
|
||||
<el-row>
|
||||
<el-col :span="11">
|
||||
<el-form-item label="监听器名称" prop="listenerName" class="listen">
|
||||
<el-input placeholder="请设置监听器名称" v-model="form.listenerName"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="11" :offset="2">
|
||||
<el-form-item label="监听器类型" prop="eventType" class="listen">
|
||||
<el-select placeholder="请选择监听器类型" @change="form.eventType = []"
|
||||
v-model="form.listenerType" filterable>
|
||||
<el-option :value="'1'" label="任务监听"/>
|
||||
<el-option :value="'2'" label="执行监听"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="11">
|
||||
<el-form-item label="事件类型" prop="listenerType" class="listen">
|
||||
<el-select multiple placeholder="请选择事件类型" v-model="form.eventType" filterable>
|
||||
<el-option value="create" label="create"/>
|
||||
<el-option v-if="form.listenerType === '2'" value="end" label="end"/>
|
||||
<el-option v-if="form.listenerType === '2'" value="take" label="take"/>
|
||||
<el-option v-if="form.listenerType === '1'" value="assignment" label="assignment"/>
|
||||
<el-option v-if="form.listenerType === '1'" value="complete" label="complete"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="11" :offset="2">
|
||||
<el-form-item label="值类型" prop="listenerValueType" class="listen">
|
||||
<el-radio-group style="margin: 0 5px;" @change="listenerValueTypeChange"
|
||||
v-model="form.listenerValueType">
|
||||
<el-radio-button :label="'0'">Java类</el-radio-button>
|
||||
<el-radio-button :label="'1'">表达式</el-radio-button>
|
||||
<el-radio-button :label="'2'">代理表达式</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col>
|
||||
<el-form-item label="值" prop="listenerValue" class="listen">
|
||||
<el-input :placeholder="listenerValuePlaceholder" v-model="form.listenerValue"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span>
|
||||
<el-button @click="handleCancel">取消</el-button>
|
||||
<el-button type="primary" @click="handleSubmit(formInstance)">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
getProcessListenerList,
|
||||
getProcessListenerDetails,
|
||||
addProcessListener,
|
||||
editProcessListener,
|
||||
delProcessListener
|
||||
} from "@/api/workflow/process-listen.js";
|
||||
import {Search, Refresh, Delete, Plus, Edit, Download} from '@element-plus/icons-vue'
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {downLoadExcel} from "@/utils/downloadZip";
|
||||
import Tag from '@/components/Tag.vue'
|
||||
import {useCacheStore} from '@/stores/cache.js'
|
||||
|
||||
import Paging from "@/components/pagination/index.vue";
|
||||
|
||||
const cacheStore = useCacheStore()
|
||||
//查询参数
|
||||
const queryParams = reactive({
|
||||
listenerType: '',
|
||||
listenerName: '',
|
||||
listenerValueType: '',
|
||||
})
|
||||
//页面信息
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const disabled = ref(true)
|
||||
const list = ref([])
|
||||
const queryForm = ref([])
|
||||
const listenId = ref([])
|
||||
const listenNameList = ref([])
|
||||
const loading = ref(true)
|
||||
const total = ref()
|
||||
const title = ref('')
|
||||
const isVisited = ref(false)
|
||||
const form = ref()
|
||||
const formInstance = ref()
|
||||
const listenerValuePlaceholder = ref('请输入类路径')
|
||||
const formRules = ref({
|
||||
listenerType: [
|
||||
{required: true, message: "监听器类型 1 : 任务监听 2: 执行监听不能为空", trigger: "change"},
|
||||
],
|
||||
eventType: [
|
||||
{required: true, message: "监听类型 create assignment complete end take不能为空", trigger: "change"},
|
||||
],
|
||||
listenerName: [
|
||||
{required: true, message: "监听器名称不能为空", trigger: "blur"},
|
||||
],
|
||||
listenerValue: [
|
||||
{required: true, message: "监听器值不能为空", trigger: "blur"},
|
||||
],
|
||||
})
|
||||
//搜索功能
|
||||
const handleSearch = () => {
|
||||
getList()
|
||||
}
|
||||
//重置搜索
|
||||
const handleReset = () => {
|
||||
queryForm.value.resetFields()
|
||||
getList()
|
||||
}
|
||||
//获取数据
|
||||
const getList = async () => {
|
||||
let params = {
|
||||
...queryParams,
|
||||
...pageInfo
|
||||
}
|
||||
loading.value = true
|
||||
getProcessListenerList(params).then(res => {
|
||||
if (res.code === 1000) {
|
||||
list.value = res.data.rows
|
||||
total.value = res.data.total
|
||||
loading.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const listenerValueTypeChange = () => {
|
||||
if (form.value.listenerValueType === '0') {
|
||||
listenerValuePlaceholder.value = '请输入类路径'
|
||||
} else {
|
||||
listenerValuePlaceholder.value = '请输入表达式'
|
||||
}
|
||||
}
|
||||
//重置from表单
|
||||
const restFrom = () => {
|
||||
form.value = {
|
||||
listenerType: '1',
|
||||
listenerName: '',
|
||||
listenerValue: '',
|
||||
eventType: [],
|
||||
listenerValueType: '0',
|
||||
}
|
||||
}
|
||||
//取消
|
||||
const handleCancel = () => {
|
||||
restFrom()
|
||||
isVisited.value = false
|
||||
}
|
||||
//提交
|
||||
const handleSubmit = async (instance) => {
|
||||
if (!instance) return
|
||||
instance.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
if (title.value === '新增系统内置监听器') {
|
||||
addProcessListener(form.value).then(res => {
|
||||
if (res.code === 1000) {
|
||||
ElMessage.success(res.msg)
|
||||
getList()
|
||||
isVisited.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
editProcessListener(form.value).then(res => {
|
||||
if (res.code === 1000) {
|
||||
ElMessage.success(res.msg)
|
||||
getList()
|
||||
isVisited.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
//添加
|
||||
const handleAdd = async () => {
|
||||
restFrom()
|
||||
title.value = "新增系统内置监听器"
|
||||
isVisited.value = true
|
||||
nextTick(()=>{
|
||||
// 清空校验
|
||||
formInstance.value.clearValidate()
|
||||
})
|
||||
}
|
||||
//修改
|
||||
const handleEdit = async (id) => {
|
||||
restFrom()
|
||||
getProcessListenerDetails(id).then(res => {
|
||||
if (res.code === 1000) {
|
||||
form.value = res.data
|
||||
title.value = "编辑系统内置监听器"
|
||||
isVisited.value = true
|
||||
nextTick(()=>{
|
||||
// 清空校验
|
||||
formInstance.value.clearValidate()
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
//导出excel
|
||||
const handleExport = () => {
|
||||
downLoadExcel('/workflow/process/listener/export', {...queryParams})
|
||||
}
|
||||
|
||||
//勾选table数据行的 Checkbox
|
||||
const handleSelect = async (selection) => {
|
||||
if (selection.length !== 0) {
|
||||
disabled.value = false
|
||||
listenId.value = selection.map(item => item.id).join()
|
||||
listenNameList.value = selection.map(item => item.listenerName).join()
|
||||
} else {
|
||||
disabled.value = true
|
||||
}
|
||||
}
|
||||
|
||||
//切换每页显示条数
|
||||
const handleSizeChange = async (val) => {
|
||||
pageInfo.value.pageSize = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
//点击页码进行分页功能
|
||||
const handleCurrentChange = async (val) => {
|
||||
pageInfo.value.pageNum = val
|
||||
await getList()
|
||||
}
|
||||
const handleMoreDelete=(listenId,listenNameList)=>{
|
||||
ElMessageBox.confirm(`确认删除名称为${listenNameList}的系统内置监听器吗?`, '系统提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
handleDelete(listenId)
|
||||
})
|
||||
}
|
||||
//删除
|
||||
const handleDelete = async (id) => {
|
||||
delProcessListener(id).then(res => {
|
||||
if (res.code === 1000) {
|
||||
ElMessage.success(res.msg)
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
getList()
|
||||
</script>
|
||||
118
src/views/workflow/process/DefaultNodeProps.js
Normal file
118
src/views/workflow/process/DefaultNodeProps.js
Normal file
@@ -0,0 +1,118 @@
|
||||
//审批节点默认属性
|
||||
export const APPROVAL_PROPS = {
|
||||
assignedType: "ASSIGN_USER", //审批类型
|
||||
mode: "AND", //会签模式
|
||||
sign: false, //是否签字
|
||||
headerBgc: '#ff943e', //节点背景颜色
|
||||
nobody: { //没有审批的的时候需要的操作
|
||||
handler: "TO_PASS", //操作
|
||||
assignedUser: [] //审批人列表
|
||||
},
|
||||
timeLimit: { //边界事件
|
||||
timeout: { //超时提醒时间
|
||||
unit: "H",
|
||||
value: 0
|
||||
},
|
||||
handler: { //超时提醒触发时候的操作
|
||||
type: "REFUSE", //操作
|
||||
notify: {
|
||||
once: true, //是否循环
|
||||
hour: 1
|
||||
}
|
||||
}
|
||||
},
|
||||
assignedUser: [], // 审批人列表
|
||||
formPerms: [], //表单权限
|
||||
selfSelect: { //用户自选
|
||||
multiple: false //用户自选时是否是多选
|
||||
},
|
||||
leaderTop: { //领导
|
||||
endCondition: "TOP",
|
||||
endLevel: 1,
|
||||
},
|
||||
leader: { //第几级领导
|
||||
level: 1
|
||||
},
|
||||
listener:{
|
||||
state: false,
|
||||
list:[]
|
||||
},
|
||||
roleList: [], //角色列表
|
||||
refuse: { //拒绝的操作
|
||||
type: 'TO_END', //驳回规则 TO_END TO_NODE TO_BEFORE
|
||||
target: '' //驳回到指定ID的节点
|
||||
},
|
||||
formUser: '' //表单用户
|
||||
}
|
||||
|
||||
//根节点默认属性
|
||||
export const ROOT_PROPS = {
|
||||
assignedUser: [], //审批人
|
||||
formPerms: [] //表单权限
|
||||
}
|
||||
|
||||
//条件节点默认属性
|
||||
export const CONDITION_PROPS = {
|
||||
groupsType: "OR", //条件组逻辑关系 OR、AND
|
||||
groups: [
|
||||
{
|
||||
groupType: "AND", //条件组内条件关系 OR、AND
|
||||
cids: [], //条件ID集合
|
||||
conditions: [] //组内子条件
|
||||
}
|
||||
],
|
||||
expression: "" //自定义表达式,灵活构建逻辑关系
|
||||
}
|
||||
|
||||
//抄送节点默认属性
|
||||
export const CC_PROPS = {
|
||||
shouldAdd: false,
|
||||
assignedUser: [],
|
||||
formPerms: []
|
||||
}
|
||||
|
||||
//触发器节点默认属性
|
||||
export const TRIGGER_PROPS = {
|
||||
type: 'WEBHOOK',
|
||||
http: {
|
||||
method: 'GET', //请求方法 支持GET/POST
|
||||
url: '', //URL地址,可以直接带参数
|
||||
headers: [ //http header
|
||||
{
|
||||
name: '',
|
||||
isField: true,
|
||||
value: '' //支持表达式 ${xxx} xxx为表单字段id
|
||||
}
|
||||
],
|
||||
contentType: 'FORM', //请求参数类型
|
||||
params: [ //请求参数
|
||||
{
|
||||
name: '',
|
||||
isField: true, //是表单字段还是自定义
|
||||
value: '' //支持表达式 ${xxx} xxx为表单字段id
|
||||
}
|
||||
],
|
||||
retry: 1,
|
||||
handlerByScript: false,
|
||||
success: 'function handlerSuccess(res) {\n return {\n state: true, \n msg: "请求成功!" \n };\n}',
|
||||
fail: 'function handlerFail(res) {\n return {\n state: true, \n msg: "请求失败!" \n };\n}'
|
||||
},
|
||||
email: {
|
||||
subject: '',
|
||||
to: [],
|
||||
cc: [],
|
||||
content: ''
|
||||
}
|
||||
}
|
||||
|
||||
//延时节点默认属性
|
||||
export const DELAY_PROPS = {
|
||||
type: "FIXED", //延时类型 FIXED:到达当前节点后延时固定时长 、AUTO:延时到 dateTime设置的时间
|
||||
time: 0, //延时时间
|
||||
unit: "M", //时间单位 D天 H小时 M分钟
|
||||
dateTime: "" //如果当天没有超过设置的此时间点,就延时到这个指定的时间,到了就直接跳过不延时
|
||||
}
|
||||
|
||||
export default {
|
||||
APPROVAL_PROPS, CC_PROPS, DELAY_PROPS, CONDITION_PROPS, ROOT_PROPS, TRIGGER_PROPS
|
||||
}
|
||||
72
src/views/workflow/process/ProcessDesign.vue
Normal file
72
src/views/workflow/process/ProcessDesign.vue
Normal file
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="scale">
|
||||
<el-button icon="Plus" size="mini" @click.stop="scale += 10" :disabled="scale >= 150" circle></el-button>
|
||||
<span>{{ scale }}%</span>
|
||||
<el-button icon="Minus" size="mini" @click.stop="scale -= 10" :disabled="scale <= 40" circle></el-button>
|
||||
</div>
|
||||
<div :style="'transform: scale('+ scale / 100 +');'">
|
||||
<div id="processTree">
|
||||
<process-tree ref="processTree" mode="design" id-name="processTree" @selectedNode="nodeSelected"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<el-drawer :title="selectedNode.name" v-model="showConfig"
|
||||
:modal-append-to-body="false"
|
||||
:size="selectedNode.type === 'CONDITION' ? '600px':'500px'"
|
||||
direction="rtl">
|
||||
<template #header>
|
||||
<div>
|
||||
<el-input v-model="selectedNode.name" size="medium" v-show="showInput"
|
||||
style="width: 300px" @blur="showInput = false"></el-input>
|
||||
<el-link v-show="!showInput" @click="showInput = true" style="font-size: medium">
|
||||
<i class="el-icon-edit" style="margin-right: 10px"></i>
|
||||
{{ selectedNode.name }}
|
||||
</el-link>
|
||||
</div>
|
||||
</template>
|
||||
<template #default>
|
||||
<div slot="title">
|
||||
</div>
|
||||
<div class="node-config-content">
|
||||
<node-config v-if="showConfig" @initRender="initRender"/>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ProcessTree from './ProcessTree.vue'
|
||||
import NodeConfig from './config/NodeConfig.vue'
|
||||
import {computed, ref, defineExpose} from 'vue';
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
|
||||
const processStore = useProcessStore()
|
||||
const scale = ref(100)
|
||||
const showConfig = ref(false)
|
||||
const processTree = ref()
|
||||
const showInput = ref(false)
|
||||
const selectedNode = computed(() => {
|
||||
return processStore.getSelectedNode()
|
||||
})
|
||||
|
||||
const nodeSelected = (node) => {
|
||||
showConfig.value = true
|
||||
}
|
||||
const initRender = () => {
|
||||
nextTick(() => {
|
||||
processTree.value.init()
|
||||
})
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
return processTree.value.validateProcess()
|
||||
}
|
||||
|
||||
|
||||
defineExpose({
|
||||
validate,
|
||||
initRender
|
||||
})
|
||||
|
||||
</script>
|
||||
342
src/views/workflow/process/ProcessEdit.vue
Normal file
342
src/views/workflow/process/ProcessEdit.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div style="margin-top: 15px">
|
||||
<el-button @click="changPan('formDesign')">表单</el-button>
|
||||
<el-button @click="changPan('processDesign')">流程</el-button>
|
||||
<el-button @click="publishProcess">发布</el-button>
|
||||
<div class="layout-body" v-if="visible">
|
||||
<div v-show="activeSelect === 'processDesign'">
|
||||
<process-design ref="processDesign"/>
|
||||
</div>
|
||||
<div v-show="activeSelect === 'formDesign'" >
|
||||
<form-design ref="formDesign"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-dialog v-model="validVisible" title="设置项检查">
|
||||
<el-steps align-center :active="validStep" finish-status="success">
|
||||
<el-step v-for="(step, i) in validOptions" :title="step.title" :key="i"
|
||||
:icon="step.icon" :status="step.status" :description="step.description"/>
|
||||
</el-steps>
|
||||
<el-result :icon="validIcon" :title="errTitle" :sub-title="validResult.desc">
|
||||
<template #icon>
|
||||
<el-icon style="font-size: 30px" v-if="!validResult.finished">
|
||||
<Loading/>
|
||||
</el-icon>
|
||||
<div slot="subTitle" class="err-info" v-if="validResult.errs.length > 0">
|
||||
<ellipsis hover-tip v-for="(err, i) in validResult.errs" :key="i + '_err'" :content="err">
|
||||
<div slot="pre">
|
||||
<el-icon>
|
||||
<WarningFilled/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</ellipsis>
|
||||
</div>
|
||||
<div v-if="validResult.finished && validResult.success">
|
||||
<el-icon color="#67c23a" size="70"><CircleCheckFilled /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
<template #extra>
|
||||
<el-button type="primary" v-if="validResult.finished" size="medium" @click="doAfter">
|
||||
{{ validResult.action }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-result>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {getProcessDefinitionInfo, addProcessDefinition} from "@/api/workflow/process-definition.js";
|
||||
import ProcessDesign from './ProcessDesign.vue'
|
||||
import FormDesign from '../form/FormDesign.vue'
|
||||
import Ellipsis from './common/Ellipsis.vue'
|
||||
import {getCurrentInstance} from '@vue/runtime-core';
|
||||
let {proxy} = getCurrentInstance();
|
||||
import {Loading,WarningFilled,CircleCheckFilled} from '@element-plus/icons-vue'
|
||||
import {ref,computed} from 'vue'
|
||||
const router = useRouter()
|
||||
const params = reactive(router.currentRoute.value.params)
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
const processStore = useProcessStore()
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
|
||||
const processDesign = ref()
|
||||
const visible = ref(false)
|
||||
const timer = ref(null)
|
||||
const validComponents = ref(['formDesign','processDesign'])
|
||||
// const activeSelect = ref('formDesign')
|
||||
const activeSelect = ref('processDesign')
|
||||
const validVisible = ref(false)
|
||||
const validStep = ref(0)
|
||||
const validResult = ref({})
|
||||
const validOptions = ref([
|
||||
{title: '基础信息', description: '', icon: '', status: ''},
|
||||
{title: '审批表单', description: '', icon: '', status: ''},
|
||||
// {title: '审批流程', description: '', icon: '', status: ''},
|
||||
// {title: '扩展设置', description: '', icon: '', status: ''}
|
||||
])
|
||||
|
||||
|
||||
const errTitle = computed(()=>{
|
||||
if (validResult.finished && !validResult.success) {
|
||||
return validResult.title + ` (${validResult.errs.length}项错误) 😥`
|
||||
}
|
||||
return validResult.title
|
||||
})
|
||||
|
||||
|
||||
const validIcon = computed(()=>{
|
||||
if (!validResult.finished) {
|
||||
return Loading
|
||||
} else if (validResult.success) {
|
||||
return CircleCheckFilled
|
||||
} else {
|
||||
return WarningFilled
|
||||
}
|
||||
})
|
||||
|
||||
const init = () => {
|
||||
let deploymentId = params.deploymentId
|
||||
if (deploymentId === undefined){
|
||||
loadInitFrom()
|
||||
}else {
|
||||
getProcessInfo()
|
||||
}
|
||||
}
|
||||
|
||||
const loadInitFrom = () => {
|
||||
let design = {
|
||||
processDefinitionKey: 'pro' + getRandomId(),
|
||||
deploymentName: "未命名表单",
|
||||
logo: {
|
||||
icon: "el-icon-eleme",
|
||||
background: "#1e90ff"
|
||||
},
|
||||
settings: {
|
||||
commiter: [],
|
||||
admin: [],
|
||||
sign: false,
|
||||
notify: {
|
||||
types: ["APP"],
|
||||
title: "消息通知标题"
|
||||
}
|
||||
},
|
||||
groupId: 1,
|
||||
formItems: [],
|
||||
process: [
|
||||
{
|
||||
id: "root",
|
||||
parentId: "admin",
|
||||
type: "ROOT",
|
||||
name: "发起人",
|
||||
desc: "任何人",
|
||||
props: {
|
||||
assignedUser: [],
|
||||
formPerms: []
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "end",
|
||||
parentId: "root",
|
||||
type: "END",
|
||||
}
|
||||
],
|
||||
remark: "备注说明"
|
||||
}
|
||||
processStore.setDesign(design)
|
||||
visible.value = true
|
||||
nextTick(()=>{
|
||||
processDesign.value.initRender()
|
||||
})
|
||||
}
|
||||
|
||||
const getRandomId = () => {
|
||||
let d = new Date().getTime()
|
||||
// x 是 0-9 或 a-f 范围内的一个32位十六进制数
|
||||
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
let r = (d + Math.random() * 16) % 16 | 0
|
||||
d = Math.floor(d / 16)
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
|
||||
})
|
||||
return 'node_' + id
|
||||
}
|
||||
|
||||
const getProcessInfo = async () => {
|
||||
getProcessDefinitionInfo(params.deploymentId).then(res => {
|
||||
if (res.code === 1000) {
|
||||
processStore.setDesign(res.data)
|
||||
visible.value = true
|
||||
nextTick(()=>{
|
||||
processDesign.value.initRender()
|
||||
})
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const validateDesign = () => {
|
||||
validVisible.value = true
|
||||
validStep.value = 0
|
||||
showValiding()
|
||||
stopTimer()
|
||||
timer.value = setInterval(() => {
|
||||
validResult.value.errs = proxy.$refs[validComponents.value[validStep.value]].validate()
|
||||
if (Array.isArray(validResult.value.errs) && validResult.value.errs.length === 0) {
|
||||
validStep.value++;
|
||||
if (validStep.value >= validOptions.value.length) {
|
||||
stopTimer()
|
||||
showValidFinish(true)
|
||||
}
|
||||
} else {
|
||||
stopTimer()
|
||||
validOptions.value[validStep.value].status = 'error'
|
||||
showValidFinish(false, getDefaultValidErr())
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
|
||||
const getDefaultValidErr = () => {
|
||||
switch (validStep.value) {
|
||||
case 0:
|
||||
return '请检查基础设置项';
|
||||
case 1:
|
||||
return '请检查审批表单相关设置'
|
||||
// case 2:
|
||||
// return '请检查审批流程,查看对应标注节点错误信息'
|
||||
// case 3:
|
||||
// return '请检查扩展设置'
|
||||
default:
|
||||
return '未知错误'
|
||||
}
|
||||
}
|
||||
const showValidFinish = (success, err) => {
|
||||
console.log("处理完成")
|
||||
validResult.value.success = success
|
||||
validResult.value.finished = true
|
||||
validResult.value.title = success ? '校验完成 😀' : '校验失败 '
|
||||
validResult.value.desc = success ? '设置项校验成功,是否提交?' : err
|
||||
validResult.value.action = success ? '提 交' : '去修改'
|
||||
}
|
||||
const showValiding = () => {
|
||||
validResult.value = {
|
||||
errs: [],
|
||||
finished: false,
|
||||
success: false,
|
||||
title: '检查中...',
|
||||
action: '处理',
|
||||
desc: '正在检查设置项'
|
||||
}
|
||||
validStep.value = 0
|
||||
validOptions.value.forEach(op => {
|
||||
op.status = ''
|
||||
op.icon = ''
|
||||
op.description = ''
|
||||
})
|
||||
}
|
||||
const doAfter = () => {
|
||||
if (validResult.value.success) {
|
||||
doPublish()
|
||||
} else {
|
||||
activeSelect.value = validComponents.value[validStep.value]
|
||||
validVisible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const tarry = (node) => {
|
||||
if (node && node.id) {
|
||||
let newNode = {...node}
|
||||
newNode.children = null
|
||||
array.push(newNode)
|
||||
tarry(node.children)
|
||||
}
|
||||
}
|
||||
const stopTimer = () => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
}
|
||||
}
|
||||
const preview = () => {
|
||||
validateDesign()
|
||||
}
|
||||
//发布流程
|
||||
const publishProcess = () => {
|
||||
validateDesign()
|
||||
}
|
||||
// todo 提交数据
|
||||
const doPublish = () => {
|
||||
ElMessageBox.confirm('如果您只想预览请选择预览,确认发布后流程立即生效,是否继续?', '提示', {
|
||||
confirmButtonText: '发布',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
let design = processStore.getDesign()
|
||||
console.log(design)
|
||||
let template = {
|
||||
...design
|
||||
}
|
||||
console.log(template)
|
||||
addProcessDefinition(template).then(res => {
|
||||
if (res.code === 1000){
|
||||
ElMessage.success(res.msg)
|
||||
// this.$router.push("/formsPanel")
|
||||
}else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
}).catch(err => {
|
||||
ElMessage.error(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const changPan = (val) => {
|
||||
activeSelect.value = val
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-body {
|
||||
min-width: 980px;
|
||||
}
|
||||
|
||||
.el-step {
|
||||
.is-success {
|
||||
color: #2a99ff;
|
||||
border-color: #2a99ff;
|
||||
}
|
||||
}
|
||||
|
||||
.err-info {
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
|
||||
& > div {
|
||||
padding: 5px;
|
||||
margin: 2px 0;
|
||||
width: 220px;
|
||||
text-align: left;
|
||||
border-radius: 3px;
|
||||
background: rgb(242 242 242);
|
||||
}
|
||||
|
||||
i {
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: #e8e8e8;
|
||||
}
|
||||
|
||||
</style>
|
||||
762
src/views/workflow/process/ProcessTree.vue
Normal file
762
src/views/workflow/process/ProcessTree.vue
Normal file
@@ -0,0 +1,762 @@
|
||||
<script setup>
|
||||
import RootNode from './nodes/RootNode.vue'
|
||||
import END from './nodes/ProcessEndNode.vue'
|
||||
import APPROVAL from './nodes/ApprovalNode.vue'
|
||||
import CcNode from './nodes/CcNode.vue'
|
||||
import ConcurrentNode from './nodes/ConcurrentNode.vue'
|
||||
import ConditionNode from './nodes/ConditionNode.vue'
|
||||
import EmptyNode from './nodes/EmptyNode.vue'
|
||||
import TriggerNode from './nodes/TriggerNode.vue'
|
||||
import MergeNode from './nodes/MergeNode.vue'
|
||||
import DelayNode from './nodes/DelayNode.vue'
|
||||
import AddBranchNode from './nodes/AddBranchNode.vue'
|
||||
|
||||
import {defineExpose, h, render, ref} from 'vue'
|
||||
import DefaultProps from "./DefaultNodeProps"
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
|
||||
const processStore = useProcessStore()
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
},
|
||||
idName: {
|
||||
type: String,
|
||||
default: 'processTree'
|
||||
}
|
||||
})
|
||||
|
||||
const valid = ref(true)
|
||||
|
||||
let vNode = {}
|
||||
|
||||
const init = () => {
|
||||
processStore.init()
|
||||
initMapping(processStore.getProcess())
|
||||
// 定义类名(可忽略)
|
||||
let processTrees = getDomTree(h, "admin")
|
||||
vNode = []
|
||||
const dom = document.getElementById(props.idName);
|
||||
vNode = h('div', {class: {'_root': true}}, processTrees);
|
||||
render(vNode, dom)
|
||||
}
|
||||
|
||||
// 初始化map集合,以便数据整理
|
||||
const initMapping = (node) => {
|
||||
console.log("初始化数据", node)
|
||||
node.forEach(nodeItem => {
|
||||
processStore.nodeMap.set(nodeItem.id, nodeItem)
|
||||
processStore.parentMap.set(nodeItem.parentId, nodeItem)
|
||||
})
|
||||
}
|
||||
|
||||
const initHeaderBgc = (node) => {
|
||||
if (node.props && props.mode === 'preview') {
|
||||
let headerBgc = '#ff943e'
|
||||
if (processStore.runningList.value.includes(node.id)) {
|
||||
headerBgc = '#1e90ff'
|
||||
} else if (processStore.endList.value.includes(node.id)) {
|
||||
headerBgc = '#20b2aa'
|
||||
} else if (processStore.noTakeList.value.includes(node.id)) {
|
||||
headerBgc = '#909399'
|
||||
} else if (processStore.refuseList.value.includes(node.id)) {
|
||||
headerBgc = '#f56c6c'
|
||||
} else if (processStore.passList.value.includes(node.id)) {
|
||||
headerBgc = '#ff943e'
|
||||
}
|
||||
node.props.headerBgc = headerBgc
|
||||
}
|
||||
}
|
||||
|
||||
// 获取demo的树形结构
|
||||
const getDomTree = (h, id) => {
|
||||
let node = processStore.parentMap.get(id)
|
||||
if (!(node && node.id)) {
|
||||
return []
|
||||
}
|
||||
initHeaderBgc(node)
|
||||
if (isPrimaryNode(node)) {
|
||||
//普通业务节点
|
||||
let childDoms = getDomTree(h, node.id)
|
||||
decodeAppendDom(h, node, childDoms)
|
||||
return [h('div', {'class': {'primary-node': true}}, childDoms)];
|
||||
} else if (isBranchNode(node)) {
|
||||
let index = 0;
|
||||
//遍历分支节点,包含并行及条件节点
|
||||
let branchItems = node.branchs.map(branchNode => {
|
||||
//处理每个分支内子节点
|
||||
toMapping(branchNode)
|
||||
let childDoms = getDomTree(h, branchNode.id)
|
||||
decodeAppendDom(h, branchNode, childDoms, {level: index + 1, size: node.branchs.length})
|
||||
//插入4条横线,遮挡掉条件节点左右半边线条
|
||||
insertCoverLine(h, index, childDoms, node.branchs)
|
||||
//遍历子分支尾部分支
|
||||
index++;
|
||||
return h('div', {'class': {'branch-node-item': true}}, childDoms);
|
||||
})
|
||||
//插入添加分支/条件的按钮
|
||||
branchItems.unshift(h('div', {'class': {'add-branch-btn': true}}, [
|
||||
h(AddBranchNode, {
|
||||
mode: props.mode,
|
||||
size: 'small', round: true,
|
||||
value: `添加${isConditionNodes(node) ? '条件' : '分支'}`,
|
||||
onAddBranch: () => addBranchNode(node),
|
||||
}, [])
|
||||
]));
|
||||
let bchDom = [h('div', {'class': {'branch-node': true}}, branchItems)]
|
||||
//继续遍历分支后的节点
|
||||
let afterChildDoms = getDomTree(h, node.id)
|
||||
return [h('div', {}, [bchDom, afterChildDoms])]
|
||||
} else if (isMergeNode(node)) {
|
||||
//空节点,存在于分支尾部
|
||||
let childDoms = getDomTree(h, node.id)
|
||||
decodeAppendDom(h, node, childDoms)
|
||||
return [h('div', {'class': {'empty-node': true}}, childDoms)];
|
||||
} else if (isEmptyNode(node)) {
|
||||
//空节点,存在于分支尾部
|
||||
let childDoms = getDomTree(h, node.id)
|
||||
decodeAppendDom(h, node, childDoms)
|
||||
return [h('div', {'class': {'empty-node': true}}, childDoms)];
|
||||
} else if (isEndNode(node)) {
|
||||
let childDoms = getDomTree(h, node.id)
|
||||
decodeAppendEndDom(h, node, childDoms)
|
||||
return [h('div', {class: 'process-end'}, childDoms)];
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
const decodeAppendEndDom = (h, node, dom, props = {}) => {
|
||||
props.config = node
|
||||
dom.unshift(h(END, {
|
||||
id: node.id,
|
||||
key: node.id,
|
||||
}, []))
|
||||
}
|
||||
|
||||
//解码渲染的时候插入dom到同级
|
||||
const decodeAppendDom = (h, node, dom, nodeProps = {}) => {
|
||||
nodeProps.config = node
|
||||
dom.unshift(h(getNodeType(node), {
|
||||
mode: props.mode,
|
||||
...nodeProps,
|
||||
id: node.id,
|
||||
key: node.id,
|
||||
//定义事件,插入节点,删除节点,选中节点,复制/移动
|
||||
'onInsertNode': type => insertNode(type, node),
|
||||
'onDelNode': () => delNode(node),
|
||||
'onSelected': () => selectNode(node),
|
||||
'onCopy': () => copyBranch(node),
|
||||
'onLeftMove': () => branchMove(node, -1),
|
||||
'onRightMove': () => branchMove(node, 1)
|
||||
}, []))
|
||||
}
|
||||
|
||||
const getNodeType = (node) => {
|
||||
switch (node.type) {
|
||||
case "ROOT":
|
||||
return RootNode;
|
||||
case "APPROVAL":
|
||||
return APPROVAL;
|
||||
case "CC":
|
||||
return CcNode;
|
||||
case "CONDITION":
|
||||
return ConditionNode;
|
||||
case "CONCURRENT":
|
||||
return ConcurrentNode;
|
||||
case "DELAY":
|
||||
return DelayNode;
|
||||
case "MERGE":
|
||||
return MergeNode;
|
||||
case "EMPTY":
|
||||
return EmptyNode;
|
||||
case "TRIGGER":
|
||||
return TriggerNode;
|
||||
}
|
||||
}
|
||||
|
||||
//id映射到map,用来向上遍历
|
||||
const toMapping = (node) => {
|
||||
if (node && node.id) {
|
||||
let newNode = {
|
||||
...node
|
||||
}
|
||||
newNode.children = []
|
||||
processStore.nodeMap.set(newNode.id, newNode)
|
||||
}
|
||||
}
|
||||
|
||||
// 新增线条
|
||||
const insertCoverLine = (h, index, doms, branchs) => {
|
||||
if (index === 0) {
|
||||
//最左侧分支
|
||||
doms.unshift(h('div', {'class': {'line-top-left': true}}, []))
|
||||
doms.unshift(h('div', {'class': {'line-bot-left': true}}, []))
|
||||
}
|
||||
if (index === branchs.length - 1) {
|
||||
//最右侧分支
|
||||
doms.unshift(h('div', {'class': {'line-top-right': true}}, []))
|
||||
doms.unshift(h('div', {'class': {'line-bot-right': true}}, []))
|
||||
}
|
||||
}
|
||||
|
||||
const copyBranch = (node) => {
|
||||
let parentNode = processStore.nodeMap.get(node.parentId)
|
||||
let branchNode = deepCopy(node)
|
||||
branchNode.name = branchNode.name + '-copy'
|
||||
forEachNode(parentNode, branchNode, (parent, node) => {
|
||||
node.id = getRandomId()
|
||||
node.parentId = parent.id
|
||||
})
|
||||
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 0, branchNode)
|
||||
init()
|
||||
}
|
||||
|
||||
//移动分支节点
|
||||
const branchMove = (node, offset) => {
|
||||
let parentNode = processStore.nodeMap.get(node.parentId)
|
||||
let index = parentNode.branchs.indexOf(node)
|
||||
let branch = parentNode.branchs[index + offset]
|
||||
parentNode.branchs[index + offset] = parentNode.branchs[index]
|
||||
parentNode.branchs[index] = branch
|
||||
init()
|
||||
}
|
||||
|
||||
//判断是否为主要业务节点
|
||||
const isPrimaryNode = (node) => {
|
||||
return node &&
|
||||
(node.type === 'ROOT' || node.type === 'APPROVAL'
|
||||
|| node.type === 'CC' || node.type === 'DELAY'
|
||||
|| node.type === 'TRIGGER');
|
||||
}
|
||||
|
||||
//是否为分支节点
|
||||
const isBranchNode = (node) => {
|
||||
return node && (node.type === 'CONDITIONS' || node.type === 'CONCURRENTS');
|
||||
}
|
||||
|
||||
|
||||
//是否为空节点
|
||||
const isEmptyNode = (node) => {
|
||||
return node && (node.type === 'EMPTY')
|
||||
}
|
||||
const isEndNode = (node) => {
|
||||
return node && (node.type === 'END')
|
||||
}
|
||||
//是否为空节点
|
||||
const isMergeNode = (node) => {
|
||||
return node && (node.type === 'MERGE')
|
||||
}
|
||||
//是分支节点
|
||||
const isConditionNodes = (node) => {
|
||||
return node.type === 'CONDITIONS';
|
||||
}
|
||||
const isConditionNode = (node) => {
|
||||
return node.type === 'CONDITION';
|
||||
}
|
||||
//是分支节点
|
||||
const isBranchSubNode = (node) => {
|
||||
return node && (node.type === 'CONDITION' || node.type === 'CONCURRENT');
|
||||
}
|
||||
//时候并行节点
|
||||
const isConcurrentNodes = (node) => {
|
||||
return node.type === 'CONCURRENTS'
|
||||
}
|
||||
//时候并行节点
|
||||
const isConcurrentNode = (node) => {
|
||||
return node.type === 'CONCURRENT'
|
||||
}
|
||||
|
||||
//新增一个节点id
|
||||
const getRandomId = () => {
|
||||
let d = new Date().getTime()
|
||||
// x 是 0-9 或 a-f 范围内的一个32位十六进制数
|
||||
let id = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
let r = (d + Math.random() * 16) % 16 | 0
|
||||
d = Math.floor(d / 16)
|
||||
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16)
|
||||
})
|
||||
return 'node_' + id
|
||||
}
|
||||
//选中一个节点
|
||||
const selectNode = (node) => {
|
||||
processStore.setSelectedNode(node)
|
||||
if (!isConcurrentNode(node)) {
|
||||
emit('selectedNode', node)
|
||||
}
|
||||
}
|
||||
|
||||
//处理节点插入逻辑
|
||||
const insertNode = (type, parentNode) => {
|
||||
//插入新节点
|
||||
let id = getRandomId();
|
||||
updateParentId(id, parentNode.id)
|
||||
let children = {
|
||||
id: id,
|
||||
parentId: parentNode.id,
|
||||
type: type,
|
||||
}
|
||||
switch (type) {
|
||||
case 'APPROVAL':
|
||||
insertApprovalNode(children);
|
||||
break;
|
||||
case 'CC':
|
||||
insertCcNode(children);
|
||||
break;
|
||||
case 'DELAY':
|
||||
insertDelayNode(children);
|
||||
break;
|
||||
case 'TRIGGER':
|
||||
insertTriggerNode(children);
|
||||
break;
|
||||
case 'CONDITIONS':
|
||||
insertConditionsNode(children);
|
||||
break;
|
||||
case 'CONCURRENTS':
|
||||
insertConcurrentsNode(children);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
console.log('开始刷新')
|
||||
init()
|
||||
}
|
||||
/**
|
||||
* 更新父id
|
||||
* @param newId
|
||||
* @param oldId
|
||||
*/
|
||||
const updateParentId = (newId, oldId) => {
|
||||
processStore.getProcess().map(node => {
|
||||
if (node.parentId === oldId) {
|
||||
node.parentId = newId
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 审批人
|
||||
* @param parentNode
|
||||
*/
|
||||
const deepCopy = (obj) => {
|
||||
return JSON.parse(JSON.stringify(obj))
|
||||
}
|
||||
const insertApprovalNode = (parentNode) => {
|
||||
let node = {
|
||||
...parentNode,
|
||||
name: "审批人",
|
||||
props: deepCopy(DefaultProps.APPROVAL_PROPS)
|
||||
}
|
||||
processStore.addProcess(node)
|
||||
}
|
||||
/**
|
||||
* 抄送人
|
||||
* @param node
|
||||
*/
|
||||
const insertCcNode = (node) => {
|
||||
let newNode = {
|
||||
...node,
|
||||
name: "抄送人",
|
||||
props: deepCopy(DefaultProps.CC_PROPS)
|
||||
}
|
||||
processStore.addProcess(newNode)
|
||||
}
|
||||
/**
|
||||
* 延时处理
|
||||
* @param node
|
||||
*/
|
||||
const insertDelayNode = (node) => {
|
||||
let newNode = {
|
||||
...node,
|
||||
name: "延时处理",
|
||||
props: deepCopy(DefaultProps.DELAY_PROPS)
|
||||
}
|
||||
processStore.addProcess(newNode)
|
||||
}
|
||||
/**
|
||||
* 触发器
|
||||
* @param node
|
||||
*/
|
||||
const insertTriggerNode = (node) => {
|
||||
let newNode = {
|
||||
...node,
|
||||
name: "触发器",
|
||||
props: deepCopy(DefaultProps.TRIGGER_PROPS)
|
||||
}
|
||||
processStore.addProcess(newNode)
|
||||
}
|
||||
/**
|
||||
* 新增条件分支F
|
||||
* @param node
|
||||
*/
|
||||
const insertConditionsNode = (node) => {
|
||||
let newNode = {
|
||||
...node,
|
||||
name: "条件分支",
|
||||
branchs: [
|
||||
{
|
||||
id: getRandomId(),
|
||||
parentId: node.id,
|
||||
type: "CONDITION",
|
||||
props: deepCopy(DefaultProps.CONDITION_PROPS),
|
||||
name: "条件1",
|
||||
children: {}
|
||||
}, {
|
||||
id: getRandomId(),
|
||||
parentId: node.id,
|
||||
type: "CONDITION",
|
||||
props: deepCopy(DefaultProps.CONDITION_PROPS),
|
||||
name: "条件2",
|
||||
children: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
processStore.addProcess(newNode)
|
||||
let emptyNode = {
|
||||
id: getRandomId(),
|
||||
parentId: node.id,
|
||||
type: "EMPTY"
|
||||
}
|
||||
updateParentId(emptyNode.id, newNode.id)
|
||||
processStore.addProcess(emptyNode)
|
||||
}
|
||||
/**
|
||||
* 新增同步运行节点
|
||||
* @param node
|
||||
*/
|
||||
const insertConcurrentsNode = (node) => {
|
||||
let newNode = {
|
||||
...node,
|
||||
name: "并行分支",
|
||||
branchs: [
|
||||
{
|
||||
id: getRandomId(),
|
||||
parentId: node.id,
|
||||
type: "CONCURRENT",
|
||||
props: deepCopy(DefaultProps.CONDITION_PROPS),
|
||||
name: "分支1",
|
||||
children: {}
|
||||
}, {
|
||||
id: getRandomId(),
|
||||
parentId: node.id,
|
||||
type: "CONCURRENT",
|
||||
props: deepCopy(DefaultProps.CONDITION_PROPS),
|
||||
name: "分支2",
|
||||
children: {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
processStore.addProcess(newNode)
|
||||
let emptyNode = {
|
||||
id: getRandomId(),
|
||||
parentId: node.id,
|
||||
type: "MERGE"
|
||||
}
|
||||
updateParentId(emptyNode.id, newNode.id)
|
||||
processStore.addProcess(emptyNode)
|
||||
}
|
||||
|
||||
const addBranchNode = (node) => {
|
||||
if (node.branchs.length < 8) {
|
||||
node.branchs.push({
|
||||
id: getRandomId(),
|
||||
parentId: node.id,
|
||||
name: (isConditionNodes(node) ? '条件' : '分支') + (node.branchs.length + 1),
|
||||
props: isConditionNodes(node) ? deepCopy(DefaultProps.CONDITION_PROPS) : {},
|
||||
type: isConditionNodes(node) ? "CONDITION" : "CONCURRENT",
|
||||
children: {}
|
||||
})
|
||||
init()
|
||||
} else {
|
||||
ElMessage.warning("最多只能添加 8 项😥")
|
||||
}
|
||||
}
|
||||
|
||||
//删除当前节点
|
||||
const delNode = (node) => {
|
||||
//获取该节点的父节点
|
||||
let parentNode = processStore.nodeMap.get(node.parentId)
|
||||
if (parentNode) {
|
||||
if (isBranchNode(parentNode)) {
|
||||
delBranchNode(parentNode, node)
|
||||
} else {
|
||||
delNodeInDomChange(node.id, parentNode.id)
|
||||
}
|
||||
init()
|
||||
} else {
|
||||
ElMessage.warning("出现错误,找不到上级节点😥")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 从dom中删除
|
||||
* @param delId
|
||||
* @param parentId
|
||||
*/
|
||||
const delNodeInDomChange = (delId, parentId) => {
|
||||
updateParentId(parentId, delId)
|
||||
let delNode = processStore.nodeMap.get(delId)
|
||||
processStore.delProcess(delNode)
|
||||
init()
|
||||
}
|
||||
|
||||
|
||||
//删除分支
|
||||
const delBranchNode = (parentNode, node) => {
|
||||
let sunNode = processStore.parentMap.get(node.id)
|
||||
//判断当前节点下有没有字节点,有则需要提示
|
||||
if (sunNode) {
|
||||
ElMessageBox.confirm('当前分支下有子节点,是否继续?', '提示', {
|
||||
confirmButtonText: '确 定',
|
||||
cancelButtonText: '取 消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
//确认后进行子节点的操作
|
||||
delBranchSunNode(sunNode.id)
|
||||
doDelBranchNode(parentNode, node)
|
||||
})
|
||||
} else {
|
||||
// 没有直接开始删除
|
||||
doDelBranchNode(parentNode, node)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//删除分支节点
|
||||
const doDelBranchNode = (parentNode, node) => {
|
||||
//判断当前分时是否为2
|
||||
if (parentNode.branchs.length === 2) {
|
||||
let nodeList = [...parentNode.branchs]
|
||||
nodeList.splice(nodeList.indexOf(node), 1)
|
||||
//查看另外一个分支上是否有节点
|
||||
let sunNode = processStore.parentMap.get(nodeList[0].id)
|
||||
//有则需要放到主分支上
|
||||
if (sunNode) {
|
||||
//更改分支上第一个节点的父id
|
||||
updateParentId(parentNode.parentId, sunNode.parentId)
|
||||
//找到最后一个节点
|
||||
let lastNode = getLastBranchNode(sunNode.id)
|
||||
let emptyNode = processStore.parentMap.get(parentNode.id)
|
||||
//更新空节点下的第一个节点的id为当前分支最后一个节点
|
||||
updateParentId(lastNode.id, emptyNode.id)
|
||||
//删除分支的主节点
|
||||
delNodeInDom(parentNode)
|
||||
//删除分支的空节点
|
||||
delNodeInDom(emptyNode)
|
||||
} else {
|
||||
//没有则直接删除
|
||||
delEntireBranch(parentNode)
|
||||
}
|
||||
} else {
|
||||
parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1)
|
||||
}
|
||||
}
|
||||
|
||||
//获取最后一个节点
|
||||
const getLastBranchNode = (id) => {
|
||||
let node = processStore.parentMap.get(id)
|
||||
if (node) {
|
||||
return getLastBranchNode(node.id)
|
||||
} else {
|
||||
return processStore.nodeMap.get(id)
|
||||
}
|
||||
}
|
||||
|
||||
const delEntireBranch = (node) => {
|
||||
//删除分支节点和空节点
|
||||
let emptyNode = processStore.parentMap.get(node.id)
|
||||
delNodeInDomChange(node.id, node.parentId)
|
||||
delNodeInDomChange(emptyNode.id, emptyNode.parentId)
|
||||
}
|
||||
|
||||
|
||||
const delNodeInDom = (delNode) => {
|
||||
processStore.delProcess(delNode)
|
||||
init()
|
||||
}
|
||||
|
||||
|
||||
//删除分支的子节点
|
||||
const delBranchSunNode = (id) => {
|
||||
let node = processStore.parentMap.get(id)
|
||||
delNodeInDomChange(id)
|
||||
if (node) {
|
||||
delBranchSunNode(node.id)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//给定一个起始节点,遍历内部所有节点
|
||||
const forEachNode = (parent, node, callback) => {
|
||||
if (isBranchNode(node)) {
|
||||
callback(parent, node)
|
||||
forEachNode(node, node.children, callback)
|
||||
node.branchs.map(branchNode => {
|
||||
callback(node, branchNode)
|
||||
forEachNode(branchNode, branchNode.children, callback)
|
||||
})
|
||||
} else if (isPrimaryNode(node) || isEmptyNode(node) || isBranchSubNode(node)) {
|
||||
callback(parent, node)
|
||||
forEachNode(node, node.children, callback)
|
||||
}
|
||||
}
|
||||
|
||||
const validateProcess = () => {
|
||||
valid.value = true
|
||||
let err = []
|
||||
validate(err, processStore.getProcess())
|
||||
return err
|
||||
}
|
||||
|
||||
const validateNode = (err, node, nodeMap) => {
|
||||
let component = nodeMap.get(node.id)
|
||||
if (component !== undefined) {
|
||||
valid.value = component.exposed.validate(err)
|
||||
}
|
||||
}
|
||||
|
||||
const validateNodeList = ['APPROVAL', 'CC', 'CONDITION', 'DELAY', 'TRIGGER']
|
||||
|
||||
const analysisNode = (vNode, nodeMap) => {
|
||||
let children = vNode.children
|
||||
if (!children || children.length === 0) {
|
||||
return
|
||||
}
|
||||
for (let child of children) {
|
||||
let node = processStore.nodeMap.get(child.key)
|
||||
if (node !== undefined && node && validateNodeList.includes(node.type)) {
|
||||
nodeMap.set(node.id, child.component)
|
||||
} else {
|
||||
analysisNode(child, nodeMap)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//校验所有节点设置
|
||||
const validate = (err, nodeList) => {
|
||||
const nodeMap = new Map()
|
||||
analysisNode(vNode, nodeMap)
|
||||
nodeList.map(node => {
|
||||
if (isPrimaryNode(node)) {
|
||||
//校验条件节点
|
||||
validateNode(err, node, nodeMap)
|
||||
} else if (isBranchNode(node)) {
|
||||
node.branchs.map(branchNode => {
|
||||
//校验条件节点
|
||||
validateNode(err, branchNode, nodeMap)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
init,
|
||||
validateProcess
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
._root {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.process-end {
|
||||
width: 60px;
|
||||
margin: 0 auto;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
padding: 5px 10px;
|
||||
font-size: small;
|
||||
color: #747474;
|
||||
background-color: #f2f2f2;
|
||||
box-shadow: 0 0 10px 0 #bcbcbc;
|
||||
}
|
||||
|
||||
|
||||
.primary-node {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.branch-node {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
/*border-top: 2px solid #cccccc;
|
||||
border-bottom: 2px solid #cccccc;*/
|
||||
}
|
||||
|
||||
.branch-node-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
//background: #f5f6f6;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-top: 2px solid #cccccc;
|
||||
border-bottom: 2px solid #cccccc;
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: calc(50% - 1px);
|
||||
margin: auto;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
background-color: #CACACA;
|
||||
}
|
||||
|
||||
.line-top-left, .line-top-right, .line-bot-left, .line-bot-right {
|
||||
position: absolute;
|
||||
width: 50%;
|
||||
height: 4px;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.line-top-left {
|
||||
top: -2px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.line-top-right {
|
||||
top: -2px;
|
||||
right: -1px;
|
||||
}
|
||||
|
||||
.line-bot-left {
|
||||
bottom: -2px;
|
||||
left: -1px;
|
||||
}
|
||||
|
||||
.line-bot-right {
|
||||
bottom: -2px;
|
||||
right: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-branch-btn {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
|
||||
.add-branch-btn-el {
|
||||
z-index: 999;
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
}
|
||||
}
|
||||
|
||||
.empty-node {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
107
src/views/workflow/process/common/AvatarEllipsis.vue
Normal file
107
src/views/workflow/process/common/AvatarEllipsis.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div :class="{'line': row === 1, 'lines': row > 1}"
|
||||
:title="hoverTip ? content: null"
|
||||
:style="{'--row':row}">
|
||||
<slot name="pre"></slot>
|
||||
<div style="display: flex;flex-wrap: wrap;">
|
||||
<div v-for="(user,index) in userInfo" :key="index" class="avatar_name">
|
||||
<el-avatar size="large"
|
||||
:src="user.avatar"></el-avatar>
|
||||
<div v-if="user.icon"
|
||||
class="el-timeline-item__node" :style="{
|
||||
backgroundColor: user.color
|
||||
}">
|
||||
<el-icon v-if="user.icon" size="15" :class="user.class">
|
||||
<component :is="user.icon"/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<el-tooltip class="item" effect="dark" :content="user.name" placement="bottom-start">
|
||||
<span class="item_name">{{ user.name }}</span>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {Loading,Close,CircleCheckFilled,MoreFilled} from '@element-plus/icons-vue'
|
||||
import {defineProps} from "vue";
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
hoverTip: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
userInfo: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
})
|
||||
|
||||
const init = () => {
|
||||
for (let user of props.userInfo) {
|
||||
initUser(user)
|
||||
}
|
||||
}
|
||||
|
||||
const initUser = (user) => {
|
||||
let state = user.state
|
||||
//创建节点
|
||||
if (state === 'CREATE') {
|
||||
user["icon"] = CircleCheckFilled
|
||||
user["color"] = "#0bbd87"
|
||||
}
|
||||
//审批通过
|
||||
if (state === 'AGREE' || state === 'AUTO_PASS') {
|
||||
user["icon"] = CircleCheckFilled
|
||||
user["color"] = "#0bbd87"
|
||||
}
|
||||
//审批处理中
|
||||
if (state === 'RUNNING') {
|
||||
user["icon"] = Loading
|
||||
user["color"] = "#f78f5f"
|
||||
user["class"] = 'is-loading'
|
||||
}
|
||||
//拒绝后评论
|
||||
if (state === 'REFUSE') {
|
||||
user["icon"] = Close
|
||||
user["color"] = "#f56c6c"
|
||||
}
|
||||
if (state === 'PASS'){
|
||||
user["icon"] = MoreFilled
|
||||
user["color"] = "#c0c4cc"
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.avatar_name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-right: 1px;
|
||||
/*width: 45px;*/
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.el-timeline-item__node {
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
right: 1px;
|
||||
}
|
||||
|
||||
.item_name {
|
||||
width: 45px;
|
||||
text-align: center;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding-top: 3px;
|
||||
}
|
||||
</style>
|
||||
329
src/views/workflow/process/common/DeptPicker.vue
Normal file
329
src/views/workflow/process/common/DeptPicker.vue
Normal file
@@ -0,0 +1,329 @@
|
||||
<template>
|
||||
<el-dialog custom-class="custom-dialog" class="border" :border="false" closeFree width="600px"
|
||||
:title="title" :visible.sync="visible" v-model="visible" append-to-body :close-on-click-modal="true"
|
||||
:destroy-on-close="true">
|
||||
<div>
|
||||
<div class="picker">
|
||||
<div class="candidate" v-loading="loading">
|
||||
<div style="padding: 5px 8px;">
|
||||
<el-input v-model="filterText" style="width: 100%;" size="small"
|
||||
clearable placeholder="输入关键字进行过滤" prefix-icon="Search"/>
|
||||
</div>
|
||||
<!-- 部门 check-strictly-->
|
||||
<el-empty :image-size="100" description="似乎没有数据" v-show="deptList.length === 0"/>
|
||||
<el-scrollbar style="height:350px">
|
||||
<el-tree :data="deptList" ref="tree" :props="deptProps" empty-text="" node-key="value" default-expand-all
|
||||
:show-checkbox="showCheckbox" highlight-current :check-strictly="multiple===false"
|
||||
@check-change="handleCheckChange" @node-click="(node,check)=>handle(node,check)"
|
||||
:filter-node-method="filterNode">
|
||||
<template #default="{ node, data }">
|
||||
<el-icon style="margin-right: 5px">
|
||||
<FolderOpened/>
|
||||
</el-icon>
|
||||
{{ node.label }}
|
||||
</template>
|
||||
</el-tree>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="selected">
|
||||
<div class="count">
|
||||
<span>已选 {{ selectList.length }} 项</span>
|
||||
<span @click="clearSelected">清空</span>
|
||||
</div>
|
||||
<div class="org-items" style="height: 350px;">
|
||||
<el-empty :image-size="100" description="请点击左侧列表选择数据" v-show="selectList.length === 0"/>
|
||||
<div v-for="(selectItem, selectIndex) in selectList" :key="selectIndex" class="org-item">
|
||||
<i class="el-icon-folder-opened"></i>
|
||||
<span>{{ selectItem.label }}</span>
|
||||
<i class="el-icon-close" @click="noSelected(selectItem)" v-if="showCheckbox===false"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<el-button size="mini" @click="visible = false">取 消</el-button>
|
||||
<el-button size="mini" type="primary" @click="selectConfirm">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {getDepartmentTree} from "@/api/workflow/process-user";
|
||||
import {computed, defineExpose, defineProps} from "vue";
|
||||
import {ElMessage,ElMessageBox} from "element-plus";
|
||||
|
||||
const emit = defineEmits(["input"]);
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
multiple: { //是否多选
|
||||
default: true,
|
||||
type: Boolean
|
||||
},
|
||||
showCheckbox: { //是否显示左侧选择框
|
||||
default: true,
|
||||
type: Boolean
|
||||
}
|
||||
});
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const title = ref("请选择");
|
||||
const selectList = ref([]);
|
||||
const deptList = ref([]);
|
||||
const filterText = ref("");
|
||||
const tree = ref([]);
|
||||
const deptProps = reactive({
|
||||
value: "value",
|
||||
label: "label",
|
||||
children: "children"
|
||||
});
|
||||
watch(() => filterText, (newVal, oldVal) => {
|
||||
tree.value.filter(newVal);
|
||||
});
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
}
|
||||
,
|
||||
set(val) {
|
||||
emit("input", val);
|
||||
}
|
||||
});
|
||||
const getDepartmentTreeList = () => {
|
||||
//获取部门信息
|
||||
getDepartmentTree().then(res => {
|
||||
deptList.value = res.data;
|
||||
console.log("获取部门信息===========", res.data);
|
||||
});
|
||||
};
|
||||
const filterNode = (value, data) => {
|
||||
//通过关键字过滤树节点
|
||||
if (!value) return true;
|
||||
return data.deptName.indexOf(value) !== -1;
|
||||
};
|
||||
const showDeptPicker = () => {
|
||||
//用于弹开部门选择
|
||||
visible.value = true;
|
||||
};
|
||||
/**
|
||||
* 选中部门
|
||||
* @param data 选择的每个节点item
|
||||
* @param checked 是否选中
|
||||
*/
|
||||
const handleCheckChange = (data, checked) => {
|
||||
// 左侧有选择框
|
||||
if (props.showCheckbox) {
|
||||
// 左侧有选择框 + 多选
|
||||
if (props.multiple) {
|
||||
//不添加重复的数据到右边
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].value === data.value) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (checked) {
|
||||
if (data.children === undefined) {
|
||||
selectList.value.push(data);
|
||||
}
|
||||
} else if (data === "1") {
|
||||
tree.value.setCheckedKeys([]);
|
||||
selectList.value = [];
|
||||
}
|
||||
} else {// 左侧有选择框 + 单选
|
||||
//不添加重复的数据到右边
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].value === data.value) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (checked) {
|
||||
tree.value.setCheckedNodes([data]);
|
||||
// this.$refs.tree.setCheckedKeys([]);
|
||||
selectList.value = [data];
|
||||
} else if (data === "1") {
|
||||
selectList.value = [];
|
||||
tree.value.setCheckedKeys([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// _value.value = selectList.value
|
||||
};
|
||||
|
||||
//左侧没有选择框时,点击tree-item
|
||||
/**
|
||||
* 可以点击树节点deptName,进行选择
|
||||
* @param node 选择的每个节点item
|
||||
* @param check checked(checkbox选择框)是否选中
|
||||
*/
|
||||
const handle = (node, check) => {
|
||||
if (check.isLeaf !== false) {
|
||||
if (props.multiple) {
|
||||
//不添加重复的数据到右边
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].value === node.value) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
check.checked = true;
|
||||
selectList.value.push(node);
|
||||
} else {
|
||||
check.checked = true;
|
||||
selectList.value = [node];
|
||||
}
|
||||
}
|
||||
// _value.value = selectList.value
|
||||
};
|
||||
const noSelected = (selectItem) => {
|
||||
//左侧无选择框时,右侧显示×
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].value === selectItem.value) {
|
||||
selectList.value.splice(i, 1);
|
||||
tree.value.setCheckedKeys(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (props.showCheckbox) {
|
||||
// 左侧有选择框 + 单选
|
||||
if (props.multiple === false) {
|
||||
tree.value.setCheckedKeys([]);
|
||||
}
|
||||
}
|
||||
};
|
||||
//清空
|
||||
const clearSelected = () => {
|
||||
ElMessageBox.confirm("您确定要清空已选中的项?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
if (!props.showCheckbox) {
|
||||
selectList.value = [];
|
||||
} else {
|
||||
handleCheckChange("1");
|
||||
}
|
||||
});
|
||||
};
|
||||
//确定按钮
|
||||
const selectConfirm = () => {
|
||||
emit("ok", selectList.value);
|
||||
visible.value = false;
|
||||
};
|
||||
defineExpose({
|
||||
showDeptPicker
|
||||
});
|
||||
getDepartmentTreeList();
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$containWidth: 278px;
|
||||
|
||||
///deep/ .el-tree-node {
|
||||
// .is-leaf + .el-checkbox .el-checkbox__inner {
|
||||
// display: inline-block;
|
||||
// }
|
||||
//
|
||||
// .el-checkbox .el-checkbox__inner {
|
||||
// display: none;
|
||||
// }
|
||||
//}
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: 402px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
|
||||
.candidate {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.candidate, .selected {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: $containWidth;
|
||||
height: 400px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.selected {
|
||||
border-left: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
.count {
|
||||
width: calc(var($containWidth) - 20px);
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin-bottom: 5px;
|
||||
|
||||
& > span:nth-child(2) {
|
||||
float: right;
|
||||
color: #c75450;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.org-items {
|
||||
overflow-y: auto;
|
||||
height: 350px;
|
||||
|
||||
.el-icon-close {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
cursor: pointer;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.org-item {
|
||||
margin: 0 5px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
padding: 7px 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-scrollbar .el-scrollbar__wrap {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
float: right;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
</style>
|
||||
45
src/views/workflow/process/common/Ellipsis.vue
Normal file
45
src/views/workflow/process/common/Ellipsis.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<div :class="{'line': row === 1, 'lines': row > 1}"
|
||||
:title="hoverTip ? content: null"
|
||||
:style="{'--row':row}">
|
||||
<slot name="pre"></slot>
|
||||
{{content}}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps} from "vue";
|
||||
const props = defineProps({
|
||||
row: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
hoverTip:{
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
content:{
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.line{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.lines{
|
||||
display: -webkit-box;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: var(--row);
|
||||
-webkit-box-orient: vertical;
|
||||
margin-right: 10px;
|
||||
}
|
||||
</style>
|
||||
117
src/views/workflow/process/common/InsertButton.vue
Normal file
117
src/views/workflow/process/common/InsertButton.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<el-popover :visible="visible" placement="bottom-start" title="添加流程节点" width="350">
|
||||
<div class="node-select">
|
||||
<div @click="addApprovalNode">
|
||||
<el-icon style="color:rgb(255, 148, 62);">
|
||||
<Coordinate/>
|
||||
</el-icon>
|
||||
<span>审批人</span>
|
||||
</div>
|
||||
<div @click="addCcNode">
|
||||
<el-icon style="color:rgb(50, 150, 250);">
|
||||
<Promotion/>
|
||||
</el-icon>
|
||||
<span>抄送人</span>
|
||||
</div>
|
||||
<div @click="addConditionsNode">
|
||||
<el-icon style="color:rgb(21, 188, 131);">
|
||||
<Share/>
|
||||
</el-icon>
|
||||
<span>条件分支</span>
|
||||
</div>
|
||||
<div @click="addConcurrentsNode">
|
||||
<el-icon style="color:#718dff;">
|
||||
<Operation/>
|
||||
</el-icon>
|
||||
<span>并行分支</span>
|
||||
</div>
|
||||
<div @click="addDelayNode">
|
||||
<el-icon style="color:#f25643;">
|
||||
<Clock/>
|
||||
</el-icon>
|
||||
<span>延迟等待</span>
|
||||
</div>
|
||||
<div @click="addTriggerNode">
|
||||
<el-icon style="color:#15BC83;">
|
||||
<SetUp/>
|
||||
</el-icon>
|
||||
<span>触发器</span>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button :icon="Plus" slot="reference" type="primary" @click="visible = !visible" size="small" circle></el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {Plus } from '@element-plus/icons-vue'
|
||||
|
||||
const emit = defineEmits(['insertNode'])
|
||||
const visible = ref(false)
|
||||
|
||||
|
||||
const addApprovalNode = () => {
|
||||
emit('insertNode', "APPROVAL")
|
||||
disVisible()
|
||||
}
|
||||
|
||||
const addCcNode = () => {
|
||||
emit('insertNode', "CC")
|
||||
disVisible()
|
||||
}
|
||||
const addDelayNode = () => {
|
||||
emit('insertNode', "DELAY")
|
||||
disVisible()
|
||||
}
|
||||
const addConditionsNode = () => {
|
||||
emit('insertNode', "CONDITIONS")
|
||||
disVisible()
|
||||
}
|
||||
const addConcurrentsNode = () => {
|
||||
emit('insertNode', "CONCURRENTS")
|
||||
disVisible()
|
||||
}
|
||||
const addTriggerNode = () => {
|
||||
emit('insertNode', "TRIGGER")
|
||||
disVisible()
|
||||
}
|
||||
|
||||
const disVisible = () =>{
|
||||
visible.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.node-select {
|
||||
div {
|
||||
display: inline-block;
|
||||
margin: 5px 5px;
|
||||
cursor: pointer;
|
||||
padding: 10px 15px;
|
||||
border: 1px solid #F8F9F9;
|
||||
background-color: #F8F9F9;
|
||||
border-radius: 10px;
|
||||
width: 130px;
|
||||
position: relative;
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
left: 65px;
|
||||
top: 18px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 8px 2px #d6d6d6;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 25px;
|
||||
padding: 5px;
|
||||
border: 1px solid #dedfdf;
|
||||
border-radius: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
src/views/workflow/process/common/RoleItems.vue
Normal file
36
src/views/workflow/process/common/RoleItems.vue
Normal file
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div style="margin-top: 10px">
|
||||
<el-tag class="role-item" v-model="_value" v-for="(role, index) in _value" :key="index + '_role'"
|
||||
closable size="mini" @close="removeRoleItem(index)">
|
||||
{{ role.roleName }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, defineProps,defineEmits} from "vue";
|
||||
const emit =defineEmits(["input"])
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: []
|
||||
}
|
||||
})
|
||||
const _value = computed({
|
||||
get(){
|
||||
return props.modelValue||''
|
||||
},
|
||||
set(val) {
|
||||
emit("input", val)
|
||||
}
|
||||
})
|
||||
const removeRoleItem = (index) => {
|
||||
_value.value.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-item {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
276
src/views/workflow/process/common/RolePicker.vue
Normal file
276
src/views/workflow/process/common/RolePicker.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<el-dialog custom-class="custom-dialog" class="border" :border="false" closeFree width="600px" :title="title"
|
||||
:visible.sync="visible" v-model="visible" append-to-body :close-on-click-modal="true"
|
||||
:destroy-on-close="true">
|
||||
<div class="picker">
|
||||
<div class="candidate" v-loading="loading">
|
||||
<div class="role-header">
|
||||
<div>系统角色</div>
|
||||
</div>
|
||||
<div class="org-items">
|
||||
<el-empty :image-size="100" description="似乎没有数据" v-show="roleList.length === 0"/>
|
||||
<!-- 系统角色-->
|
||||
<div class="org-role-item" v-for="(roleItem , roleIndex) in roleList" :key="roleIndex"
|
||||
@click="selectChange(roleItem)">
|
||||
<el-checkbox v-model="roleItem.selected" v-if="showCheckbox" @change="selectChange(roleItem)"></el-checkbox>
|
||||
<i class="el-icon-user-solid" style="margin-left: 5px"></i>
|
||||
<span>{{ roleItem.roleName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="selected">
|
||||
<div class="count">
|
||||
<span>已选 {{ selectList.length }} 项</span>
|
||||
<span @click="clearSelected">清空</span>
|
||||
</div>
|
||||
<div class="org-items">
|
||||
<el-empty :image-size="100" description="请点击左侧列表选择数据" v-show="selectList.length === 0"/>
|
||||
<div v-for="(selectItem, selectIndex) in selectList" :key="selectIndex" class="org-role-item">
|
||||
<i class="el-icon-user-solid"></i>
|
||||
<span>{{ selectItem.roleName }}</span>
|
||||
<i class="el-icon-close" @click="noSelected(selectItem)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<el-button size="mini" @click="visible = false">取 消</el-button>
|
||||
<el-button size="mini" type="primary" @click="selectConfirm">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {getRole} from "@/api/workflow/process-user";
|
||||
import {computed, defineProps, defineExpose} from "vue";
|
||||
import {ElMessageBox} from "element-plus";
|
||||
|
||||
const emit = defineEmits();
|
||||
|
||||
const props = defineProps({
|
||||
selected: {
|
||||
default: () => {
|
||||
return [];
|
||||
},
|
||||
type: Array
|
||||
},
|
||||
multiple: { //是否多选
|
||||
default: true,
|
||||
type: Boolean
|
||||
},
|
||||
showCheckbox: { //是否显示左侧选择框
|
||||
default: true,
|
||||
type: Boolean
|
||||
}
|
||||
});
|
||||
const _value = computed({
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
emit("input", val);
|
||||
}
|
||||
});
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const title = ref("请选择");
|
||||
const selectList = ref([]);
|
||||
const roleList = ref([]);
|
||||
|
||||
const getRoleList = () => {
|
||||
//获取角色信息
|
||||
getRole().then(res => {
|
||||
roleList.value = res.data.map(function (val) {
|
||||
return {roleId: val.value, roleName: val.label};
|
||||
});
|
||||
});
|
||||
};
|
||||
const showRolePicker = () => {
|
||||
//用于弹出角色选择器
|
||||
getRoleList();
|
||||
visible.value = true;
|
||||
};
|
||||
const selectChange = (roleItem) => {
|
||||
//选中角色
|
||||
// 左侧有选择框
|
||||
if (props.showCheckbox) {
|
||||
// 左侧有选择框 + 多选
|
||||
if (props.multiple) {
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].roleId === roleItem.roleId) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (roleItem.selected) {
|
||||
selectList.value.push(roleItem);
|
||||
}
|
||||
} else {// 左侧有选择框 + 单选
|
||||
//用于左侧选择框选中取消,引起右侧数据变化
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].roleId === roleItem.roleId) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (roleItem.selected) {
|
||||
selectList.value = [roleItem];
|
||||
}
|
||||
for (let i = 0; i < roleList.value.length; i++) {
|
||||
for (let j = 0; j < selectList.value.length; j++) {
|
||||
if (roleList.value[i].roleId !== selectList.value[j].roleId) {
|
||||
roleList.value[i].selected = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {// 左侧没有选择框
|
||||
// 左侧没有选择框 + 多选
|
||||
if (props.multiple) {
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].roleId === roleItem.roleId) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
selectList.value.push(roleItem);
|
||||
} else {// 左侧没有选择框 + 单选
|
||||
selectList.value = [roleItem];
|
||||
}
|
||||
}
|
||||
_value.value = selectList.value;
|
||||
};
|
||||
const noSelected = (selectItem) => {
|
||||
//右侧的×
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].roleId === selectItem.roleId) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
selectItem.selected = false;
|
||||
};
|
||||
const clearSelected = () => {
|
||||
//清空
|
||||
ElMessageBox.confirm("您确定要清空已选中的项?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].selected === true) {
|
||||
selectList.value[i].selected = false;
|
||||
}
|
||||
}
|
||||
selectList.value = [];
|
||||
});
|
||||
};
|
||||
const selectConfirm = () => {
|
||||
//确定按钮
|
||||
emit("ok", selectList.value);
|
||||
visible.value = false;
|
||||
};
|
||||
defineExpose({
|
||||
showRolePicker
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$containWidth: 278px;
|
||||
.el-dialog__body {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
|
||||
.picker {
|
||||
height: 402px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
|
||||
.candidate {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.role-header {
|
||||
padding: 10px !important;
|
||||
margin-bottom: 5px;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
}
|
||||
}
|
||||
|
||||
.candidate, .selected {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: $containWidth;
|
||||
height: 400px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.selected {
|
||||
border-left: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
.count {
|
||||
width: calc(var($containWidth) - 20px);
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin-bottom: 5px;
|
||||
|
||||
& > span:nth-child(2) {
|
||||
float: right;
|
||||
color: #c75450;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
.org-items {
|
||||
overflow-y: auto;
|
||||
height: 350px;
|
||||
|
||||
.el-icon-close {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
cursor: pointer;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.org-role-item {
|
||||
padding: 7px 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin: 0 5px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
::-webkit-scrollbar {
|
||||
float: right;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: #efefef;
|
||||
}
|
||||
</style>
|
||||
381
src/views/workflow/process/common/UserPicker.vue
Normal file
381
src/views/workflow/process/common/UserPicker.vue
Normal file
@@ -0,0 +1,381 @@
|
||||
<template>
|
||||
<el-dialog custom-class="custom-dialog" class="border" :border="false" closeFree width="600px"
|
||||
:title="title" :visible.sync="visible" v-model="visible" append-to-body :close-on-click-modal="true"
|
||||
:destroy-on-close="true">
|
||||
<div>
|
||||
<div class="picker">
|
||||
<div class="candidate" v-loading="loading">
|
||||
<div style="padding: 5px 8px;">
|
||||
<el-input v-model="filterText" style="width: 100%;" size="small"
|
||||
clearable placeholder="输入关键字进行过滤" prefix-icon="el-icon-search"/>
|
||||
<div style="margin-top: 5px">
|
||||
<el-radio-group v-model="radio" size="mini" @change="radioChange">
|
||||
<el-radio-button :label="0">角色</el-radio-button>
|
||||
<el-radio-button :label="1">部门</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 人员选择 -->
|
||||
<el-empty :image-size="100" description="似乎没有数据" v-show="dataList.length === 0"/>
|
||||
<el-scrollbar style="height:317px">
|
||||
<div class="tree">
|
||||
<el-tree :data="dataList" ref="tree" :props="defaultProps" empty-text="" node-key="value"
|
||||
:default-expanded-keys="expandedKeys" lazy accordion
|
||||
@node-click="handleChange"
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<div class="tree-node">
|
||||
<div v-if="data.type === 0" style="display: flex;align-items: center;padding: 3px 0">
|
||||
<el-avatar :src="data.avatar"></el-avatar>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div v-else-if="data.type ===1">
|
||||
<el-icon>
|
||||
<UserFilled/>
|
||||
</el-icon>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-icon>
|
||||
<FolderOpened/>
|
||||
</el-icon>
|
||||
{{ node.label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tree>
|
||||
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<div class="selected">
|
||||
<div class="count">
|
||||
<span>已选 {{ selectList.length }} 项</span>
|
||||
<span @click="clearSelected">清空</span>
|
||||
</div>
|
||||
<div class="org-items" style="height: 350px;">
|
||||
<el-empty :image-size="100" description="请点击左侧列表选择数据" v-show="selectList.length === 0"/>
|
||||
<div v-for="(selectItem, selectIndex) in selectList" :key="selectIndex" class="org-item">
|
||||
<el-avatar :src="selectItem.avatar" style="margin-right: 5px;"></el-avatar>
|
||||
{{ selectItem.name}}
|
||||
<i class="el-icon-close" @click="noSelected(selectItem)"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<el-button size="mini" @click="visible = false">取 消</el-button>
|
||||
<el-button size="mini" type="primary" @click="selectConfirm">确 定</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {getUserTree} from "@/api/workflow/process-user";
|
||||
import {computed, defineProps, defineExpose} from "vue";
|
||||
import {ElMessageBox} from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
multiple: { //是否多选
|
||||
default: true,
|
||||
type: Boolean
|
||||
},
|
||||
showCheckbox: { //是否显示左侧选择框
|
||||
default: true,
|
||||
type: Boolean
|
||||
}
|
||||
});
|
||||
const radio = ref(0);
|
||||
const chooseId = ref(0);
|
||||
let selectItem = reactive({
|
||||
type: -1,
|
||||
value: "0"
|
||||
});
|
||||
const activeNames = ref(["1"]);
|
||||
const visible = ref(false);
|
||||
const loading = ref(false);
|
||||
const title = ref("请选择");
|
||||
const selectList = ref([]);
|
||||
const filterText = ref("");
|
||||
const dataList = ref([]);
|
||||
const tree = ref([]);
|
||||
const expandedKeys = ref([]);
|
||||
const defaultProps = {
|
||||
value: "value",
|
||||
label: "name",
|
||||
children: "children"
|
||||
// isLeaf: function (data, node) {
|
||||
// return data.type === 0;
|
||||
};
|
||||
const emit = defineEmits();
|
||||
|
||||
const _value = computed({
|
||||
get() {
|
||||
return props.value;
|
||||
},
|
||||
set(val) {
|
||||
emit("input", val);
|
||||
}
|
||||
});
|
||||
|
||||
watch(() => filterText, (newVal, oldVal) => {
|
||||
tree.value.filter(newVal);
|
||||
});
|
||||
|
||||
|
||||
const radioChange = (e) => {
|
||||
selectItem.type = -2;
|
||||
chooseId.value = 0;
|
||||
radio.value = e;
|
||||
expandedKeys.value = [];
|
||||
getList();
|
||||
};
|
||||
|
||||
const getList = () => {
|
||||
getUserTree(radio.value, chooseId.value).then(res => {
|
||||
// if (res.data) {
|
||||
if (selectItem.type === -1 || selectItem.type === -2) {//角色/部门
|
||||
dataList.value = res.data;
|
||||
} else if (selectItem.type === 1) {
|
||||
selectItem.children = res.data;
|
||||
if(chooseId.value!==0&&res.data.length===0){
|
||||
selectItem.children=[{
|
||||
type:1,
|
||||
name:'暂无数据'
|
||||
}]
|
||||
}
|
||||
} else if (selectItem.type === 2) {
|
||||
selectItem.children = res.data;
|
||||
}
|
||||
// }
|
||||
});
|
||||
};
|
||||
|
||||
const setData = (source) => {
|
||||
// for (let item of source) {
|
||||
// this.$set(item, "value", selectItem.value + "-" + item.id)
|
||||
// }
|
||||
return source;
|
||||
};
|
||||
|
||||
//通过关键字过滤树节点
|
||||
// filterNode(value, data) {
|
||||
// if (!value) return true;
|
||||
// return data.name.indexOf(value) !== -1;
|
||||
// },
|
||||
//用于用户选择
|
||||
const showUserPicker = () => {
|
||||
selectItem = {
|
||||
type: -1,
|
||||
value: "0"
|
||||
};
|
||||
dataList.value = [];
|
||||
selectList.value = []
|
||||
chooseId.value = 0;
|
||||
radio.value = 0;
|
||||
visible.value = true;
|
||||
expandedKeys.value = [];
|
||||
getList();
|
||||
};
|
||||
const handleChange = (item, data, node) => {
|
||||
//渲染子节点用户或部门及用户数据
|
||||
selectItem = item;
|
||||
if(expandedKeys.value.indexOf(item.value)==-1){
|
||||
expandedKeys.value.push(item.value);
|
||||
}else {
|
||||
return;
|
||||
}
|
||||
if (data.expanded === true) {
|
||||
if (item.type !== 0) {
|
||||
chooseId.value = item.id;
|
||||
getList();
|
||||
return;
|
||||
}
|
||||
}
|
||||
//仅选择用户
|
||||
if (item.avatar !== null) {
|
||||
if (props.multiple) {
|
||||
//不添加重复的数据到右边
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].id === item.id) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
selectList.value.push(item);
|
||||
} else {
|
||||
selectList.value = [item];
|
||||
}
|
||||
}
|
||||
// _value = selectList.value
|
||||
};
|
||||
|
||||
const noSelected = (selectItem) => {
|
||||
//右侧的x
|
||||
for (let i = 0; i < selectList.value.length; i++) {
|
||||
if (selectList.value[i].value === selectItem.value) {
|
||||
selectList.value.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
selectItem.selected = false;
|
||||
};
|
||||
const clearSelected = () => {
|
||||
//清空
|
||||
ElMessageBox.confirm("您确定要清空已选中的项?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
selectList.value = [];
|
||||
});
|
||||
};
|
||||
const selectConfirm = () => {
|
||||
//确定按钮
|
||||
emit("ok", selectList.value);
|
||||
dataList.value = []
|
||||
visible.value = false;
|
||||
};
|
||||
defineExpose({
|
||||
showUserPicker
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$containWidth: 278px;
|
||||
:deep(.tree) {
|
||||
.el-tree {
|
||||
.el-tree-node {
|
||||
.el-tree-node__children {
|
||||
.el-tree-node {
|
||||
.el-tree-node__content {
|
||||
height: 45px !important;
|
||||
|
||||
.tree-node {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-tree-node {
|
||||
.is-leaf + .el-checkbox .el-checkbox__inner {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.el-checkbox .el-checkbox__inner {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node {
|
||||
div {
|
||||
.el-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.el-dialog__body {
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: 402px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
|
||||
.candidate {
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.candidate, .selected {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: $containWidth;
|
||||
height: 400px;
|
||||
border: 1px solid #e8e8e8;
|
||||
}
|
||||
|
||||
.selected {
|
||||
border-left: none;
|
||||
right: 0;
|
||||
top: 0;
|
||||
|
||||
.count {
|
||||
width: calc(var($containWidth) - 20px);
|
||||
padding: 10px;
|
||||
display: inline-block;
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin-bottom: 5px;
|
||||
|
||||
& > span:nth-child(2) {
|
||||
float: right;
|
||||
color: #c75450;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.org-items {
|
||||
overflow-y: auto;
|
||||
height: 350px;
|
||||
|
||||
.el-icon-close {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
cursor: pointer;
|
||||
font-size: larger;
|
||||
}
|
||||
|
||||
.org-item {
|
||||
margin: 0 5px;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
padding: 7px 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
> span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.el-scrollbar .el-scrollbar__wrap {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
float: right;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 16px;
|
||||
background-color: #efefef;
|
||||
}
|
||||
|
||||
</style>
|
||||
402
src/views/workflow/process/config/ApprovalNodeConfig.vue
Normal file
402
src/views/workflow/process/config/ApprovalNodeConfig.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<el-form label-position="top" label-width="90px">
|
||||
<el-form-item label="⚙ 选择审批对象" prop="text" class="user-type">
|
||||
<el-radio-group v-model="nodeProps.assignedType">
|
||||
<el-radio v-for="item in approvalTypes" :label="item.type" :key="item.type">{{ item.name }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="nodeProps.assignedType === 'ASSIGN_USER'">
|
||||
<el-button size="mini" icon="Plus" type="primary" @click="showSysRolePicker" round>
|
||||
选择人员
|
||||
</el-button>
|
||||
<user-picker title="请选择系统角色" :multiple="false" ref="sysRolePicker" :v-model="assignedUser" @ok="selectedUser"/>
|
||||
<avatar-ellipsis :row="3" :user-info="assignedUser"/>
|
||||
</div>
|
||||
<div v-else-if="nodeProps.assignedType === 'SELF_SELECT'">
|
||||
<el-radio-group size="mini" v-model="nodeProps.selfSelect.multiple">
|
||||
<el-radio-button :label="false">自选一个人</el-radio-button>
|
||||
<el-radio-button :label="true">自选多个人</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-else-if="nodeProps.assignedType === 'LEADER_TOP'">
|
||||
<el-divider/>
|
||||
<el-form-item label="审批终点" prop="text" class="approve-end">
|
||||
<el-radio-group v-model="nodeProps.leaderTop.endCondition">
|
||||
<el-radio label="TOP">直到最上层主管</el-radio>
|
||||
<el-radio label="LEAVE">不超过发起人的</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="approve-end-leave" v-if="nodeProps.leaderTop.endCondition === 'LEAVE'">
|
||||
<span>第 </span>
|
||||
<el-input-number :min="1" :max="20" :step="1" size="mini" v-model="nodeProps.leaderTop.level"/>
|
||||
<span> 级主管</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-else-if="nodeProps.assignedType === 'LEADER'">
|
||||
<el-divider/>
|
||||
<el-form-item label="指定主管" prop="text">
|
||||
<span>发起人的第 </span>
|
||||
<el-input-number :min="1" :max="20" :step="1" size="mini"
|
||||
v-model="nodeProps.leader.level"></el-input-number>
|
||||
<span> 级主管</span>
|
||||
<div style="color: #409EFF; font-size: small;">👉 直接主管为 第 1 级主管</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-else-if="nodeProps.assignedType === 'ROLE'">
|
||||
<el-button size="mini" icon="Plus" type="primary" @click="showRolePicker()" round>
|
||||
选择系统角色
|
||||
</el-button>
|
||||
<role-picker title="请选择人员" :multiple="false" ref="rolePicker" :v-model="roleList" @ok="selectedRole"/>
|
||||
<role-items v-model="nodeProps.roleList"/>
|
||||
</div>
|
||||
<div v-else-if="nodeProps.assignedType === 'FORM_USER'">
|
||||
<el-form-item label="选择表单联系人项" prop="text" class="approve-end">
|
||||
<el-select style="width: 80%;" size="small" v-model="nodeProps.formUser" placeholder="请选择包含联系人的表单项" filterable>
|
||||
<el-option v-for="(op,i) in forms" :label="op.title" :value="op.id" :key="i"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-else>
|
||||
<span class="item-desc">发起人自己作为审批人进行审批</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-divider></el-divider>
|
||||
<el-form-item label="👤 审批人为空时" prop="text" class="line-mode">
|
||||
<el-radio-group v-model="nodeProps.nobody.handler">
|
||||
<el-radio label="TO_PASS">自动通过</el-radio>
|
||||
<el-radio label="TO_REFUSE">自动驳回</el-radio>
|
||||
<el-radio label="TO_ADMIN">转交审批管理员</el-radio>
|
||||
<el-radio label="TO_USER">转交到指定人员</el-radio>
|
||||
</el-radio-group>
|
||||
<div style="margin-top: 10px" v-if="nodeProps.nobody.handler === 'TO_USER'">
|
||||
<el-button size="mini" icon="Plus" type="primary" @click="showUserPicker()" round>
|
||||
选择人员
|
||||
</el-button>
|
||||
<user-picker title="请指定用户" :multiple="false" ref="toUserPicker" :v-model="nobodyAssignedUser"
|
||||
@ok="selectNoSetUser"/>
|
||||
<avatar-ellipsis :row="3" :user-info="nobodyAssignedUser"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<div v-if="showMode">
|
||||
<el-divider/>
|
||||
<el-form-item label="👩👦👦 多人审批时审批方式" prop="text" class="approve-mode">
|
||||
<el-radio-group v-model="nodeProps.mode">
|
||||
<el-radio label="NEXT">会签 (按选择顺序审批,每个人必须同意)</el-radio>
|
||||
<el-radio label="AND">会签(可同时审批,每个人必须同意)</el-radio>
|
||||
<el-radio label="OR">或签(有一人同意即可)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-divider>高级设置</el-divider>
|
||||
<el-form-item label="✍ 审批同意时是否需要签字" prop="text">
|
||||
<el-switch inactive-text="不用" active-text="需要" v-model="nodeProps.sign"></el-switch>
|
||||
<el-tooltip class="item" effect="dark" content="如果全局设置了需要签字,则此处不生效" placement="top-start">
|
||||
<i class="el-icon-question" style="margin-left: 10px; font-size: medium; color: #b0b0b1"></i>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
<el-form-item label="⏱ 审批期限(为 0 则不生效)" prop="timeLimit">
|
||||
<el-input style="width: 180px;" placeholder="时长" type="number"
|
||||
v-model="nodeProps.timeLimit.timeout.value">
|
||||
<el-select style="width: 75px;" v-model="nodeProps.timeLimit.timeout.unit" slot="append" placeholder="请选择" filterable>
|
||||
<el-option label="天" value="D"></el-option>
|
||||
<el-option label="小时" value="H"></el-option>
|
||||
</el-select>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="审批期限超时后执行" prop="level" v-if="nodeProps.timeLimit.timeout.value > 0">
|
||||
<el-radio-group v-model="nodeProps.timeLimit.handler.type">
|
||||
<el-radio label="PASS">自动通过</el-radio>
|
||||
<el-radio label="REFUSE">自动驳回</el-radio>
|
||||
<el-radio label="NOTIFY">发送提醒</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="nodeProps.timeLimit.handler.type === 'NOTIFY'">
|
||||
<div style="color:#409EEF; font-size: small">默认提醒当前审批人</div>
|
||||
<el-switch inactive-text="循环" active-text="一次" v-model="nodeProps.timeLimit.handler.notify.once"></el-switch>
|
||||
<span style="margin-left: 20px" v-if="!nodeProps.timeLimit.handler.notify.once">
|
||||
每隔
|
||||
<el-input-number :min="0" :max="10000" :step="1" size="mini"
|
||||
v-model="nodeProps.timeLimit.handler.notify.hour"/>
|
||||
小时提醒一次
|
||||
</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="🙅 如果审批被驳回 👇">
|
||||
<el-radio-group v-model="nodeProps.refuse.type">
|
||||
<el-radio label="TO_INITIAL">重新开始流程</el-radio>
|
||||
<el-radio label="TO_BEFORE">驳回到上级审批节点</el-radio>
|
||||
<el-radio label="TO_NODE">驳回到指定节点</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="nodeProps.refuse.type === 'TO_NODE'">
|
||||
<span>指定节点:</span>
|
||||
<el-select style="margin-left: 10px; width: 150px;" placeholder="选择跳转步骤" size="small"
|
||||
v-model="nodeProps.refuse.target" filterable>
|
||||
<el-option v-for="(node, index) in nodeOptions" :key="index" :label="node.name"
|
||||
:value="node.id"></el-option>
|
||||
</el-select>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="自定义监听器">
|
||||
<div slot="label">
|
||||
<span style="margin-left: 20px">使用自定义监听器: </span>
|
||||
<!-- <el-switch v-model="config.listener.state" @change="getListener"></el-switch>-->
|
||||
</div>
|
||||
<div v-if="config.listener.state">
|
||||
<div slot="label">
|
||||
<span style="margin-right: 10px">设置监听器</span>
|
||||
<el-button type="primary" @click="addListener(config.listener.list)" link> + 添加</el-button>
|
||||
</div>
|
||||
<div v-for="(listen, index) in config.listener.list" :key="index">
|
||||
<el-input v-if="listen.isSys" placeholder="监听器名称" :disabled="true" size="small" style="width: 100px;"
|
||||
v-model="listen.listenerName"/>
|
||||
<el-input v-if="!listen.isSys" placeholder="监听器名称" size="small" style="width: 100px;"
|
||||
v-model="listen.listenerName"/>
|
||||
<el-radio-group size="small" style="margin: 0 5px;" @change="typeChange(listen)" v-model="listen.isSys">
|
||||
<el-radio-button :label="true">内置</el-radio-button>
|
||||
<el-radio-button :label="false">自定义</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-select v-if="listen.isSys" style="width: 180px;" v-model="listen.listenerValue" size="small"
|
||||
@change="listenerOptionChange(listen)"
|
||||
placeholder="请选择表单字段" filterable>
|
||||
<el-option v-for="option in listenerOption" :key="option.value" :label="option.label"
|
||||
:value="option.value"/>
|
||||
</el-select>
|
||||
<!-- <el-input v-if="listen.isSys" placeholder="请设置字段值" size="small" v-model="listen.listenerValue" style="width: 180px;"/>-->
|
||||
<el-button v-if="!listen.isSys" type="primary" size="small" @click="settingListener(listen)" link>设置</el-button>
|
||||
<el-button @click="delListener(config.listener.list, index)"
|
||||
class="el-icon-delete" type="primary"
|
||||
style="margin-left: 5px; color: #c75450;" link/>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!--
|
||||
<el-dialog custom-class="custom-dialog" class="border" width="600px" title="定义监听器设置"
|
||||
append-to-body :close-on-click-modal="true"
|
||||
:destroy-on-close="true" :visible.sync="editListenShow">
|
||||
<el-form ref="listenerForm" label-position="left" label-width="100px" :rules="listenerRules">
|
||||
<el-form-item label="监听器名称" prop="listenerName" class="listen">
|
||||
<el-input placeholder="请设置监听器名称" size="small" v-model="selectListen.listenerName"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="监听器类型" prop="eventType" class="listen">
|
||||
<el-select placeholder="请选择监听器类型" @change="selectListen.eventType = []"
|
||||
v-model="selectListen.listenerType" filterable>
|
||||
<el-option :value="'1'" label="任务监听"/>
|
||||
<el-option :value="'2'" label="执行监听"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="事件类型" prop="listenerType" class="listen">
|
||||
<el-select multiple placeholder="请选择事件类型" v-model="selectListen.eventType" filterable>
|
||||
<el-option value="create" label="create"/>
|
||||
<el-option v-if="selectListen.listenerType === '2'" value="end" label="end"/>
|
||||
<el-option v-if="selectListen.listenerType === '2'" value="take" label="take"/>
|
||||
<el-option v-if="selectListen.listenerType === '1'" value="assignment" label="assignment"/>
|
||||
<el-option v-if="selectListen.listenerType === '1'" value="complete" label="complete"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="值类型" prop="listenerValueType" class="listen">
|
||||
<el-radio-group size="small" style="margin: 0 5px;" @change="listenerValueTypeChange"
|
||||
v-model="selectListen.listenerValueType">
|
||||
<el-radio-button :label="'0'">Java类</el-radio-button>
|
||||
<el-radio-button :label="'1'">表达式</el-radio-button>
|
||||
<el-radio-button :label="'2'">代理表达式</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="值" prop="listenerValue" class="listen">
|
||||
<el-input :placeholder="listenerValuePlaceholder" size="small" v-model="selectListen.listenerValue"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div slot="footer">
|
||||
<el-button size="mini" @click="editListenShow = false">取消</el-button>
|
||||
<el-button size="mini" type="primary" @click="editListenShow = false">确认</el-button>
|
||||
</div>
|
||||
</el-dialog>-->
|
||||
</template>
|
||||
<script setup>
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import UserPicker from '../common/UserPicker.vue'
|
||||
import RolePicker from '../common/RolePicker.vue'
|
||||
import AvatarEllipsis from '../common/AvatarEllipsis.vue'
|
||||
import RoleItems from "../common/RoleItems.vue";
|
||||
import {computed, defineProps} from 'vue'
|
||||
|
||||
const processStore = useProcessStore()
|
||||
|
||||
const toUserPicker = ref()
|
||||
const rolePicker = ref()
|
||||
const sysRolePicker = ref()
|
||||
const showOrgSelect = ref(false)
|
||||
const orgPickerSelected = ref([])
|
||||
const approvalTypes = reactive([
|
||||
{name: "指定人员", type: "ASSIGN_USER"},
|
||||
{name: "发起人自选", type: "SELF_SELECT"},
|
||||
{name: "连续多级主管", type: "LEADER_TOP"},
|
||||
{name: "主管", type: "LEADER"},
|
||||
{name: "角色", type: "ROLE"},
|
||||
{name: "发起人自己", type: "SELF"},
|
||||
{name: "表单内联系人", type: "FORM_USER"}
|
||||
])
|
||||
const listenerOption = ref([])
|
||||
const selectListen = ref({})
|
||||
const editListenShow = ref(false)
|
||||
const listenerValuePlaceholder = ref('请输入类路径')
|
||||
const listenerRules = ref({
|
||||
listenerName: [
|
||||
{required: true, message: '监听器名称不能为空', trigger: 'blur'}
|
||||
],
|
||||
eventType: [
|
||||
{required: true, message: '监听器类型不能为空', trigger: 'blur'},
|
||||
// {validator: checkLength,trigger:'blur'}
|
||||
],
|
||||
listenerType: [
|
||||
{required: true, message: '监听器类型不能为空', trigger: 'blur'},
|
||||
],
|
||||
listenerValue: [
|
||||
{required: true, message: '值不能为空', trigger: 'blur'}
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const nodeProps = computed(() => {
|
||||
return processStore.getSelectedNode().props;
|
||||
})
|
||||
|
||||
const assignedUser = computed({
|
||||
get() {
|
||||
return props.config.assignedUser || [];
|
||||
},
|
||||
set(val) {
|
||||
props.config.assignedUser = val
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const nobodyAssignedUser = computed({
|
||||
get() {
|
||||
return props.config.nobody.assignedUser || [];
|
||||
},
|
||||
set(val) {
|
||||
props.config.nobody.assignedUser = val
|
||||
}
|
||||
})
|
||||
|
||||
const roleList = computed({
|
||||
get() {
|
||||
return props.config.roleList || [];
|
||||
},
|
||||
set(val) {
|
||||
props.config.roleList = val
|
||||
}
|
||||
})
|
||||
watch(()=>props.config.roleList,(value)=>{
|
||||
roleList.value = value
|
||||
})
|
||||
|
||||
const forms = computed(() => {
|
||||
return processStore.getDesign().formItems.filter(f => {
|
||||
return f.name === "UserPicker";
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
const nodeOptions = computed(() => {
|
||||
let values = [];
|
||||
const excType = ["ROOT", "EMPTY", "CONDITION", "CONDITIONS", "CONCURRENT", "CONCURRENTS", "CC", "END", "TRIGGER"];
|
||||
processStore.nodeMap.forEach((v) => {
|
||||
if (excType.indexOf(v.type) === -1) {
|
||||
values.push({id: v.id, name: v.name});
|
||||
}
|
||||
});
|
||||
return values;
|
||||
})
|
||||
|
||||
|
||||
const showMode = computed(() => {
|
||||
let props = processStore.getSelectedNode().props;
|
||||
switch (props.assignedType) {
|
||||
case "ASSIGN_USER":
|
||||
return props.assignedUser.length > 0;
|
||||
case "SELF_SELECT":
|
||||
return props.selfSelect.multiple;
|
||||
case "LEADER_TOP":
|
||||
return props.formUser !== "";
|
||||
case "FORM_USER":
|
||||
case "ROLE":
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})
|
||||
//指定人员
|
||||
const showSysRolePicker = () => {
|
||||
sysRolePicker.value.showUserPicker()
|
||||
}
|
||||
//点击转交给指定人员
|
||||
const showUserPicker = () => {
|
||||
toUserPicker.value.showUserPicker()
|
||||
}
|
||||
//选择系统角色
|
||||
const showRolePicker = () => {
|
||||
rolePicker.value.showRolePicker()
|
||||
}
|
||||
const checkLength = (rule, value, callback) => {
|
||||
if (value.length === 0) {
|
||||
callback(new Error("事件类型不能为空!"))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
const selectNoSetUser = (select) => {
|
||||
let userInfoList = []
|
||||
for (let val of select) {
|
||||
let userInfo = {
|
||||
id: val.id,
|
||||
name: val.name,
|
||||
avatar: val.avatar,
|
||||
}
|
||||
userInfoList.push(userInfo)
|
||||
}
|
||||
nobodyAssignedUser.value = userInfoList
|
||||
}
|
||||
|
||||
|
||||
const selectedRole = (select) => {
|
||||
let roleInfoList = []
|
||||
for (let val of select) {
|
||||
let userInfo = {
|
||||
roleId: val.roleId,
|
||||
roleName: val.roleName
|
||||
}
|
||||
roleInfoList.push(userInfo)
|
||||
}
|
||||
roleList.value = roleInfoList
|
||||
}
|
||||
const selectedUser = (select) => {
|
||||
let userInfoList = []
|
||||
for (let val of select) {
|
||||
let userInfo = {
|
||||
id: val.id,
|
||||
name: val.name,
|
||||
avatar: val.avatar,
|
||||
}
|
||||
userInfoList.push(userInfo)
|
||||
}
|
||||
assignedUser.value = userInfoList
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
73
src/views/workflow/process/config/CcNodeConfig.vue
Normal file
73
src/views/workflow/process/config/CcNodeConfig.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<el-button size="mini" icon="Plus" type="primary" @click="selectUser" round>选择抄送人</el-button>
|
||||
<div class="option">
|
||||
<el-checkbox label="允许发起人添加抄送人" v-model="shouldAdd"></el-checkbox>
|
||||
</div>
|
||||
<!-- <org-items v-model="select"/>-->
|
||||
<avatar-ellipsis :row="3" :user-info="assignedUser"/>
|
||||
<user-picker title="请选择抄送人" multiple ref="userPicker" :v-model="assignedUser" @ok="selectedUser"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, defineProps} from 'vue'
|
||||
import UserPicker from "../common/UserPicker.vue";
|
||||
import AvatarEllipsis from "../common/AvatarEllipsis.vue";
|
||||
const userPicker=ref()
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
const shouldAdd = computed({
|
||||
get() {
|
||||
return props.config.shouldAdd || false
|
||||
},
|
||||
set(val) {
|
||||
props.config.shouldAdd = val
|
||||
}
|
||||
})
|
||||
const assignedUser = computed({
|
||||
get() {
|
||||
return props.config.assignedUser || []
|
||||
},
|
||||
set(val) {
|
||||
props.config.assignedUser = val
|
||||
}
|
||||
})
|
||||
const selectUser = () => {
|
||||
userPicker.value.showUserPicker()
|
||||
}
|
||||
const selectedUser = (select) => {
|
||||
let userInfoList = []
|
||||
for (let val of select) {
|
||||
let userInfo = {
|
||||
id: val.id,
|
||||
name: val.name,
|
||||
avatar: val.avatar,
|
||||
}
|
||||
userInfoList.push(userInfo)
|
||||
}
|
||||
assignedUser.value = userInfoList
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.option {
|
||||
color: #606266;
|
||||
margin-top: 20px;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: small;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
.org-item {
|
||||
margin: 5px;
|
||||
}
|
||||
</style>
|
||||
308
src/views/workflow/process/config/ConditionGroupItemConfig.vue
Normal file
308
src/views/workflow/process/config/ConditionGroupItemConfig.vue
Normal file
@@ -0,0 +1,308 @@
|
||||
<template>
|
||||
<div v-for="(group, index) in selectedNode.props.groups" :key="index + '_g'" class="group">
|
||||
<div class="group-header">
|
||||
<span class="group-name">条件组 {{ groupNames[index] }}</span>
|
||||
<div class="group-cp">
|
||||
<span>组内条件关系:</span>
|
||||
<el-switch v-model="group.groupType" active-color="#409EFF"
|
||||
inactive-color="#c1c1c1" active-value="AND" inactive-value="OR"
|
||||
active-text="且" inactive-text="或"/>
|
||||
</div>
|
||||
|
||||
<div class="group-operation">
|
||||
<el-popover placement="bottom" title="选择审批条件" width="300" trigger="click">
|
||||
<template #reference>
|
||||
<el-icon :size="18" class="group-icon">
|
||||
<Plus/>
|
||||
</el-icon>
|
||||
</template>
|
||||
<div>以下条件将决定具体的审批流程</div>
|
||||
<el-checkbox-group v-model="group.cids" value-key="id">
|
||||
<el-checkbox :label="condition.id" v-for="(condition, cindex) in conditionList" :key="condition.id"
|
||||
@change="conditionChange(cindex, group)">
|
||||
{{ condition.title }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-popover>
|
||||
<el-icon :size="18" class="group-icon" @click="delGroup(index)">
|
||||
<Close/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-content">
|
||||
<p v-if="group.conditions.length === 0">点击右上角 + 为本条件组添加条件</p>
|
||||
<div v-else>
|
||||
<el-form ref="condition-form">
|
||||
<!--构建表达式-->
|
||||
<el-form-item v-for="(condition, cindex) in group.conditions" :key="condition.id + '_' + cindex">
|
||||
<ellipsis slot="label" hover-tip :content="condition.title"/>
|
||||
<span v-if="condition.valueType === ValueType.string">
|
||||
<el-select placeholder="判断符" style="width: 120px;" v-model="condition.compare"
|
||||
@change="condition.value = []" filterable>
|
||||
<el-option label="等于" value="="></el-option>
|
||||
<el-option label="包含在" value="IN"></el-option>
|
||||
</el-select>
|
||||
<span v-if="isSelect(condition.id)" style="margin-left: 10px">
|
||||
<el-select v-if="condition.compare === 'IN'" style="width: 280px;" clearable multiple size="small"
|
||||
v-model="condition.value" placeholder="选择值" filterable>
|
||||
<el-option v-for="(option, oi) in getOptions(condition.id)" :key="oi" :label="option"
|
||||
:value="option"></el-option>
|
||||
</el-select>
|
||||
<el-select v-else style="width: 280px;" clearable size="small" v-model="condition.value[0]"
|
||||
placeholder="选择值" filterable>
|
||||
<el-option v-for="(option, oi) in getOptions(condition.id)" :key="oi" :label="option"
|
||||
:value="option"></el-option>
|
||||
</el-select>
|
||||
</span>
|
||||
<span v-else style="margin-left: 10px">
|
||||
<el-input v-if="condition.compare === '='" style="width: 280px;" placeholder="输入比较值"
|
||||
v-model="condition.value[0]"/>
|
||||
<el-select v-else style="width: 280px;" multiple clearable filterable allow-create size="small"
|
||||
v-model="condition.value" placeholder="输入可能包含的值"></el-select>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else-if="condition.valueType === ValueType.number">
|
||||
<el-select size="small" placeholder="判断符" style="width: 120px;" v-model="condition.compare" filterable>
|
||||
<el-option :label="exp.label" :value="exp.value" :key="exp.value" v-for="exp in explains"></el-option>
|
||||
</el-select>
|
||||
<span style="margin-left: 10px">
|
||||
<el-input style="width: 280px;" v-if="conditionValType(condition.compare) === 0"
|
||||
placeholder="输入比较值" type="number" v-model="condition.value[0]"/>
|
||||
<el-select style="width: 280px;" multiple filterable allow-create
|
||||
v-else-if="conditionValType(condition.compare) === 1"
|
||||
v-model="condition.value" placeholder="输入可能包含的值"></el-select>
|
||||
<span v-else>
|
||||
<el-input style="width: 130px;" type="number" placeholder="输入比较值"
|
||||
v-model="condition.value[0]"/>
|
||||
<span> ~
|
||||
<el-input style="width: 130px;" type="number" placeholder="输入比较值"
|
||||
v-model="condition.value[1]"/>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<span v-else-if="condition.valueType === ValueType.user">
|
||||
<span class="item-desc" style="margin-right: 20px">属于某部门 / 为某些人其中之一</span>
|
||||
<el-button size="mini" :icon="Plus" type="primary" @click="selectUser(condition.value, 'user')"
|
||||
round>选择人员/部门</el-button>
|
||||
<org-items :modelValue="users"/>
|
||||
</span>
|
||||
<span v-else-if="condition.valueType === ValueType.dept">
|
||||
<span class="item-desc" style="margin-right: 20px">为某部门 / 某部门下的部门</span>
|
||||
<el-button size="mini" :icon="Plus" type="primary" @click="selectUser(condition.value, 'dept')"
|
||||
round>选择部门</el-button>
|
||||
<org-items :modelValue="users"/>
|
||||
</span>
|
||||
<span v-else-if="condition.valueType === ValueType.date"></span>
|
||||
<el-icon class="delete-icon" @click="delSubCondition(group, cindex)"><Minus /></el-icon>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<org-picker title="请选择人员/部门" multiple ref="orgPicker" :v-model="users" @ok="selected"></org-picker>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import OrgPicker from "../common/UserPicker.vue";
|
||||
import OrgItems from "../common/RoleItems.vue";
|
||||
import {ValueType} from '@/views/workflow/form/ComponentsConfigExport.js'
|
||||
import {computed, ref} from 'vue'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import Ellipsis from '../common/Ellipsis.vue'
|
||||
import {Plus, Minus} from '@element-plus/icons-vue'
|
||||
|
||||
const processStore = useProcessStore()
|
||||
|
||||
const orgPicker = ref()
|
||||
const users = ref([])
|
||||
// const orgType = ref('user')
|
||||
const showOrgSelect = ref(false)
|
||||
const groupNames = ref(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'])
|
||||
const supportTypes = ref([ValueType.number, ValueType.string, ValueType.date, ValueType.dateRange, ValueType.dept, ValueType.user])
|
||||
const explains = ref(
|
||||
[
|
||||
{label: '等于', value: '='},
|
||||
{label: '大于', value: '>'},
|
||||
{label: '大于等于', value: '>='},
|
||||
{label: '小于', value: '<'},
|
||||
{label: '小于等于', value: '<='},
|
||||
{label: '包含在', value: 'IN'},
|
||||
{label: 'x < 值 < x', value: 'B'},
|
||||
{label: 'x ≤ 值 < x', value: 'AB'},
|
||||
{label: 'x < 值 ≤ x', value: 'BA'},
|
||||
{label: 'x ≤ 值 ≤ x', value: 'ABA'},
|
||||
]
|
||||
)
|
||||
|
||||
const selectedNode = computed(() => {
|
||||
//当前选择的节点
|
||||
return processStore.getSelectedNode()
|
||||
})
|
||||
|
||||
const conditionList = computed(() => {
|
||||
//条件数组
|
||||
//构造可用条件选项
|
||||
const conditionItems = []
|
||||
processStore.getDesign().formItems.forEach(item => filterCondition(item, conditionItems))
|
||||
if (conditionItems.length === 0 || conditionItems[0].id !== 'root') {
|
||||
conditionItems.unshift({id: 'root', title: '发起人', valueType: 'User'})
|
||||
}
|
||||
return conditionItems
|
||||
})
|
||||
|
||||
const isSelect = (processDefinitionKey) => {
|
||||
//判断是否被选择
|
||||
let form = processStore.getFormMap().get(processDefinitionKey)
|
||||
return (form && (form.name === 'SelectInput' || form.name === 'MultipleSelect'))
|
||||
}
|
||||
|
||||
const getOptions = (processDefinitionKey) => {
|
||||
//获取到对应的option选项
|
||||
return processStore.getFormMap().get(processDefinitionKey).props.options || []
|
||||
}
|
||||
|
||||
const conditionValType = (type) => {
|
||||
//条件类型选择
|
||||
switch (type) {
|
||||
case '=':
|
||||
case '>':
|
||||
case '>=':
|
||||
case '<':
|
||||
case '<=':
|
||||
return 0;
|
||||
case 'IN':
|
||||
return 1;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
const selectUser = (value, orgType) => {
|
||||
//选择用户,倒开组织选择器
|
||||
// orgType.value = orgType
|
||||
users.value = value
|
||||
orgPicker.value.showUserPicker()
|
||||
}
|
||||
|
||||
const filterCondition = (item, list) => {
|
||||
//从表单中过滤出可以选择的条件
|
||||
if (item.name === 'SpanLayout') {
|
||||
item.props.items.forEach(sub => filterCondition(sub, list))
|
||||
} else if (supportTypes.value.indexOf(item.valueType) > -1 && item.props.required) {
|
||||
list.push({title: item.title, id: item.id, valueType: item.valueType})
|
||||
}
|
||||
}
|
||||
|
||||
const selected = (selected) => {
|
||||
let userInfoList = []
|
||||
for (let val of selected) {
|
||||
let userInfo = {
|
||||
id: val.id,
|
||||
roleName: val.name
|
||||
}
|
||||
userInfoList.push(userInfo)
|
||||
}
|
||||
users.value = userInfoList
|
||||
//组织选择器的选中回调函数
|
||||
// users.value.length = 0
|
||||
// console.log('processStore.getAssignedUser()',processStore.getAssignedUser())
|
||||
processStore.getAssignedUser().forEach(u => users.value=userInfoList)
|
||||
}
|
||||
|
||||
const delGroup = (index) => {
|
||||
//删除条件组
|
||||
processStore.getSelectedNode().props.groups.splice(index, 1)
|
||||
}
|
||||
|
||||
const delSubCondition = (group, index) => {
|
||||
//删除
|
||||
group.cids.splice(index, 1)
|
||||
group.conditions.splice(index, 1)
|
||||
}
|
||||
|
||||
const conditionChange = (index, group) => {
|
||||
//条件组进行发生了改变
|
||||
//判断新增的
|
||||
group.cids.forEach(cid => {
|
||||
if (0 > group.conditions.findIndex(cd => cd.id === cid)) {
|
||||
//新增条件
|
||||
let condition = {...conditionList.value[index]}
|
||||
console.log('fs', condition, conditionList.value, index)
|
||||
condition.compare = '';
|
||||
condition.value = []
|
||||
group.conditions.push(condition)
|
||||
}
|
||||
})
|
||||
for (let i = 0; i < group.conditions.length; i++) {
|
||||
//去除没有选中的
|
||||
if (group.cids.indexOf(group.conditions[i].id) < 0) {
|
||||
group.conditions.splice(i, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group {
|
||||
margin-bottom: 20px;
|
||||
color: #5e5e5e;
|
||||
overflow: hidden;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e3e3e3;
|
||||
|
||||
.group-header {
|
||||
padding: 5px 10px;
|
||||
background: #e3e3e3;
|
||||
position: relative;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.group-cp {
|
||||
font-size: small;
|
||||
position: absolute;
|
||||
left: 100px;
|
||||
display: flex;
|
||||
top: 5px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.group-operation {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
|
||||
.group-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group-content {
|
||||
padding: 10px 5px;
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
font-size: small;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 12px;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.condition-title {
|
||||
display: block;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
141
src/views/workflow/process/config/ConditionNodeConfig.vue
Normal file
141
src/views/workflow/process/config/ConditionNodeConfig.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<el-form inline label-width="100px">
|
||||
<el-form-item label="调整优先级" prop="level">
|
||||
<el-popover placement="right" title="拖拽条件调整优先级顺序" width="250" trigger="click">
|
||||
<draggable style="width: 100%; min-height:25px" :list="prioritySortList" group="from"
|
||||
@end="onEnd"
|
||||
:options="sortOption">
|
||||
<template #item="{ index,element }">
|
||||
<div :class="{'drag-no-choose': true, 'drag-hover': element.id === selectedNode.id}">
|
||||
<ellipsis style="width: 80px;" hover-tip :content="element.name"/>
|
||||
<div>优先级 {{ index + 1 }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<template #reference>
|
||||
<el-button slot="reference">第{{ nowNodeLeave + 1 }}级</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<el-form-item label="条件组关系" label-width="150px">
|
||||
<el-switch v-model="selectedNode.props.groupsType" active-color="#409EFF"
|
||||
inactive-color="#c1c1c1" active-value="AND" inactive-value="OR"
|
||||
active-text="且" inactive-text="或">
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="条件组表达式">
|
||||
<el-input v-model="config.expression" placeholder="输入条件组关系表达式 &为与,|为或"/>
|
||||
<span class="item-desc">使用表达式构建复杂逻辑,例如: (A & B) | C</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div>
|
||||
<el-button type="primary" icon="Plus" style="margin: 0 15px 15px 0" round
|
||||
@click="addConditionGroup">
|
||||
添加条件组
|
||||
</el-button>
|
||||
<span class="item-desc">只有必填选项才能作为审批条件</span>
|
||||
</div>
|
||||
<group-item/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import draggable from 'vuedraggable';
|
||||
import Ellipsis from '../common/Ellipsis.vue'
|
||||
import {computed, defineProps, ref,defineEmits} from 'vue'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
const emit = defineEmits()
|
||||
|
||||
const processStore = useProcessStore()
|
||||
import GroupItem from "./ConditionGroupItemConfig.vue"
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: {}
|
||||
}
|
||||
})
|
||||
|
||||
const sortOption = reactive({
|
||||
animation: 300,
|
||||
chosenClass: 'choose',
|
||||
scroll: true,
|
||||
sort: true
|
||||
})
|
||||
|
||||
const selectedNode = computed(() => {
|
||||
//当前选择的节点
|
||||
return processStore.getSelectedNode()
|
||||
})
|
||||
|
||||
|
||||
//条件节点
|
||||
const prioritySortList = computed(() => {
|
||||
let node = processStore.nodeMap.get(processStore.getSelectedNode().parentId)
|
||||
if (node) {
|
||||
return node.branchs || []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
const nowNodeLeave = computed(() => {
|
||||
let node = processStore.nodeMap.get(processStore.getSelectedNode().parentId)
|
||||
let list = []
|
||||
if (node) {
|
||||
list = node.branchs || []
|
||||
}
|
||||
return list.indexOf(processStore.getSelectedNode())
|
||||
})
|
||||
|
||||
|
||||
const onEnd = () => {
|
||||
emit('initRender')
|
||||
}
|
||||
const addConditionGroup = (select) => {
|
||||
props.config.groups.push({
|
||||
cids: [],
|
||||
groupType: "OR",
|
||||
conditions: []
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// const selected = (select) => {
|
||||
// select.forEach(val => processStore.addAssignedUser(val))
|
||||
// }
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.choose {
|
||||
border-radius: 5px;
|
||||
margin-top: 2px;
|
||||
background: #f4f4f4;
|
||||
border: 1px dashed #1890FF !important;
|
||||
}
|
||||
|
||||
.drag-hover {
|
||||
color: #1890FF
|
||||
}
|
||||
|
||||
.drag-no-choose {
|
||||
cursor: move;
|
||||
background: #f8f8f8;
|
||||
border-radius: 5px;
|
||||
margin: 5px 0;
|
||||
height: 25px;
|
||||
line-height: 25px;
|
||||
padding: 5px 10px;
|
||||
border: 1px solid #ffffff;
|
||||
|
||||
div {
|
||||
display: inline-block;
|
||||
font-size: small !important;
|
||||
}
|
||||
|
||||
div:nth-child(2) {
|
||||
float: right !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
src/views/workflow/process/config/DelayNodeConfig.vue
Normal file
41
src/views/workflow/process/config/DelayNodeConfig.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="margin-bottom: 20px">
|
||||
<p class="item-desc">延时方式</p>
|
||||
<el-radio-group v-model="config.type">
|
||||
<el-radio-button label="FIXED">固定时长</el-radio-button>
|
||||
<el-radio-button label="AUTO">自动计算</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-if="config.type === 'FIXED'">
|
||||
<el-input style="width: 180px;" placeholder="时间单位" type="number" v-model="config.time">
|
||||
</el-input>
|
||||
<el-select style="width: 75px;" v-model="config.unit" slot="append" placeholder="请选择" filterable>
|
||||
<el-option label="天" value="D"></el-option>
|
||||
<el-option label="小时" value="H"></el-option>
|
||||
<el-option label="分钟" value="M"></el-option>
|
||||
</el-select>
|
||||
<span class="item-desc"> 后进入下一步</span>
|
||||
</div>
|
||||
<div class="item-desc" v-else>
|
||||
<el-time-picker value-format="HH:mm:ss" style="width: 150px;" v-model="config.dateTime" placeholder="任意时间点"></el-time-picker>
|
||||
<span class="item-desc"> 后进入下一步</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps} from 'vue'
|
||||
const props = defineProps({
|
||||
config:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
117
src/views/workflow/process/config/FormAuthorityConfig.vue
Normal file
117
src/views/workflow/process/config/FormAuthorityConfig.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<el-table :header-cell-style="{background:'#f5f6f6'}" :data="formPerms" border style="width: 100%" v-tabh>
|
||||
<el-table-column prop="title" show-overflow-tooltip label="表单字段">
|
||||
<template #default="scope">
|
||||
<span v-if="scope.row.required" style="color: #c75450"> * </span>
|
||||
<span>{{ scope.row.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column v-if="nodeType !== 'ROOT'" prop="readOnly" label="只读" width="80">
|
||||
<template slot="header" #header="scope">
|
||||
<el-radio label="R" v-model="permSelect" @change="allSelect('R')">只读</el-radio>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-radio v-model="scope.row.perm" label="R" :name="scope.row.id"></el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="editable" label="可编辑" width="90" v-if="nowNode.type !== 'CC'">
|
||||
<template slot="header" #header="scope">
|
||||
<el-radio label="E" v-model="permSelect" @change="allSelect('E')">可编辑</el-radio>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-radio v-model="scope.row.perm" label="E" :name="scope.row.id"></el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="hide" label="隐藏" width="80">
|
||||
<template slot="header" #header="scope">
|
||||
<el-radio label="H" v-model="permSelect" @change="allSelect('H')">隐藏</el-radio>
|
||||
</template>
|
||||
<template #default="scope">
|
||||
<el-radio v-model="scope.row.perm" label="H" :name="scope.row.id"></el-radio>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineProps, watch,computed} from "vue";
|
||||
|
||||
import {useProcessStore} from "@/stores/processStore.js";
|
||||
|
||||
const props = defineProps({
|
||||
nodeType: {
|
||||
type: String,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
const processStore = useProcessStore();
|
||||
|
||||
const tableData = ref([]);
|
||||
const isIndeterminate = ref(false);
|
||||
const permSelect = ref("");
|
||||
const checkStatus = reactive({
|
||||
readOnly: true,
|
||||
editable: false,
|
||||
hide: false
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
const init = () => {
|
||||
let oldPermMap = new Map()
|
||||
if (Array.isArray(formPerms.value) && formPerms.value.length){
|
||||
for (let item of formPerms.value) {
|
||||
oldPermMap.set(item.id,item)
|
||||
}
|
||||
}
|
||||
processStore.getSelectedNode().props.formPerms = [];
|
||||
formPermsLoad(oldPermMap, processStore.getDesign().formItems);
|
||||
};
|
||||
|
||||
const formPerms = computed(() => {
|
||||
return processStore.getSelectedNode().props.formPerms;
|
||||
});
|
||||
|
||||
const nowNode = computed(() => {
|
||||
return processStore.getSelectedNode();
|
||||
});
|
||||
|
||||
const formItems = computed(() => {
|
||||
return processStore.getDesign().formItems;
|
||||
});
|
||||
|
||||
const allSelect = (type) => {
|
||||
permSelect.value = type;
|
||||
formPerms.value.forEach(f => f.perm = type);
|
||||
};
|
||||
|
||||
|
||||
const formPermsLoad = (oldPermMap, forms) => {
|
||||
forms.forEach(form => {
|
||||
if (form.name === "SpanLayout") {
|
||||
formPermsLoad(oldPermMap, form.props.items);
|
||||
} else {
|
||||
//刷新名称
|
||||
let old = oldPermMap.get(form.id)
|
||||
if (old) {
|
||||
old.title = form.title;
|
||||
old.required = form.props.required;
|
||||
formPerms.value.push(old);
|
||||
} else {
|
||||
formPerms.value.push({
|
||||
id: form.id,
|
||||
title: form.title,
|
||||
required: form.props.required,
|
||||
perm: nowNode.type === "ROOT" ? "E" : "R"
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
init();
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
92
src/views/workflow/process/config/NodeConfig.vue
Normal file
92
src/views/workflow/process/config/NodeConfig.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<el-tabs v-model="active" v-if="visible && name && formConfig.length > 0">
|
||||
<el-tab-pane :label="name" name="properties">
|
||||
<component :is="com" :config="selectNode.props" @initRender="emit('initRender')"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="表单权限设置" name="permissions">
|
||||
<form-authority-config :node-type="selectNode.type"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<div v-else>
|
||||
<component :is="com" :config="selectNode.props" @initRender="emit('initRender')"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import FormAuthorityConfig from './FormAuthorityConfig.vue'
|
||||
import Approval from './ApprovalNodeConfig.vue'
|
||||
import Root from './RootConfig.vue'
|
||||
import Delay from './DelayNodeConfig.vue'
|
||||
import CcNode from './CcNodeConfig.vue'
|
||||
import Condition from './ConditionNodeConfig.vue'
|
||||
import Trigger from './TriggerNodeConfig.vue'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import {computed,defineEmits} from 'vue'
|
||||
const emit = defineEmits()
|
||||
|
||||
const processStore = useProcessStore()
|
||||
|
||||
const selectNode = computed(() => {
|
||||
return processStore.getSelectedNode()
|
||||
})
|
||||
|
||||
|
||||
const formConfig = computed(() => {
|
||||
return processStore.getDesign().formItems
|
||||
})
|
||||
|
||||
|
||||
const com = ref()
|
||||
const active = ref('properties')
|
||||
const visible = ref(false)
|
||||
|
||||
|
||||
const name = computed(()=>{
|
||||
switch (processStore.getSelectedNode().type) {
|
||||
case 'ROOT':
|
||||
return '设置发起人';
|
||||
case 'APPROVAL':
|
||||
return '设置审批人';
|
||||
case 'CC':
|
||||
return '设置抄送人';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const init = () => {
|
||||
console.log(processStore.getSelectedNode().type)
|
||||
switch (processStore.getSelectedNode().type) {
|
||||
case 'APPROVAL' :
|
||||
com.value = Approval;
|
||||
break;
|
||||
case 'ROOT' :
|
||||
com.value = Root;
|
||||
break;
|
||||
case 'DELAY' :
|
||||
com.value = Delay;
|
||||
break;
|
||||
case 'CC' :
|
||||
com.value = CcNode;
|
||||
break;
|
||||
case 'CONDITION' :
|
||||
com.value = Condition;
|
||||
break;
|
||||
case 'TRIGGER' :
|
||||
com.value = Trigger;
|
||||
break;
|
||||
}
|
||||
visible.value = false
|
||||
nextTick(() => {
|
||||
visible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
44
src/views/workflow/process/config/RootConfig.vue
Normal file
44
src/views/workflow/process/config/RootConfig.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="desc">选择能发起该审批的人员/部门,不选则默认开放给所有人</p>
|
||||
<el-button size="mini" @click="selectOrg" icon="el-icon-plus" type="primary" round>请选择</el-button>
|
||||
<org-items v-model="select"/>
|
||||
<org-picker title="请选择可发起本审批的人员/部门" multiple ref="orgPicker" :selected="select" @ok="selected"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import {computed, defineExpose} from 'vue'
|
||||
const props = defineProps({
|
||||
config:{
|
||||
type: Object,
|
||||
default: ()=>{
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const showOrgSelect = ref(false)
|
||||
const select = computed(()=>{
|
||||
return props.config.assignedUser
|
||||
|
||||
})
|
||||
|
||||
const selectOrg = () => {
|
||||
// this.$refs.orgPicker.show()
|
||||
}
|
||||
|
||||
const selected = (item) => {
|
||||
item.forEach(val => select.push(val))
|
||||
}
|
||||
|
||||
const removeOrgItem = (index) => {
|
||||
this.select.splice(index, 1)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
342
src/views/workflow/process/config/TriggerNodeConfig.vue
Normal file
342
src/views/workflow/process/config/TriggerNodeConfig.vue
Normal file
@@ -0,0 +1,342 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form label-position="top" label-width="90px">
|
||||
<el-form-item label="选择触发的动作" prop="text" class="user-type">
|
||||
<el-radio-group v-model="config.type">
|
||||
<el-radio label="WEBHOOK">发送网络请求</el-radio>
|
||||
<el-radio label="EMAIL">发送邮件</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div v-if="config.type === 'WEBHOOK'">
|
||||
<el-form-item label="请求地址" prop="text">
|
||||
<el-input placeholder="请输入URL地址" v-model="config.http.url">
|
||||
<el-select v-model="config.http.method" style="width: 85px;" slot="prepend" placeholder="URL" filterable>
|
||||
<el-option label="GET" value="GET"></el-option>
|
||||
<el-option label="POST" value="POST"></el-option>
|
||||
<el-option label="PUT" value="PUT"></el-option>
|
||||
<el-option label="DELETE" value="DELETE"></el-option>
|
||||
</el-select>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Header请求头" prop="text">
|
||||
<div slot="label">
|
||||
<span style="margin-right: 10px">Header请求头</span>
|
||||
<el-button type="primary" @click="addItem(config.http.headers)" link> + 添加</el-button>
|
||||
</div>
|
||||
<div v-for="(header, index) in config.http.headers" :key="index">
|
||||
-
|
||||
<el-input placeholder="参数名" style="width: 100px;" v-model="header.name"/>
|
||||
<el-radio-group v-model="header.isField">
|
||||
<el-radio-button :label="true">表单</el-radio-button>
|
||||
<el-radio-button :label="false">固定</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-select v-if="header.isField" style="width: 180px;" v-model="header.value"
|
||||
placeholder="请选择表单字段" filterable>
|
||||
<el-option v-for="form in forms" :key="form.id" :label="form.title" :value="form.title"></el-option>
|
||||
</el-select>
|
||||
<el-input v-else placeholder="请设置字段值" v-model="header.value" style="width: 180px;"/>
|
||||
<el-icon class="el-icon-delete" @click="delItem(config.http.headers, index)"
|
||||
style="margin-left: 5px; color: #c75450; cursor: pointer"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Header请求参数" prop="text">
|
||||
<div slot="label">
|
||||
<span style="margin-right: 10px">请求参数 </span>
|
||||
<el-button style="margin-right: 20px" type="primary" @click="addItem(config.http.params)" link> + 添加</el-button>
|
||||
<span>参数类型 - </span>
|
||||
<el-radio-group style="margin: 0 5px;" v-model="config.http.contentType">
|
||||
<el-radio-button label="JSON">json</el-radio-button>
|
||||
<el-radio-button label="FORM">form</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-for="(param, index) in config.http.params" :key="index">
|
||||
-
|
||||
<el-input placeholder="参数名" style="width: 100px;" v-model="param.name"/>
|
||||
<el-radio-group style="margin: 0 5px;" v-model="param.isField">
|
||||
<el-radio-button :label="true">表单</el-radio-button>
|
||||
<el-radio-button :label="false">固定</el-radio-button>
|
||||
</el-radio-group>
|
||||
<el-select v-if="param.isField" style="width: 180px;" v-model="param.value"
|
||||
placeholder="请选择表单字段" filterable>
|
||||
<el-option v-for="form in forms" :key="form.id" :label="form.title" :value="form.id"></el-option>
|
||||
</el-select>
|
||||
<el-input v-else placeholder="请设置字段值" v-model="param.value" style="width: 180px;"/>
|
||||
<el-icon class="el-icon-delete" @click="delItem(config.http.params, index)"
|
||||
style="margin-left: 5px; color: #c75450; cursor: pointer"/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="请求结果处理" prop="text">-->
|
||||
<div slot="label">
|
||||
<span>请求结果处理</span>
|
||||
<span style="margin-left: 20px">自定义脚本: </span>
|
||||
<el-switch v-model="config.http.handlerByScript"></el-switch>
|
||||
</div>
|
||||
<span class="item-desc" v-if="config.http.handlerByScript">
|
||||
<p>👉 返回值为 ture 则流程通过,为 false 则流程将被驳回</p>
|
||||
<p>👉 (上线注意)占不支持ES高级语法</p>
|
||||
<p>👉 (上线注意)不支持浏览器的内置函数</p>
|
||||
<!-- <div>支持函数-->
|
||||
<!-- <span style="color: dodgerblue">setFormByName(-->
|
||||
<!-- <span style="color: #939494">'表单字段名', '表单字段值'</span>-->
|
||||
<!-- )</span>-->
|
||||
<!-- 可改表单数据-->
|
||||
<!-- </div>-->
|
||||
</span>
|
||||
<span class="item-desc" v-else>👉 无论请求结果如何,均通过</span>
|
||||
<div v-if="config.http.handlerByScript">
|
||||
<div>
|
||||
<el-button @click="requestTestHandler">测试</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<span>请求成功😀:</span>
|
||||
<js-code-edit v-model="config.http.success" :editor-placeholder="'请输入js代码'"
|
||||
:editor-height="250" :tab-size="2"/>
|
||||
</div>
|
||||
<div>
|
||||
<span>请求失败😥:</span>
|
||||
<js-code-edit v-model="config.http.fail" :editor-placeholder="'请输入js代码'"
|
||||
:editor-height="250" :tab-size="2"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- </el-form-item>-->
|
||||
</div>
|
||||
<div v-else-if="config.type === 'EMAIL'">
|
||||
<el-form-item label="邮件主题" prop="text">
|
||||
<el-input placeholder="请输入邮件主题" v-model="config.email.subject"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="收件方" prop="text">
|
||||
<el-select style="width: 100%;" v-model="config.email.to" filterable multiple allow-create
|
||||
default-first-option placeholder="请输入收件人">
|
||||
<el-option v-for="sender in config.email.to" :key="sender" :label="sender" :value="sender"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="抄送方" prop="text">
|
||||
<el-select style="width: 100%;" v-model="config.email.cc" filterable multiple allow-create
|
||||
default-first-option placeholder="请输入收件人">
|
||||
<el-option v-for="item in config.email.cc" :key="item" :label="item" :value="item"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="邮件正文" prop="text">
|
||||
<el-input type="textarea" v-model="config.email.content" :rows="4"
|
||||
placeholder="邮件内容,支持变量提取表单数据 ${表单字段名} "></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {computed, defineProps} from 'vue'
|
||||
import {ElMessage, ElMessageBox} from 'element-plus'
|
||||
import axios from "axios";
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import JsCodeEdit from "@/components/codeEdit/JsCodeEdit.vue";
|
||||
|
||||
const processStore = useProcessStore()
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
const forms = computed(() => {
|
||||
return processStore.getDesign().formItems || []
|
||||
})
|
||||
|
||||
const addItem = (items) => {
|
||||
if (items.length > 0 && (items[items.length - 1].name.trim() === ''
|
||||
|| items[items.length - 1].value.trim() === '')) {
|
||||
ElMessage.warning("请完善之前项后在添加")
|
||||
return;
|
||||
}
|
||||
items.push({name: '', value: '', isField: true})
|
||||
}
|
||||
|
||||
|
||||
const delItem = (items, index) => {
|
||||
items.splice(index, 1)
|
||||
}
|
||||
//url规范性检查
|
||||
const restfulCheck = (url) => {
|
||||
const httpProtocolPattern = /^http:/;
|
||||
const httpsProtocolPattern = /^https:/;
|
||||
const restfulUrlPattern = /\/\w+\/\w+(\/\{[^}]+\})*/;
|
||||
if (httpProtocolPattern.test(url) || httpsProtocolPattern.test(url)) {
|
||||
return restfulUrlPattern.test(url);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
//是否含有动态参数
|
||||
const hasUrlParams = (url) => {
|
||||
const pattern = /{[^{}]+}/g;
|
||||
let match;
|
||||
while ((match = pattern.exec(url)) !== null) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
//获取到动态参数的名称
|
||||
const getDynamicParamNames = (url) => {
|
||||
const pattern = /{([^{}]+)}/g;
|
||||
let match;
|
||||
const paramNames = [];
|
||||
while ((match = pattern.exec(url)) !== null) {
|
||||
if (match[0].startsWith('{') && match[0].endsWith('}')) {
|
||||
const paramName = match[1];
|
||||
paramNames.push(paramName);
|
||||
}
|
||||
}
|
||||
return paramNames;
|
||||
}
|
||||
//替换rul动态参数
|
||||
const replaceDynamicParams = (url, params) => {
|
||||
const dynamicParamPattern = /{\s*(\w+)\s*}/g;
|
||||
return url.replace(dynamicParamPattern, (match, param) => {
|
||||
return params[param] || '0';
|
||||
});
|
||||
}
|
||||
//获取到参数值
|
||||
const getParamsValue = (params, paramName) => {
|
||||
for (let param of params) {
|
||||
if (param.name === paramName) {
|
||||
return param.value
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
//设置头部
|
||||
const setHeaders = (http) => {
|
||||
let headers = {}
|
||||
for (let header of http.headers) {
|
||||
if (header.isField) {
|
||||
ElMessage.error("测试只支持固定参数")
|
||||
return
|
||||
}
|
||||
if (header.name !== "" && header.value !== "") {
|
||||
this.$set(headers, header.name, header.value);
|
||||
}
|
||||
}
|
||||
if (http.contentType === "FORM") {
|
||||
this.$set(headers, "Content-Type", "multipart/form-data")
|
||||
} else {
|
||||
this.$set(headers, "Content-Type", "application/json")
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
//设置post和put参数
|
||||
const setPostAndPutParams = (http) => {
|
||||
let params = {}
|
||||
for (let param of http.params) {
|
||||
params[param.name] = param.value
|
||||
}
|
||||
return params;
|
||||
}
|
||||
//设置get和delete的参数
|
||||
const setGetAndDeleteParams = (http) => {
|
||||
let dynamicParams = []
|
||||
let url = http.url
|
||||
let hasParams = hasUrlParams(url)
|
||||
if (hasParams) {
|
||||
dynamicParams = getDynamicParamNames(url);
|
||||
let replaceParams = {}
|
||||
for (let paramsName of dynamicParams) {
|
||||
let value = getParamsValue(http.params, paramsName)
|
||||
if (null == value) {
|
||||
ElMessage.error(paramsName + '参数未设置')
|
||||
return;
|
||||
}
|
||||
replaceParams[paramsName] = value
|
||||
}
|
||||
url = replaceDynamicParams(url, replaceParams);
|
||||
}
|
||||
if (http.method === "DELETE") {
|
||||
return url;
|
||||
}
|
||||
let getParams = []
|
||||
for (let param of http.params) {
|
||||
if (dynamicParams.indexOf(param.name) === -1 && param.name !== '' && param.value !== '') {
|
||||
getParams.push(param.name + "=" + param.value)
|
||||
}
|
||||
}
|
||||
if (getParams.length > 0) {
|
||||
url += "?"
|
||||
for (let i = 0; i < getParams.length; i++) {
|
||||
if (i !== getParams.length - 1) {
|
||||
url += getParams[i] + "&";
|
||||
} else {
|
||||
url += getParams[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
//请求测试
|
||||
|
||||
const requestTestHandler = () => {
|
||||
let http = props.config.http;
|
||||
if (http.url == null || http.url === '') {
|
||||
ElMessage.error("请填写请求路径!")
|
||||
return
|
||||
}
|
||||
if (!restfulCheck(http.url)) {
|
||||
ElMessage.error("当前只支持 RESTful URL!")
|
||||
return
|
||||
}
|
||||
let headers = setHeaders(http)
|
||||
let request
|
||||
switch (http.method) {
|
||||
case "GET":
|
||||
case "DELETE":
|
||||
let url = setGetAndDeleteParams(http)
|
||||
if (null == url) {
|
||||
return;
|
||||
}
|
||||
request = axios.request({
|
||||
method: http.method,
|
||||
url: url,
|
||||
headers: headers,
|
||||
});
|
||||
break;
|
||||
case "POST":
|
||||
case "PUT":
|
||||
request = axios.request({
|
||||
method: http.method,
|
||||
url: http.url,
|
||||
headers: headers,
|
||||
data: setPostAndPutParams(http)
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
console.log("==================[测试打印内容]==================")
|
||||
request.then(res => {
|
||||
console.log(res)
|
||||
if (res.status === 200) {
|
||||
let data = res.data
|
||||
console.log(data)
|
||||
let successFun = eval("(false ||" + http.success + ")");
|
||||
let result = successFun(data);
|
||||
console.log(result, "成功函数执行的返回结果")
|
||||
} else {
|
||||
let failFun = eval("(false ||" + http.fail + ")");
|
||||
let result = failFun(res);
|
||||
console.log(result, "失败函数执行的返回结果")
|
||||
}
|
||||
}).finally(() => {
|
||||
console.log("==================[测试打印结束]==================")
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.item-desc {
|
||||
color: #939494;
|
||||
}
|
||||
</style>
|
||||
266
src/views/workflow/process/index.vue
Normal file
266
src/views/workflow/process/index.vue
Normal file
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="queryParams" inline class="query-form" ref="queryForm" @submit.prevent="getList">
|
||||
<el-form-item label="部署名称" prop="deploymentName">
|
||||
<el-input v-model="queryParams.deploymentName" placeholder="请输入部署名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="状态" prop="enable">-->
|
||||
<!-- <el-select v-model="queryParams.enable" placeholder="请选择状态" clearable filterable>-->
|
||||
<!-- <el-option-->
|
||||
<!-- v-for="dict in cacheStore.getDict('regular_enable')"-->
|
||||
<!-- :key="dict.value"-->
|
||||
<!-- :label="dict.label"-->
|
||||
<!-- :value="dict.value"-->
|
||||
<!-- />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getList" :icon="Search">搜索</el-button>
|
||||
<el-button type="primary" v-perm="['rapid:regular:add']" @click="handleAdd" :icon="Plus">新增</el-button>
|
||||
<el-button type="primary" @click="handleReset" :icon="Refresh" plain>重置</el-button>
|
||||
<el-button type="primary" v-perm="['rapid:regular:export']" @click="handleExport" :icon="Download" plain>导出
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="table">
|
||||
<el-table
|
||||
:data="list"
|
||||
row-key="id"
|
||||
:lazy="true"
|
||||
stripe
|
||||
ref="singleTable"
|
||||
v-loading="loading"
|
||||
@select="handleSelect"
|
||||
v-tabh
|
||||
>
|
||||
<el-table-column type="selection" width="30"/>
|
||||
<el-table-column label="序号" type="index" width="60" align="center"/>
|
||||
<el-table-column prop="deploymentName" label="部署名称" align="center"/>
|
||||
<el-table-column prop="version" label="版本" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag type="success">{{ scope.row.version }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注说明" align="center"/>
|
||||
<el-table-column prop="updateTime" label="更新时间" align="center"/>
|
||||
<el-table-column prop="state" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<point-tag dict-type="normal_disable" :value="scope.row.state"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="mini" v-perm="['rapid:regular:edit']"
|
||||
@click="handleEdit(scope.row.deploymentId)" link>编辑
|
||||
</el-button>
|
||||
<el-button type="primary" size="mini" v-perm="['rapid:regular:edit']"
|
||||
@click="viewHistoricalVersion(scope.row)" link>历史
|
||||
</el-button>
|
||||
<popover-delete :name="scope.row.version" :type="'版本'" :perm="['rapid:regular:del']"
|
||||
@delete="handleDelete(scope.row.deploymentId)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<paging :current-page="pageInfo.pageNum" :page-size="pageInfo.pageSize" :page-sizes="[10, 20, 30, 40,50]"
|
||||
:total="total" @changeSize="handleSizeChange" @goPage="handleCurrentChange"/>
|
||||
<el-dialog v-model="isVisited" title="历史" width="800px">
|
||||
<div class="table">
|
||||
<el-table
|
||||
:data="historyVersionList"
|
||||
row-key="id"
|
||||
:lazy="true"
|
||||
ref="singleTable"
|
||||
v-loading="loading"
|
||||
v-tabh
|
||||
>
|
||||
<el-table-column prop="deploymentName" label="部署名称" align="center"/>
|
||||
<el-table-column prop="version" label="版本" align="center">
|
||||
<template #default="scope">
|
||||
<el-tag type="success">{{ scope.row.version }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="state" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<point-tag dict-type="normal_disable" :value="scope.row.state"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="remark" label="备注说明" align="center"/>
|
||||
<el-table-column label="操作">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="mini"
|
||||
:style="scope.row.state=== '1' ?'color: red':''"
|
||||
@click="suspendOrActivateHistoryView(scope.row)" link>
|
||||
{{ scope.row.state === "1" ? "停用" : "启用" }}
|
||||
</el-button>
|
||||
<popover-delete :name="scope.row.version" :type="'版本'"
|
||||
@delete="handleDeleteHistoryVersion(scope.row.deploymentId)"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
activateProcessDefinition,
|
||||
getProcessDefinitionList,
|
||||
suspendProcessDefinition,
|
||||
getHistoryVersion,
|
||||
deleteHistoryVersion
|
||||
} from "@/api/workflow/process-definition.js";
|
||||
import {Search, Refresh, Delete, Plus, Edit, Download, Document} from '@element-plus/icons-vue'
|
||||
import {ElMessage, ElMessageBox} from "element-plus";
|
||||
import {useCacheStore} from '@/stores/cache.js'
|
||||
import PointTag from "@/components/PointTag.vue";
|
||||
import Paging from "@/components/pagination/index.vue";
|
||||
|
||||
const dictStore = useCacheStore()
|
||||
dictStore.setCacheKey(['normal_disable'])
|
||||
const router = useRouter()
|
||||
|
||||
//查询参数
|
||||
const queryParams = reactive({
|
||||
deploymentName: '',
|
||||
// startTime: null,
|
||||
// endTime: null,
|
||||
// state: undefined,
|
||||
})
|
||||
|
||||
//页面信息
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const loading = ref(true)
|
||||
const list = ref([])
|
||||
const queryForm = ref()
|
||||
const total = ref()
|
||||
const selectDefinition = ref(null)
|
||||
const historyVersionList = ref([])
|
||||
const singleTable = ref()
|
||||
const isVisited = ref(false)
|
||||
|
||||
//重置搜索
|
||||
const handleReset = () => {
|
||||
queryForm.value.resetFields()
|
||||
getList()
|
||||
}
|
||||
|
||||
|
||||
//获取数据
|
||||
const getList = async () => {
|
||||
let params = {
|
||||
...queryParams,
|
||||
...pageInfo
|
||||
}
|
||||
loading.value = true
|
||||
getProcessDefinitionList(params).then(res => {
|
||||
if (res.code === 1000) {
|
||||
list.value = res.data.rows
|
||||
total.value = res.data.total
|
||||
loading.value = false
|
||||
} else {
|
||||
ElMessage.error(res.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
router.push({
|
||||
path: '/workflow/process/edit',
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = (deploymentId) => {
|
||||
router.push({
|
||||
path: `/workflow/process/edit/${deploymentId}`,
|
||||
})
|
||||
}
|
||||
//查看历史版本
|
||||
const viewHistoricalVersion = (row) => {
|
||||
selectDefinition.value = row
|
||||
loading.value = true
|
||||
getHistoryVersion(row.processDefinitionKey).then(res => {
|
||||
loading.value = false;
|
||||
// console.log("历史版本数据==", res.data);
|
||||
res.data.forEach(row => {
|
||||
row.logo = JSON.parse(row.logo);
|
||||
});
|
||||
historyVersionList.value = res.data;
|
||||
}).catch(err => {
|
||||
ElMessage.error(err.response.data);
|
||||
});
|
||||
isVisited.value = true;
|
||||
}
|
||||
|
||||
//启用,挂起历史版本
|
||||
const suspendOrActivateHistoryView = (row) => {
|
||||
let tip = row.state === "1" ? " 您正在停用 [ " + row.deploymentName + " ] 流程,是否继续?" : " 您正在激活 [ " + row.deploymentName + " ] 流程,是否继续?";
|
||||
ElMessageBox.confirm(tip, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning"
|
||||
}).then(() => {
|
||||
// let processDefinitionId = row.processDefinitionId;
|
||||
if (row.state === "1") {
|
||||
//停用
|
||||
suspendProcessDefinition(row.processDefinitionId).then(res => {
|
||||
if (res.code === 1000) {
|
||||
viewHistoricalVersion(selectDefinition.value)
|
||||
ElMessage.success(res.msg);
|
||||
} else {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
//启用
|
||||
activateProcessDefinition(row.processDefinitionId).then(res => {
|
||||
if (res.code === 1000) {
|
||||
viewHistoricalVersion(selectDefinition.value)
|
||||
ElMessage.success(res.msg);
|
||||
} else {
|
||||
ElMessage.error(res.msg);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//删除工作流历史版本
|
||||
const handleDeleteHistoryVersion = (deploymentId) => {
|
||||
deleteHistoryVersion(deploymentId).then(res => {
|
||||
viewHistoricalVersion(selectDefinition.value)
|
||||
})
|
||||
}
|
||||
|
||||
//勾选table数据行的 Checkbox
|
||||
const handleSelect = async (selection, row) => {
|
||||
if (selection.length !== 0) {
|
||||
disabled.value = false
|
||||
id.value = row.id
|
||||
if (selection.length > 1) {
|
||||
const del_row = selection.shift();
|
||||
singleTable.value.toggleRowSelection(del_row, false);
|
||||
}
|
||||
} else {
|
||||
disabled.value = true
|
||||
}
|
||||
}
|
||||
|
||||
//切换每页显示条数
|
||||
const handleSizeChange = async (val) => {
|
||||
pageInfo.value.pageSize = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
//点击页码进行分页功能
|
||||
const handleCurrentChange = async (val) => {
|
||||
pageInfo.value.pageNum = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
getList()
|
||||
</script>
|
||||
27
src/views/workflow/process/nodes/AddBranchNode.vue
Normal file
27
src/views/workflow/process/nodes/AddBranchNode.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<el-button v-if="mode === 'design'" size="small" round class="add-branch-btn-el" @click="emit('addBranch')">{{value}}</el-button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {defineEmits,defineProps,computed} from "vue";
|
||||
const emit = defineEmits()
|
||||
const props = defineProps({
|
||||
value:{
|
||||
type:String,
|
||||
default: ""
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
|
||||
const viewer = computed(() => {
|
||||
return false;
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
200
src/views/workflow/process/nodes/ApprovalNode.vue
Normal file
200
src/views/workflow/process/nodes/ApprovalNode.vue
Normal file
@@ -0,0 +1,200 @@
|
||||
<template>
|
||||
<node :title="config.name" :show-error="showError" :content="content"
|
||||
:show-avatar="config.props.assignedType === 'ASSIGN_USER'" :user-info="assignedUser"
|
||||
:error-info="errorInfo"
|
||||
:select-user="selectUser"
|
||||
:mode="mode"
|
||||
@selected="emit('selected')" @delNode="emit('delNode')" @insertNode="type => emit('insertNode', type)"
|
||||
placeholder="请设置审批人" :header-bgc="headerBgc" :header-icon="Stamp"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Node from './Node.vue'
|
||||
import {computed, defineExpose} from 'vue'
|
||||
import {Stamp} from '@element-plus/icons-vue'
|
||||
const emit = defineEmits(['insertNode', 'selected', 'delNode'])
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
|
||||
const showError = ref(false)
|
||||
const errorInfo = ref('')
|
||||
|
||||
|
||||
const selectUser = computed(() => {
|
||||
return {
|
||||
show: props.config.props.assignedType === 'SELF_SELECT',
|
||||
multiple: props.config.props.selfSelect.multiple
|
||||
};
|
||||
})
|
||||
|
||||
const assignedUser = computed(() => {
|
||||
if (props.config.props.assignedType === 'SELF_SELECT') {
|
||||
props.config.props.assignedUser = []
|
||||
}
|
||||
return props.config.props.assignedUser;
|
||||
})
|
||||
|
||||
const headerBgc = computed(() => {
|
||||
if (props.mode === 'design' || props.mode === 'view') {
|
||||
return '#ff943e'
|
||||
} else {
|
||||
return props.config.props.headerBgc
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const content = computed(() => {
|
||||
const config = props.config.props
|
||||
switch (config.assignedType) {
|
||||
case "ASSIGN_USER":
|
||||
if (config.assignedUser.length > 0) {
|
||||
let texts = []
|
||||
config.assignedUser.forEach(org => texts.push(org.name))
|
||||
return String(texts).replaceAll(',', '、')
|
||||
} else {
|
||||
return '请指定审批人'
|
||||
}
|
||||
case "SELF":
|
||||
return '发起人自己'
|
||||
case "SELF_SELECT":
|
||||
return config.selfSelect.multiple ? '发起人自选多人' : '发起人自选一人'
|
||||
case "LEADER_TOP":
|
||||
return '多级主管依次审批'
|
||||
case "LEADER":
|
||||
return config.leader.level > 1 ? '发起人的第 ' + config.leader.level + ' 级主管' : '发起人的直接主管'
|
||||
case "FORM_USER":
|
||||
if (!config.formUser || config.formUser === '') {
|
||||
return '表单内联系人(未选择)'
|
||||
} else {
|
||||
// let text = getFormItemById(config.formUser)
|
||||
if (text && text.title) {
|
||||
return `表单(${text.title})内的人员`
|
||||
} else {
|
||||
return '该表单已被移除😥'
|
||||
}
|
||||
}
|
||||
case "ROLE":
|
||||
if (config.roleList.length > 0) {
|
||||
return config.roleList.map(role => {
|
||||
return role.roleName;
|
||||
}).join("、")
|
||||
} else {
|
||||
return '指定角色(未设置)'
|
||||
}
|
||||
default:
|
||||
return '未知设置项😥'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
//校验数据配置的合法性
|
||||
const validate = (err) => {
|
||||
try {
|
||||
console.log(props.config.props.assignedType)
|
||||
switch (props.config.props.assignedType) {
|
||||
case "ASSIGN_USER":
|
||||
showError.value = !validate_ASSIGN_USER(err);
|
||||
break;
|
||||
case "SELF":
|
||||
showError.value = !validate_SELF(err);
|
||||
break;
|
||||
case "SELF_SELECT":
|
||||
showError.value = !validate_SELF_SELECT(err);
|
||||
console.log(showError.value);
|
||||
break;
|
||||
case "LEADER_TOP":
|
||||
showError.value = !validate_LEADER_TOP(err);
|
||||
break;
|
||||
case "LEADER":
|
||||
showError.value = !validate_LEADER(err);
|
||||
break;
|
||||
case "FORM_USER":
|
||||
showError.value = !validate_FORM_USER(err);
|
||||
break;
|
||||
case "ROLE":
|
||||
showError.value = !validate_ROLE(err);
|
||||
break;
|
||||
default:
|
||||
showError.value = true
|
||||
err.push("未知设置项😥")
|
||||
break;
|
||||
}
|
||||
if (props.config.props.nobody.handler === 'TO_USER' && props.config.props.nobody.assignedUser.length === 0) {
|
||||
errorInfo.value = '审批人为空时, 转交给指定人员:【请指定一个具体的人】'
|
||||
err.push('审批人为空时, 转交给指定人员:【请指定一个具体的人】')
|
||||
showError.value = true
|
||||
}
|
||||
return showError
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const validate_ASSIGN_USER = (err) => {
|
||||
if (props.config.props.assignedUser.length > 0) {
|
||||
return true;
|
||||
} else {
|
||||
errorInfo.value = '请指定审批人员'
|
||||
err.push(`${props.config.name} 未指定审批人员`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
const validate_SELF_SELECT = (err) => {
|
||||
// if (!this.viewer) {
|
||||
// return true
|
||||
// }
|
||||
// let userInfo = this.$store.state.selectUserMap.get(this.config.id);
|
||||
// if (undefined !== userInfo && Array.isArray(userInfo) && userInfo.length > 0) {
|
||||
// return true;
|
||||
// }
|
||||
errorInfo.value = '请指定审批人员'
|
||||
err.push(`${props.config.name} 未指定审批人员`)
|
||||
return false;
|
||||
}
|
||||
const validate_LEADER_TOP = (err) => {
|
||||
return true;
|
||||
}
|
||||
const validate_LEADER = (err) => {
|
||||
return true;
|
||||
}
|
||||
const validate_ROLE = (err) => {
|
||||
if (props.config.props.roleList.length <= 0) {
|
||||
errorInfo.value = '请指定负责审批的系统角色'
|
||||
err.push(`${props.config.name} 未指定审批角色`)
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const validate_SELF = (err) => {
|
||||
return true;
|
||||
}
|
||||
const validate_FORM_USER = (err) => {
|
||||
if (props.config.props.formUser === '') {
|
||||
errorInfo.value = '请指定表单中的人员组件'
|
||||
err.push(`${props.config.name} 审批人为表单中人员,但未指定`)
|
||||
return false
|
||||
}
|
||||
return true;
|
||||
}
|
||||
const validate_REFUSE = (err) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
75
src/views/workflow/process/nodes/CcNode.vue
Normal file
75
src/views/workflow/process/nodes/CcNode.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<node :title="config.name" :show-error="showError" :select-user="selectUser" :mode="mode" :content="content" :node-id="config.id"
|
||||
:error-info="errorInfo" :show-avatar="true" :user-info="config.props.assignedUser"
|
||||
@selected="emit('selected')" @delNode="emit('delNode')" @insertNode="type => emit('insertNode', type)"
|
||||
placeholder="请设置抄送人" :header-bgc="headerBgc" :header-icon="Promotion"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Node from './Node.vue'
|
||||
import {defineProps, defineEmits, computed, defineExpose} from "vue";
|
||||
const emit = defineEmits(['insertNode', 'selected', 'delNode'])
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
const showError = ref(false)
|
||||
const errorInfo = ref('')
|
||||
import {Promotion} from '@element-plus/icons-vue'
|
||||
|
||||
const headerBgc = computed(() => {
|
||||
if (props.mode === 'design' || props.mode === 'view') {
|
||||
return '#3296fa'
|
||||
} else {
|
||||
return props.config.props.headerBgc
|
||||
}
|
||||
})
|
||||
|
||||
const selectUser = computed(() => {
|
||||
return {
|
||||
show: props.config.props.assignedType !== 'ASSIGN_USER' && props.config.props.shouldAdd,
|
||||
multiple: true
|
||||
};
|
||||
})
|
||||
|
||||
const content = computed(() => {
|
||||
if (props.config.props.shouldAdd) {
|
||||
return '由发起人指定'
|
||||
} else if (props.config.props.assignedUser.length > 0) {
|
||||
let texts = []
|
||||
props.config.props.assignedUser.forEach(org => texts.push(org.name))
|
||||
return String(texts).replaceAll(',', '、')
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
//校验数据配置的合法性
|
||||
const validate = (err) => {
|
||||
showError.value = false
|
||||
if (props.config.props.shouldAdd) {
|
||||
showError.value = false
|
||||
} else if (props.config.props.assignedUser.length === 0) {
|
||||
showError.value = true
|
||||
errorInfo.value = '请选择需要抄送的人员'
|
||||
}
|
||||
if (showError.value) {
|
||||
err.push(`抄送节点 ${props.config.name} 未设置抄送人`)
|
||||
}
|
||||
return !showError.value
|
||||
}
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
195
src/views/workflow/process/nodes/ConcurrentNode.vue
Normal file
195
src/views/workflow/process/nodes/ConcurrentNode.vue
Normal file
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="node">
|
||||
<!-- 并行分支选择后右侧出现操作面板,占时不需要 <div class="node-body" @click="emit('selected')">-->
|
||||
<div v-if="designState" class="node-body" @click="emit('selected')">
|
||||
<div class="node-body-left" @click.stop="emit('leftMove')" v-if="level > 1">
|
||||
<el-icon>
|
||||
<ArrowLeftBold/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="node-body-main">
|
||||
<div class="node-body-main-header">
|
||||
<span class="title">
|
||||
<el-icon :size="15">
|
||||
<Operation/>
|
||||
</el-icon>
|
||||
<ellipsis class="name" hover-tip :content="config.name ? config.name:('并行任务' + level)"/>
|
||||
</span>
|
||||
<span class="option">
|
||||
<el-tooltip effect="dark" content="复制分支" placement="top">
|
||||
<el-icon @click.stop="emit('copy')" :size="20">
|
||||
<CopyDocument/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-icon @click.stop="emit('delNode')" :size="20">
|
||||
<CloseBold/>
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-body-main-content">
|
||||
<span>并行任务(同时进行)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-body-right" @click.stop="emit('rightMove')" v-if="level < size && designState ">
|
||||
<el-icon>
|
||||
<ArrowRightBold/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-footer">
|
||||
<div class="btn" :style="(designState ? '' : 'height:0px')">
|
||||
<insert-button v-if="designState" @insertNode="type => emit('insertNode', type)"></insert-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!--并行节点-->
|
||||
<script setup>
|
||||
import InsertButton from '../common/InsertButton.vue'
|
||||
import {Operation, ArrowRightBold, ArrowLeftBold, CopyDocument, CloseBold, Warning} from '@element-plus/icons-vue'
|
||||
import Ellipsis from '../common/Ellipsis.vue'
|
||||
import {defineProps, defineEmits} from "vue";
|
||||
|
||||
const emit = defineEmits(['insertNode'])
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
//条件数
|
||||
size: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
const designState = computed(()=>{
|
||||
return props.mode === 'design'
|
||||
})
|
||||
const designStart = () => {
|
||||
// return this.$store.state.diagramMode === 'design'
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
|
||||
.node {
|
||||
padding: 30px 55px 0;
|
||||
//width: 220px;
|
||||
width: 320px;
|
||||
|
||||
.node-body {
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
min-height: 80px;
|
||||
max-height: 120px;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
box-shadow: 0px 0px 5px 0px #d8d8d8;
|
||||
|
||||
&:hover {
|
||||
.node-body-left, .node-body-right {
|
||||
i {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.node-body-main {
|
||||
.option {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
|
||||
box-shadow: 0px 0px 3px 0px;
|
||||
}
|
||||
|
||||
.node-body-left, .node-body-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #ececec;
|
||||
}
|
||||
}
|
||||
|
||||
.node-body-left {
|
||||
color: #888888;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.node-body-right {
|
||||
color: #888888;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.node-body-main {
|
||||
position: absolute;
|
||||
width: 188px;
|
||||
left: 17px;
|
||||
display: inline-block;
|
||||
|
||||
.node-body-main-header {
|
||||
padding: 10px 0px 5px;
|
||||
font-size: xx-small;
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
color: #718dff;
|
||||
|
||||
.name {
|
||||
display: inline-block;
|
||||
height: 14px;
|
||||
width: 130px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.option {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
display: none;
|
||||
font-size: medium;
|
||||
|
||||
i {
|
||||
color: #888888;
|
||||
padding: 0 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-body-main-content {
|
||||
padding: 6px;
|
||||
color: #656363;
|
||||
font-size: 14px;
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 55%;
|
||||
right: 10px;
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
299
src/views/workflow/process/nodes/ConditionNode.vue
Normal file
299
src/views/workflow/process/nodes/ConditionNode.vue
Normal file
@@ -0,0 +1,299 @@
|
||||
<template>
|
||||
<div :class="{'node': true, 'node-error-state': showError}">
|
||||
<div :class="{'node-body': true, 'error': showError}">
|
||||
<div class="node-body-left" @click="emit('leftMove')" v-if="level > 1 && designState">
|
||||
<el-icon>
|
||||
<ArrowLeftBold/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="node-body-main" @click="emit('selected')">
|
||||
<div class="node-body-main-header">
|
||||
<ellipsis class="title" hover-tip :content="config.name ? config.name : ('条件' + level)"/>
|
||||
<span class="level">优先级{{ level }}</span>
|
||||
<span class="option" v-if="designState">
|
||||
<el-tooltip effect="dark" content="复制条件" placement="top">
|
||||
<el-icon @click.stop="emit('copy')" :size="20">
|
||||
<CopyDocument/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
<el-icon @click.stop="emit('delNode')" :size="20">
|
||||
<CloseBold/>
|
||||
</el-icon>
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-body-main-content">
|
||||
<span class="placeholder" v-if="(content || '').trim() === ''">{{ placeholder }}</span>
|
||||
<ellipsis hoverTip :row="4" :content="content" v-else/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-body-right" @click="emit('rightMove')" v-if="level < size && designState">
|
||||
<el-icon>
|
||||
<ArrowRightBold/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="node-error" v-if="showError">
|
||||
<el-tooltip effect="dark" :content="errorInfo" placement="top-start">
|
||||
<el-icon>
|
||||
<Warning/>
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-footer">
|
||||
<div class="btn">
|
||||
<insert-button v-if="designState" @insertNode="type => emit('insertNode', type)"></insert-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InsertButton from '../common/InsertButton.vue'
|
||||
import Ellipsis from '../common/Ellipsis.vue'
|
||||
import {ValueType} from '@/views/workflow/form/ComponentsConfigExport.js'
|
||||
import {ArrowRightBold,ArrowLeftBold,CopyDocument,CloseBold,Warning} from '@element-plus/icons-vue'
|
||||
import {defineProps, defineEmits, defineExpose} from "vue";
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
//索引位置
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
//条件数
|
||||
size: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
|
||||
const groupNames = ref(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J']);
|
||||
const placeholder = ref('请设置条件');
|
||||
const errorInfo = ref('');
|
||||
const showError = ref(false);
|
||||
const content = computed(() => {
|
||||
const groups = props.config.props.groups
|
||||
let confitions = []
|
||||
groups.forEach(group => {
|
||||
let subConditions = []
|
||||
group.conditions.forEach(subCondition => {
|
||||
let subConditionStr = ''
|
||||
switch (subCondition.valueType) {
|
||||
case ValueType.dept:
|
||||
case ValueType.user:
|
||||
subConditionStr = `${subCondition.title}属于[${String(subCondition.value.map(u => u.name)).replaceAll(',', '. ')}]之一`
|
||||
break;
|
||||
case ValueType.number:
|
||||
case ValueType.string:
|
||||
subConditionStr = getOrdinaryConditionContent(subCondition)
|
||||
break;
|
||||
}
|
||||
subConditions.push(subConditionStr)
|
||||
})
|
||||
//根据子条件关系构建描述
|
||||
let subConditionsStr = String(subConditions)
|
||||
.replaceAll(',', subConditions.length > 1 ?
|
||||
(group.groupType === 'AND' ? ') 且 (' : ') 或 (') :
|
||||
(group.groupType === 'AND' ? ' 且 ' : ' 或 '))
|
||||
confitions.push(subConditions.length > 1 ? `(${subConditionsStr})` : subConditionsStr)
|
||||
})
|
||||
//构建最终描述
|
||||
return String(confitions).replaceAll(',', (props.config.props.groupsType === 'AND' ? ' 且 ' : ' 或 '))
|
||||
})
|
||||
|
||||
const designState = computed(()=>{
|
||||
return props.mode === 'design'
|
||||
})
|
||||
|
||||
|
||||
const getDefault = (val, df) => {
|
||||
return val && val !== '' ? val : df;
|
||||
}
|
||||
|
||||
|
||||
const getOrdinaryConditionContent = (subCondition) => {
|
||||
switch (subCondition.compare) {
|
||||
case 'IN':
|
||||
return `${subCondition.title}为[${String(subCondition.value).replaceAll(',', '、')}]中之一`
|
||||
case 'B':
|
||||
return `${subCondition.value[0]} < ${subCondition.title} < ${subCondition.value[1]}`
|
||||
case 'AB':
|
||||
return `${subCondition.value[0]} ≤ ${subCondition.title} < ${subCondition.value[1]}`
|
||||
case 'BA':
|
||||
return `${subCondition.value[0]} < ${subCondition.title} ≤ ${subCondition.value[1]}`
|
||||
case 'ABA':
|
||||
return `${subCondition.value[0]} ≤ ${subCondition.title} ≤ ${subCondition.value[1]}`
|
||||
case '<=':
|
||||
return `${subCondition.title} ≤ ${getDefault(subCondition.value[0], ' ?')}`
|
||||
case '>=':
|
||||
return `${subCondition.title} ≥ ${getDefault(subCondition.value[0], ' ?')}`
|
||||
default:
|
||||
return `${subCondition.title}${subCondition.compare}${getDefault(subCondition.value[0], ' ?')}`
|
||||
}
|
||||
}
|
||||
|
||||
//校验数据配置的合法性
|
||||
const validate = (err) => {
|
||||
const defineProps = props.config.props
|
||||
if (defineProps.groups.length <= 0){
|
||||
showError.value = true
|
||||
errorInfo.value = '请设置分支条件'
|
||||
err.push(`${defineProps.config.name} 未设置条件`)
|
||||
}else {
|
||||
for (let i = 0; i < defineProps.groups.length; i++) {
|
||||
if (defineProps.groups[i].cids.length === 0){
|
||||
showError.value = true
|
||||
errorInfo.value = `请设置条件组${groupNames.value[i]}内的条件`
|
||||
err.push(`条件 ${props.config.name} 条件组${groupNames.value[i]}内未设置条件`)
|
||||
break
|
||||
}else {
|
||||
let conditions = defineProps.groups[i].conditions
|
||||
for (let ci = 0; ci < conditions.length; ci++) {
|
||||
let subc = conditions[ci]
|
||||
showError.value = subc.value.length === 0;
|
||||
if (showError.value){
|
||||
errorInfo.value = `请完善条件组${groupNames.value[i]}内的${subc.title}条件`
|
||||
err.push(`条件 ${props.config.name} 条件组${groupNames.value[i]}内${subc.title}条件未完善`)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return !showError.value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.node {
|
||||
padding: 30px 55px 0;
|
||||
//width: 220px;
|
||||
width: 320px;
|
||||
|
||||
.node-body {
|
||||
cursor: pointer;
|
||||
min-height: 80px;
|
||||
max-height: 120px;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
box-shadow: 0px 0px 5px 0px #d8d8d8;
|
||||
|
||||
&:hover {
|
||||
.node-body-left, .node-body-right {
|
||||
i {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
.node-body-main {
|
||||
.level {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: inline-block !important;
|
||||
}
|
||||
}
|
||||
|
||||
box-shadow: 0px 0px 3px 0px;
|
||||
}
|
||||
|
||||
.node-body-left, .node-body-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
|
||||
i {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: #ececec;
|
||||
}
|
||||
}
|
||||
|
||||
.node-body-left {
|
||||
color: #888888;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.node-body-right {
|
||||
color: #888888;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.node-body-main {
|
||||
//position: absolute;
|
||||
width: 188px;
|
||||
margin-left: 17px;
|
||||
display: inline-block;
|
||||
|
||||
.node-body-main-header {
|
||||
padding: 10px 0px 5px;
|
||||
font-size: xx-small;
|
||||
position: relative;
|
||||
|
||||
.title {
|
||||
color: #15bca3;
|
||||
display: inline-block;
|
||||
height: 14px;
|
||||
width: 125px;
|
||||
}
|
||||
|
||||
.level {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
color: #888888;
|
||||
}
|
||||
|
||||
.option {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
display: none;
|
||||
font-size: medium;
|
||||
|
||||
i {
|
||||
color: #888888;
|
||||
padding: 0 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-body-main-content {
|
||||
padding: 6px;
|
||||
color: #656363;
|
||||
font-size: 14px;
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 55%;
|
||||
right: 10px;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
src/views/workflow/process/nodes/DelayNode.vue
Normal file
86
src/views/workflow/process/nodes/DelayNode.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<node :title="config.name" :mode="mode" :show-error="showError" :content="content" :error-info="errorInfo"
|
||||
@selected="emit('selected')" @delNode="emit('delNode')" @insertNode="type => emit('insertNode', type)"
|
||||
placeholder="请设置延时时间" :header-bgc="headerBgc" :header-icon="Clock"/>
|
||||
</template>
|
||||
<!--延时器节点-->
|
||||
<script setup>
|
||||
import Node from './Node.vue'
|
||||
import {defineProps, defineEmits, computed, defineExpose} from "vue";
|
||||
const emit = defineEmits()
|
||||
import {Clock} from '@element-plus/icons-vue'
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const showError = ref(false)
|
||||
const errorInfo = ref('')
|
||||
|
||||
|
||||
const headerBgc = computed(() => {
|
||||
if (props.mode === 'design' || props.mode === 'view') {
|
||||
return '#f25643'
|
||||
} else {
|
||||
return props.config.props.headerBgc
|
||||
}
|
||||
})
|
||||
|
||||
const content = computed(() => {
|
||||
if (props.config.props.type === 'FIXED') {
|
||||
return `等待 ${props.config.props.time} ${getName(props.config.props.unit)}`
|
||||
} else if (props.config.props.type === 'AUTO') {
|
||||
return `至当天 ${props.config.props.dateTime}`
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})
|
||||
//校验数据配置的合法性
|
||||
const validate = (err) => {
|
||||
showError.value = false
|
||||
try {
|
||||
if (props.config.props.type === "AUTO") {
|
||||
if ((props.config.props.dateTime || "") === "") {
|
||||
showError.value = true
|
||||
errorInfo.value = "请选择时间点"
|
||||
}
|
||||
} else {
|
||||
if (props.config.props.time <= 0) {
|
||||
showError.value = true
|
||||
errorInfo.value = "请设置延时时长"
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
showError.value = true
|
||||
errorInfo.value = "配置出现问题"
|
||||
}
|
||||
if (showError) {
|
||||
err.push(`${props.config.name} 未设置延时规则`)
|
||||
}
|
||||
return !showError
|
||||
}
|
||||
const getName = (unit) => {
|
||||
switch (unit) {
|
||||
case 'D':
|
||||
return '天';
|
||||
case 'H':
|
||||
return '小时';
|
||||
case 'M':
|
||||
return '分钟';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
20
src/views/workflow/process/nodes/EmptyNode.vue
Normal file
20
src/views/workflow/process/nodes/EmptyNode.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<node :show="false" :mode="mode" @insertNode="type => emit('insertNode', type)"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Node from './Node.vue'
|
||||
import {defineEmits,defineProps} from "vue";
|
||||
|
||||
const emit = defineEmits()
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
20
src/views/workflow/process/nodes/MergeNode.vue
Normal file
20
src/views/workflow/process/nodes/MergeNode.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<node :show="false" :merge="true" :mode="mode" @insertNode="type => emit('insertNode', type)"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Node from './Node.vue'
|
||||
import {defineEmits, defineProps} from "vue";
|
||||
const emit = defineEmits()
|
||||
|
||||
const props = defineProps({
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
269
src/views/workflow/process/nodes/Node.vue
Normal file
269
src/views/workflow/process/nodes/Node.vue
Normal file
@@ -0,0 +1,269 @@
|
||||
<template>
|
||||
<div :class="{'node': true, 'root': isRoot || !show, 'node-error-state': showError}">
|
||||
<div v-if="show" @click="emit('selected')" :class="{'node-body': true, 'error': showError}">
|
||||
<div class="node-body-header" :style="{'background-color': headerBgc}">
|
||||
<el-icon v-if="headerIcon" size="15">
|
||||
<component :is="headerIcon"/>
|
||||
</el-icon>
|
||||
<ellipsis class="name" hover-tip :content="title"/>
|
||||
<el-icon v-if="!isRoot && designState" size="15" style="float:right;" @click="emit('delNode')">
|
||||
<Close/>
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="node-body-content">
|
||||
<el-icon v-if="leftIcon">
|
||||
<component :is="leftIcon"/>
|
||||
</el-icon>
|
||||
<template v-if="selectUser.show && mode === 'view'">
|
||||
<div class="avatar_button">
|
||||
<avatar-ellipsis :row="3" v-if="userInfo.length > 0" :user-info="userInfo"/>
|
||||
<el-button type="primary" :icon="Plus" circle/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="showAvatar">
|
||||
<span class="placeholder" v-if="userInfo.length === 0">{{ placeholder }}</span>
|
||||
<avatar-ellipsis :row="3" :user-info="userInfo" v-else/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="placeholder" v-if="(content || '').trim() === ''">{{ placeholder }}</span>
|
||||
<ellipsis :row="3" :content="content" v-else/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="node-error" v-if="showError">
|
||||
<el-tooltip effect="dark" :content="errorInfo" placement="top-start">
|
||||
<el-icon><Warning /></el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="node-footer">
|
||||
<div v-if="merge" class="branch-merge">
|
||||
<img data-v-1e7b1da5=""
|
||||
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAABXlJREFUeF7tmm2IVGUUx3/nLkQUqxmkpAVhSPUllZX8EEGhvVgZQWCU6wcpsqi2mXtnoaBwoqLCufeO9oJYUJQiFZWlZPQCfYiIXkylstTyS1oWWeYWCe6cuLsz7t3r3Ll3du6907oz35bnPM9z/r895zznmWeECf6RCa6fDoBOBExwAp0UmOAB0CmCnRTopECbCZi2nivK1CMD7FpXlH+ydqctKZBz9QyjwkqFG4HzfKK/0gpvl/ulmBWIzAHkVmlRDFY2EihwwLFkRhYQxgzAtHW2CpcBc4CpOsg26eK7yVPYVFwu/9ZzPufq2VLhQBxhAk85lvSF2Y5l/3prjQlAvqR3IjwJTKqz6DeGssQuyLfBsbyjz6PcFgeAZyOwzLFk/QnrjHH/RACYtj6ncHuUCDWYXs7Lz367XEn3inB+1NzauMLGsiW3+u1b2b9lAH22LuiCD2IJED5yTbmiZpt3dAbKT7Hmjhjtdi25oPZnK/uH7dtUCpi2vqVwQ1wRWuHhWkW3StpTEb6IO3fIThhwTemuzWll/0QA5G39FTgrtghhs2vKELA7inra6d38HXvusOEnriWXHo+iFvZvGUDfap3VdYzdzQgIHmd5W7cBc2Ovoax1C3KXZ5/E/i3VgCQciNMD+J1Ug55yXjxo7QfgOdFKCvjyeL/C9MgoEBa5przrt0ti/+C+mRVB/8amrWsU7m2Ql/c5lqwJjre9CDZ1DMFnriXzw0SatvZW4FqBW6o2vwGvCGxyLPmw3rym9g8cwy0XQV8Ix2qEgH0C1ziWNCyceVu1uvZrriVLGqVGsainHO7mRyDynlCvEWupCI7Kxcat6BFg+OwW9ohyfSMIcQF44v/sZovAlRH1I7QVTwyAt1Cjy8hfh9hYvepGQogDoI74HYbSXzHwusTYl7FEAURVcdPWN+NAiAJQTzzCda4p+6N8iDPe1CkQZ8FAtY+E0AhA2uKHszTlT1QkhAHIQnwmAKr1IjQS6gHISnxmABpBCALIUnymAMIgKHxf6wMmH6E3cNTtSLLgZXoKNOgAR6UDyqyq7esKk3znfOriM48AXzc5AmGE1EFgWvXPTMS3DcAJ6TA6XDIT31YAoRCEc5JqcuKc8Kn3AVFO+E4BryvJVHzbI8BzwAdgi2vJ4ihgSY//nyIg8jqctPi2RYBp61wV5qsyR2BFVdhBhBe0wtdisNU15VAagoNrZh4BOVsfFcVCODVUoLBn6E2hIBvShpApgLyt3nvhRXFFCWxwLOmNaz8Wu8wA5G31ntQWNOukwD2OJc80Oy+ufSYAciVdJsJLcZ0K2nV1MbuUk51jnd9oXuoACmW9eHCQj49/T+j3RtiJslPgFxXmoVxe11nhPdeUq8clANPWPoXVQef9D6e1saEHVINHUBYF7Q1lnl2QL5OGkHoE5G19GRhVyBQ+L1tyST0xoa/Iygq3IOvGI4BdwIWjHBeecE15IExM3tYfgJmBOetcU2o9Q2IcsoiAw8Gf0qhyU7kgb4QCcPRTlNGvSspmtzD81J7kJ30AJX0fYeHo2seLjiXLG0TA78CZ/nGFB8uWPJakeG+t9AE4+jjK/XGLWtgTusBix5It4w5ArqRLRTjhl14IW40KD/kre66ki0R4p57IrgozS/2yb9wByDvqhbKX07Xv/oIavMfOP6pFb0pdgcqrbkFuTlp8JingbRIaBfEUHUOYltbtMPUaUNNo2rpeYWk8zSNWqvSmeSvMDIAnybT1boWnY0I4aigL7YJ4bXRqn0wBeCqG7gYVVqFcFapKWXt0gP5nizKQmvLqwpkDqAkaanmhB4MelOkI27XCDjHY7pqyN23htfXbBiArgVH7dABEETrZxzsRcLL/h6P0/Qc1qphfvB2K3wAAAABJRU5ErkJggg=="
|
||||
alt="">
|
||||
</div>
|
||||
<div class="btn">
|
||||
<insert-button v-if="designState" @insertNode="type => emit('insertNode', type)"/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <user-picker v-if="selectUser.show" title="请选择系统用户" :multiple="selectUser.multiple" ref="userPicker"-->
|
||||
<!-- :selected="_userInfo"-->
|
||||
<!-- @ok="selectedUser"/>-->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InsertButton from '../common/InsertButton.vue'
|
||||
import Ellipsis from '../common/Ellipsis.vue'
|
||||
import AvatarEllipsis from '../common/AvatarEllipsis.vue'
|
||||
import {defineProps,defineEmits} from "vue";
|
||||
const emit = defineEmits(['insertNode'])
|
||||
import {Close,Warning,Plus} from '@element-plus/icons-vue'
|
||||
const props = defineProps({
|
||||
//是否为根节点
|
||||
isRoot: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
nodeId:{
|
||||
type:String,
|
||||
default:()=>{
|
||||
return "";
|
||||
}
|
||||
},
|
||||
//是否显示节点体
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
//是否显示节点体
|
||||
merge: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
//节点内容区域文字
|
||||
content: {
|
||||
type: String,
|
||||
default: ""
|
||||
},
|
||||
//节点内容区域文字
|
||||
userInfo: {
|
||||
type: Array,
|
||||
default() {
|
||||
return []
|
||||
}
|
||||
},
|
||||
//节点内容区域文字
|
||||
showAvatar: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
selectUser: {
|
||||
type: Object,
|
||||
default() {
|
||||
return {
|
||||
show: false,
|
||||
multiple: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "标题"
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "请设置"
|
||||
},
|
||||
//节点体左侧图标
|
||||
leftIcon: {
|
||||
type: Object,
|
||||
default: null
|
||||
},
|
||||
//头部图标
|
||||
headerIcon: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
//头部背景色
|
||||
headerBgc: {
|
||||
type: String,
|
||||
default: '#576a95'
|
||||
},
|
||||
//是否显示错误状态
|
||||
showError: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
errorInfo: {
|
||||
type: String,
|
||||
default: '无信息'
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
|
||||
const designState = computed(()=>{
|
||||
return props.mode === 'design'
|
||||
})
|
||||
|
||||
|
||||
const init = () => {
|
||||
// let userInfo = this.$store.state.selectUserMap.get(this.nodeId);
|
||||
// if (userInfo){
|
||||
// let userInfoList = []
|
||||
// for (let val of userInfo) {
|
||||
// let userInfo = {
|
||||
// id: val.id,
|
||||
// name: val.name,
|
||||
// avatar: val.avatar,
|
||||
// }
|
||||
// userInfoList.push(userInfo)
|
||||
// }
|
||||
// // this._userInfo = userInfoList
|
||||
// }
|
||||
}
|
||||
|
||||
init()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.root {
|
||||
&:before {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.node {
|
||||
padding: 0 50px;
|
||||
//width: 220px;
|
||||
width: 320px;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
left: 50%;
|
||||
-webkit-transform: translateX(-50%);
|
||||
transform: translateX(-50%);
|
||||
width: 0;
|
||||
border-style: solid;
|
||||
border-width: 8px 6px 4px;
|
||||
border-color: #CACACA transparent transparent;
|
||||
background: #F5F5F7;
|
||||
}
|
||||
|
||||
.node-body {
|
||||
cursor: pointer;
|
||||
min-height: 63px;
|
||||
position: relative;
|
||||
border-radius: 5px;
|
||||
background-color: white;
|
||||
box-shadow: 0px 0px 5px 0px #d8d8d8;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 3px 0px;
|
||||
|
||||
.node-body-header {
|
||||
.el-icon-close {
|
||||
display: inline;
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.node-body-header {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
padding: 5px 15px;
|
||||
color: white;
|
||||
font-size: xx-small;
|
||||
|
||||
.el-icon-close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.name {
|
||||
height: 14px;
|
||||
width: 150px;
|
||||
display: inline-block
|
||||
}
|
||||
}
|
||||
|
||||
.node-body-content {
|
||||
padding: 18px;
|
||||
color: #656363;
|
||||
font-size: 14px;
|
||||
|
||||
.avatar_button {
|
||||
//float: left;
|
||||
display: flex;
|
||||
//flex: 1;
|
||||
flex-wrap: wrap;
|
||||
button {
|
||||
margin-top: 3px;
|
||||
height: 40px;
|
||||
//flex-shrink: 0;
|
||||
//flex-grow: 0;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
position: absolute;
|
||||
top: 55%;
|
||||
right: 5px;
|
||||
font-size: medium;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: #8c8c8c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
</style>
|
||||
3
src/views/workflow/process/nodes/ProcessEndNode.vue
Normal file
3
src/views/workflow/process/nodes/ProcessEndNode.vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
流程结束
|
||||
</template>
|
||||
48
src/views/workflow/process/nodes/RootNode.vue
Normal file
48
src/views/workflow/process/nodes/RootNode.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<node title="发起人" :is-root="true" :mode="mode" :content="content" show-avatar :user-info="config.props.assignedUser"
|
||||
@selected="emit('selected')" @insertNode="type => emit('insertNode', type)"
|
||||
placeholder="所有人" :header-bgc="config.props.headerBgc" :header-icon="UserFilled"/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import Node from './Node.vue'
|
||||
import {UserFilled} from '@element-plus/icons-vue'
|
||||
import {defineExpose, defineProps} from "vue";
|
||||
const emit = defineEmits(['insertNode','selected'])
|
||||
|
||||
const props = defineProps({
|
||||
config:{
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const content = computed(() => {
|
||||
if (props.config.props.assignedUser.length > 0){
|
||||
let texts = []
|
||||
props.config.props.assignedUser.forEach(org => texts.push(org.name))
|
||||
return String(texts).replaceAll(',', '、')
|
||||
} else {
|
||||
return '所有人'
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
const validate = () => {
|
||||
console.log("调用成功")
|
||||
return []
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
96
src/views/workflow/process/nodes/TriggerNode.vue
Normal file
96
src/views/workflow/process/nodes/TriggerNode.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<node :title="config.name" :mode="mode" :show-error="showError" :content="content" :error-info="errorInfo"
|
||||
@selected="emit('selected')" @delNode="emit('delNode')" @insertNode="type => emit('insertNode', type)"
|
||||
placeholder="请设置触发器" :header-bgc="headerBgc" :header-icon="SetUp"/>
|
||||
</template>
|
||||
<!--触发器节点-->
|
||||
<script setup>
|
||||
import Node from './Node.vue'
|
||||
const emit = defineEmits()
|
||||
import {defineProps, defineEmits, defineExpose} from "vue";
|
||||
import {SetUp} from '@element-plus/icons-vue'
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'design'
|
||||
}
|
||||
})
|
||||
const showError = ref(false)
|
||||
const errorInfo = ref('')
|
||||
const viewer = computed(() => {
|
||||
return false;
|
||||
})
|
||||
const preview = computed(() => {
|
||||
return false;
|
||||
})
|
||||
|
||||
const headerBgc = computed(() => {
|
||||
return '#47bc82'
|
||||
// if (preview || !viewer) {
|
||||
// return '#ff943e'
|
||||
// } else {
|
||||
// return props.config.props.headerBgc
|
||||
// }
|
||||
})
|
||||
const content = computed(() => {
|
||||
return '请设置触发器'
|
||||
})
|
||||
//校验数据配置的合法性
|
||||
const validate = (err) => {
|
||||
showError.value = false
|
||||
if (props.config.props.type === 'WEBHOOK') {
|
||||
if (isNotEmpty(props.config.props.http.url)) {
|
||||
showError.value = false
|
||||
} else {
|
||||
showError.value = true
|
||||
errorInfo.value = '请设置WEBHOOK的URL地址'
|
||||
}
|
||||
if (restfulCheck(props.config.props.http.url)) {
|
||||
showError.value = false
|
||||
} else {
|
||||
showError.value = true
|
||||
errorInfo.value = 'WEBHOOK的URL地址不符合RESTful标准'
|
||||
}
|
||||
} else if (props.config.props.type === 'EMAIL') {
|
||||
if (!isNotEmpty(props.config.props.email.subject)
|
||||
|| props.config.props.email.to.length === 0
|
||||
|| !isNotEmpty(props.config.props.email.content)) {
|
||||
showError.value = true
|
||||
errorInfo.value = '请设置邮件发送配置'
|
||||
} else {
|
||||
showError.value = false
|
||||
}
|
||||
}
|
||||
if (showError) {
|
||||
err.push(`${props.config.name} 触发动作未设置完善`)
|
||||
}
|
||||
return !showError
|
||||
}
|
||||
//url规范性检查
|
||||
const isNotEmpty = (obj) => {
|
||||
return (obj !== undefined && obj !== null && obj !== '' && obj !== 'null')
|
||||
}
|
||||
const restfulCheck = (url) => {
|
||||
const httpProtocolPattern = /^http:/;
|
||||
const httpsProtocolPattern = /^https:/;
|
||||
const restfulUrlPattern = /\/\w+\/\w+(\/\{[^}]+\})*/;
|
||||
if (httpProtocolPattern.test(url) || httpsProtocolPattern.test(url)) {
|
||||
return restfulUrlPattern.test(url);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
defineExpose({
|
||||
validate
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
134
src/views/workflow/task/index.vue
Normal file
134
src/views/workflow/task/index.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :model="queryParams" inline class="query-form" ref="queryForm" @submit.prevent="getList">
|
||||
<el-form-item label="部署名称" prop="deploymentName">
|
||||
<el-input v-model="queryParams.deploymentName" placeholder="请输入部署名称" clearable></el-input>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item label="状态" prop="enable">-->
|
||||
<!-- <el-select v-model="queryParams.enable" placeholder="请选择状态" clearable filterable>-->
|
||||
<!-- <el-option-->
|
||||
<!-- v-for="dict in cacheStore.getDict('regular_enable')"-->
|
||||
<!-- :key="dict.value"-->
|
||||
<!-- :label="dict.label"-->
|
||||
<!-- :value="dict.value"-->
|
||||
<!-- />-->
|
||||
<!-- </el-select>-->
|
||||
<!-- </el-form-item>-->
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="getList" :icon="Search">搜索</el-button>
|
||||
<el-button type="primary" @click="handleReset" :icon="Refresh" plain>重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="table">
|
||||
<el-table
|
||||
:data="list"
|
||||
row-key="id"
|
||||
:lazy="true"
|
||||
ref="singleTable"
|
||||
@cell-click="showDetails"
|
||||
v-loading="loading"
|
||||
v-tabh
|
||||
>
|
||||
<el-table-column type="selection" width="30"/>
|
||||
<el-table-column label="序号" type="index" width="60" align="center"/>
|
||||
<el-table-column prop="processName" label="审批类型" align="center"/>
|
||||
<el-table-column prop="initiatorName" label="发起人" align="center"/>
|
||||
<el-table-column prop="createdDate" label="提交时间" align="center"/>
|
||||
<el-table-column prop="taskName" label="当前节点" align="center"/>
|
||||
<el-table-column prop="arriveDate" label="到达时间" align="center"/>
|
||||
<el-table-column prop="state" label="状态" align="center">
|
||||
<template #default="scope">
|
||||
<point-tag dict-type="process_state" :value="scope.row.state"/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<paging :current-page="pageInfo.pageNum" :page-size="pageInfo.pageSize" :page-sizes="[10, 20, 30, 40,50]"
|
||||
:total="total" @changeSize="handleSizeChange" @goPage="handleCurrentChange"/>
|
||||
<task-details ref="taskDetails" :task="selectTask" @refresh="getList"/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {getTaskList} from "@/api/workflow/process-task.js";
|
||||
import {Search, Refresh, Delete, Edit} from '@element-plus/icons-vue'
|
||||
import TaskDetails from '@/views/workflow/common/TaskDetails.vue'
|
||||
import {useProcessStore} from '@/stores/processStore.js'
|
||||
import Paging from "@/components/pagination/index.vue";
|
||||
import {useCacheStore} from '@/stores/cache.js'
|
||||
import PointTag from "@/components/PointTag.vue";
|
||||
|
||||
const processStore = useProcessStore()
|
||||
const dictStore = useCacheStore()
|
||||
dictStore.setCacheKey(['process_state'])
|
||||
//查询参数
|
||||
const queryParams = reactive({
|
||||
deploymentName: '',
|
||||
state: 1
|
||||
})
|
||||
|
||||
//页面信息
|
||||
const pageInfo = reactive({
|
||||
pageNum: 1,
|
||||
pageSize: 10,
|
||||
})
|
||||
const list = ref([])
|
||||
const selectTask = ref()
|
||||
const total = ref()
|
||||
const loading = ref()
|
||||
const taskDetails = ref()
|
||||
const queryForm = ref()
|
||||
|
||||
const showDetails = (row, column) => {
|
||||
if (column.label !== '操作') {
|
||||
console.log(row)
|
||||
selectTask.value = row
|
||||
nextTick(() => {
|
||||
taskDetails.value.init()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//重置搜索
|
||||
const handleReset = () => {
|
||||
queryForm.value.resetFields()
|
||||
getList()
|
||||
}
|
||||
const getList = async () => {
|
||||
let params = {
|
||||
...queryParams,
|
||||
...pageInfo
|
||||
}
|
||||
loading.value = true
|
||||
getTaskList(params).then(res => {
|
||||
list.value = res.data.rows
|
||||
total.value = res.data.total
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
//切换每页显示条数
|
||||
const handleSizeChange = async (val) => {
|
||||
pageInfo.value.pageSize = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
//点击页码进行分页功能
|
||||
const handleCurrentChange = async (val) => {
|
||||
pageInfo.value.pageNum = val
|
||||
await getList()
|
||||
}
|
||||
|
||||
const getTimeConsuming = async (instance) => {
|
||||
if (instance.state != 1) {
|
||||
//dateFormat(开始时间,结束时间)
|
||||
// return timeLength(instance.submitTime, instance.endTime);
|
||||
}
|
||||
}
|
||||
|
||||
getList()
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user