初始版本提交
This commit is contained in:
95
src/views/workflow/task/components/flow/AddOrSubtract.vue
Normal file
95
src/views/workflow/task/components/flow/AddOrSubtract.vue
Normal file
@ -0,0 +1,95 @@
|
||||
<template>
|
||||
<a-button class="mr-2" @click="show">{{ t('加签或减签') }}</a-button>
|
||||
<a-modal
|
||||
:width="1000"
|
||||
:visible="data.visible"
|
||||
:title="t('加签或减签')"
|
||||
:maskClosable="false"
|
||||
@ok="submit"
|
||||
@cancel="cancel"
|
||||
>
|
||||
<div class="p-5 box" v-if="data.visible">
|
||||
<SelectApproveUser
|
||||
:schemaId="props.schemaId"
|
||||
:taskId="props.taskId"
|
||||
v-model:select-ids="data.selectedIds"
|
||||
/>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive } from 'vue';
|
||||
import SelectApproveUser from './SelectApproveUser.vue';
|
||||
import { postSetSign } from '/@/api/workflow/task';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
const props = defineProps({
|
||||
schemaId: {
|
||||
type: String,
|
||||
// required: true,
|
||||
},
|
||||
processId: {
|
||||
type: String,
|
||||
// required: true,
|
||||
},
|
||||
taskId: {
|
||||
type: String,
|
||||
// required: true,
|
||||
},
|
||||
});
|
||||
let data: {
|
||||
visible: boolean;
|
||||
selectedIds: Array<string>;
|
||||
} = reactive({
|
||||
visible: false,
|
||||
selectedIds: [],
|
||||
});
|
||||
function show() {
|
||||
data.selectedIds = [];
|
||||
data.visible = true;
|
||||
}
|
||||
function cancel() {
|
||||
data.selectedIds = [];
|
||||
data.visible = false;
|
||||
}
|
||||
async function submit() {
|
||||
let msgs: Array<string> = [];
|
||||
if (msgs.length > 0) {
|
||||
msgs.forEach((msg) => {
|
||||
notification.open({
|
||||
type: 'error',
|
||||
message: t('加签减签'),
|
||||
description: msg,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
if (props.schemaId && props.taskId) {
|
||||
await postSetSign(props.schemaId, props.taskId, data.selectedIds);
|
||||
cancel();
|
||||
}
|
||||
} catch (_error) {
|
||||
notification.open({
|
||||
type: 'error',
|
||||
message: t('加签减签'),
|
||||
description: t('选择加签减签失败'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.box {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.list-page-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="800"
|
||||
:visible="true"
|
||||
:title="t('指派审核人')"
|
||||
:maskClosable="false"
|
||||
@ok="submit"
|
||||
@cancel="close"
|
||||
>
|
||||
<div class="p-5">
|
||||
<div class="mt-2"
|
||||
><div>{{ title }}{{ t('【当前】:') }}</div>
|
||||
<a-input :value="data.currentUserNames" disabled />
|
||||
</div>
|
||||
<div class="mt-2"
|
||||
><div>{{ title }}{{ t('【指派给】:') }}</div>
|
||||
|
||||
<SelectUser
|
||||
:selectedIds="selectedIds"
|
||||
:disabledIds="data.currentUserIds"
|
||||
:multiple="true"
|
||||
@change="getUserList"
|
||||
>
|
||||
<a-input :value="data.selectedNames" />
|
||||
</SelectUser>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive } from 'vue';
|
||||
import { getApproveUserList, postSetAssignee } from '/@/api/workflow/task';
|
||||
import { SelectUser } from '/@/components/SelectOrganizational/index';
|
||||
import { getUserMulti } from '/@/api/system/user';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
const props = defineProps({
|
||||
schemaId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
taskId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
let emits = defineEmits(['close']);
|
||||
let data: {
|
||||
currentUserNames: string;
|
||||
currentUserIds: Array<string>;
|
||||
selectedNames: string;
|
||||
selectedList: Array<{ id: string; name: string }>;
|
||||
} = reactive({
|
||||
selectedList: [],
|
||||
currentUserIds: [],
|
||||
currentUserNames: '',
|
||||
selectedNames: '',
|
||||
});
|
||||
const selectedIds = computed(() => {
|
||||
return data.selectedList.map((ele) => {
|
||||
return ele.id;
|
||||
});
|
||||
});
|
||||
onMounted(async () => {
|
||||
if (props.schemaId && props.taskId) {
|
||||
try {
|
||||
let userList = await getApproveUserList(props.schemaId, props.taskId);
|
||||
data.currentUserNames = userList
|
||||
.map((ele) => {
|
||||
return ele.name;
|
||||
})
|
||||
.join(',');
|
||||
data.currentUserIds = userList.map((ele) => {
|
||||
return ele.id;
|
||||
});
|
||||
} catch (_error) {}
|
||||
}
|
||||
});
|
||||
|
||||
async function getUserList(list: Array<string>) {
|
||||
data.selectedList = await getUserMulti(list.join(','));
|
||||
data.selectedNames = data.selectedList
|
||||
.map((ele) => {
|
||||
return ele.name;
|
||||
})
|
||||
.join(',');
|
||||
}
|
||||
async function submit() {
|
||||
try {
|
||||
let res = await postSetAssignee(props.taskId, selectedIds.value);
|
||||
if (res) {
|
||||
notification.open({
|
||||
type: 'success',
|
||||
message: t('指派审核人'),
|
||||
description: t('指派审核人成功'),
|
||||
});
|
||||
close();
|
||||
} else {
|
||||
notification.open({
|
||||
type: 'error',
|
||||
message: t('指派审核人'),
|
||||
description: t('指派审核人失败'),
|
||||
});
|
||||
}
|
||||
} catch (error) {}
|
||||
}
|
||||
function close() {
|
||||
emits('close');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
129
src/views/workflow/task/components/flow/ApproveUser.vue
Normal file
129
src/views/workflow/task/components/flow/ApproveUser.vue
Normal file
@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<a-modal
|
||||
:width="1000"
|
||||
:visible="true"
|
||||
:title="t('下一节点审批人')"
|
||||
:maskClosable="false"
|
||||
:closable="false"
|
||||
:cancel-button-props="{
|
||||
disabled: true,
|
||||
}"
|
||||
:okText="t('确认')"
|
||||
:cancelText="t('取消')"
|
||||
@ok="submit"
|
||||
>
|
||||
<div class="p-5">
|
||||
<NodeHead :nodeName="t('信息')" />
|
||||
<div class="mt-5 mb-5 ml-5">{{
|
||||
t('请在十分钟内指定相关审批人员,时限内未完成指定的话,系统将按照现有默认人员进行处理。')
|
||||
}}</div>
|
||||
<a-tabs v-model:activeKey="data.index">
|
||||
<a-tab-pane v-for="(item, index) in data.tasks" :key="index" :tab="item.taskName">
|
||||
<SelectApproveUser
|
||||
:schemaId="props.schemaId"
|
||||
:taskId="item.taskId"
|
||||
:hasMoreBtn="item.provisionalApprover"
|
||||
v-model:select-ids="data.tasks[index].selectIds"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue';
|
||||
import SelectApproveUser from './SelectApproveUser.vue';
|
||||
import { batchApproverUsers } from '/@/api/workflow/task';
|
||||
import { NodeHead } from '/@/components/ModalPanel/index';
|
||||
import { ApproveTask, BatchApproverUsersParams } from '/@/model/workflow/bpmnConfig';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
taskList: Array<ApproveTask>;
|
||||
schemaId: string | undefined;
|
||||
}>(),
|
||||
{
|
||||
taskList: () => {
|
||||
return [];
|
||||
},
|
||||
schemaId: '',
|
||||
},
|
||||
);
|
||||
|
||||
let emits = defineEmits(['change']);
|
||||
let data: {
|
||||
index: number;
|
||||
tasks: Array<ApproveTask>;
|
||||
} = reactive({
|
||||
index: 0,
|
||||
tasks: [],
|
||||
});
|
||||
onMounted(() => {
|
||||
if (props.schemaId && props.taskList.length > 0) {
|
||||
props.taskList.forEach(async (item) => {
|
||||
data.tasks.push({
|
||||
taskId: item.taskId,
|
||||
taskName: item.taskName,
|
||||
provisionalApprover: item.provisionalApprover,
|
||||
selectIds: [],
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
async function submit() {
|
||||
let approveUserList: BatchApproverUsersParams = [];
|
||||
let msgs: Array<string> = [];
|
||||
data.tasks.forEach((element) => {
|
||||
if (element.selectIds.length > 0) {
|
||||
approveUserList.push({
|
||||
taskId: element.taskId,
|
||||
approvedUsers: element.selectIds,
|
||||
});
|
||||
} else {
|
||||
msgs.push(t('任务:') + element.taskName + t('未选择下一节点审批人'));
|
||||
}
|
||||
});
|
||||
if (msgs.length > 0) {
|
||||
msgs.forEach((msg) => {
|
||||
notification.open({
|
||||
type: 'error',
|
||||
message: t('下一节点审批人'),
|
||||
description: msg,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
if (props.schemaId) {
|
||||
let res = await batchApproverUsers(props.schemaId, approveUserList);
|
||||
if (res) {
|
||||
emits('change');
|
||||
} else {
|
||||
notification.open({
|
||||
type: 'error',
|
||||
message: t('下一节点审批人'),
|
||||
description: t('选择下一节点审批人失败'),
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (_error) {
|
||||
notification.open({
|
||||
type: 'error',
|
||||
message: t('下一节点审批人'),
|
||||
description: t('选择下一节点审批人失败'),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
:deep(.list-page-box) {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
61
src/views/workflow/task/components/flow/EmptyBox.vue
Normal file
61
src/views/workflow/task/components/flow/EmptyBox.vue
Normal file
@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="empty-box">
|
||||
<IconFontSymbol icon="history" class="empty-icon" />
|
||||
<div class="title">{{ title }}</div>
|
||||
<div class="desc">{{ desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
const defaultMsg = t('暂无数据');
|
||||
|
||||
export default {};
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue';
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: defaultMsg,
|
||||
},
|
||||
desc: {
|
||||
type: String,
|
||||
default: defaultMsg,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.empty-box {
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 60px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
margin: 6px 0;
|
||||
color: #666666;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 14px;
|
||||
color: #999999;
|
||||
margin: 6px 0;
|
||||
}
|
||||
</style>
|
||||
51
src/views/workflow/task/components/flow/FlowPanel.vue
Normal file
51
src/views/workflow/task/components/flow/FlowPanel.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<a-tabs :tab-position="props.position" v-model:activeKey="activeKey">
|
||||
<a-tab-pane :key="1" :tab="t('表单信息')" force-render><slot></slot></a-tab-pane>
|
||||
<a-tab-pane :key="2" :tab="t('流程信息')">
|
||||
<ProcessInformation :xml="xml" :processId="processId"
|
||||
/></a-tab-pane>
|
||||
<a-tab-pane :key="3" :tab="t('流转记录')" style="overflow: auto"
|
||||
><FlowRecord :list="taskRecords" :processId="processId"
|
||||
/></a-tab-pane>
|
||||
<a-tab-pane :key="4" :tab="t('附件汇总')"
|
||||
><SummaryOfAttachments :processId="processId"
|
||||
/></a-tab-pane>
|
||||
<a-tab-pane :key="5 + index" v-for="(item, index) in predecessorTasks" :tab="item.schemaName">
|
||||
<LookRelationTask
|
||||
v-if="activeKey === 5 + index"
|
||||
:taskId="item.taskId"
|
||||
:processId="item.processId"
|
||||
position="left"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FlowRecord from './FlowRecord.vue';
|
||||
import ProcessInformation from './ProcessInformation.vue';
|
||||
import SummaryOfAttachments from './SummaryOfAttachments.vue';
|
||||
import LookRelationTask from './LookRelationTask.vue';
|
||||
import { ref } from 'vue';
|
||||
import { SchemaTaskItem } from '/@/model/workflow/bpmnConfig';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
let props = withDefaults(
|
||||
defineProps<{
|
||||
position: string;
|
||||
xml: string | undefined;
|
||||
taskRecords: Array<any> | undefined;
|
||||
processId: string | undefined;
|
||||
predecessorTasks: Array<SchemaTaskItem> | undefined;
|
||||
}>(),
|
||||
{
|
||||
xml: '',
|
||||
processId: '',
|
||||
predecessorTasks: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const activeKey = ref(1);
|
||||
</script>
|
||||
208
src/views/workflow/task/components/flow/FlowRecord.vue
Normal file
208
src/views/workflow/task/components/flow/FlowRecord.vue
Normal file
@ -0,0 +1,208 @@
|
||||
<template>
|
||||
<div class="flow-box">
|
||||
<!-- 流转记录 -->
|
||||
<EmptyBox
|
||||
v-if="results.length == 0"
|
||||
:title="t('当前无相关流转记录')"
|
||||
:desc="t('流程需发起后才会有流转记录产生')"
|
||||
/>
|
||||
<div class="relative" v-else>
|
||||
<a-button type="primary" class="!absolute right-0 cursor-pointer z-99" @click="selfHandle">{{
|
||||
btnText ? '仅查看本人' : '查看所有'
|
||||
}}</a-button>
|
||||
<a-tabs v-model:activeKey="activeKey">
|
||||
<a-tab-pane v-for="(parent, idx) in results" :key="idx" :tab="t(parent.schemaName)">
|
||||
<div class="card-box" v-for="(item, index) in parent.records" :key="index">
|
||||
<div class="card-item" v-if="index % 2 == 0">
|
||||
<div class="card basic">
|
||||
<div class="node-box">
|
||||
<span class="icon blue"></span>
|
||||
<span class="title">{{ t('节点名称') }}</span>
|
||||
<span class="sign">: </span>
|
||||
<span class="text color2">{{ item.nodeName }}</span></div
|
||||
>
|
||||
<div class="node-box">
|
||||
<span class="icon green"></span>
|
||||
<span class="title">{{ t('审批信息') }}</span>
|
||||
<span class="sign">: </span>
|
||||
<span class="text">{{ item.comment }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dot-box"
|
||||
><div class="line"></div><div class="dot"></div><div class="line"></div
|
||||
></div>
|
||||
<div class="date basic">{{ item.startTime }}</div>
|
||||
</div>
|
||||
<div class="card-item" v-else>
|
||||
<div class="date basic date-padding date-right">{{ item.startTime }}</div>
|
||||
<div class="dot-box"
|
||||
><div class="line"></div><div class="dot"></div><div class="line"></div
|
||||
></div>
|
||||
<div class="card basic">
|
||||
<div class="node-box">
|
||||
<span class="icon blue"></span>
|
||||
<span class="title">{{ t('节点名称') }}</span>
|
||||
<span class="sign">: </span>
|
||||
<span class="text color2">{{ item.nodeName }}</span></div
|
||||
>
|
||||
<div class="node-box">
|
||||
<span class="icon green"></span>
|
||||
<span class="title">{{ t('审批信息') }}</span>
|
||||
<span class="sign">: </span>
|
||||
<span class="text">{{ item.comment }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue';
|
||||
import EmptyBox from './EmptyBox.vue';
|
||||
import { getSelfRecords } from '/@/api/workflow/process';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { TaskRecordList } from '/@/model/workflow/bpmnConfig';
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
list: { type: Array as PropType<TaskRecordList[]> },
|
||||
processId: String,
|
||||
});
|
||||
const results = ref<TaskRecordList[]>(props.list || []);
|
||||
const activeKey = ref(0);
|
||||
const btnText = ref(true);
|
||||
onMounted(() => {
|
||||
if (!props.list) getRecords(0);
|
||||
});
|
||||
function selfHandle() {
|
||||
results.value = [];
|
||||
if (btnText.value) {
|
||||
getRecords(1);
|
||||
} else {
|
||||
if (props.list) {
|
||||
results.value = props.list;
|
||||
} else {
|
||||
getRecords(0);
|
||||
}
|
||||
}
|
||||
|
||||
btnText.value = !btnText.value;
|
||||
}
|
||||
function getRecords(isSelf) {
|
||||
getSelfRecords({ processId: props.processId, onlySelf: isSelf }).then((res) => {
|
||||
if (res.taskRecords) {
|
||||
results.value.push({
|
||||
records: res.taskRecords,
|
||||
schemaName: '当前流程',
|
||||
});
|
||||
}
|
||||
|
||||
if (res.otherProcessApproveRecord) {
|
||||
results.value = results.value.concat(res.otherProcessApproveRecord);
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.flow-box {
|
||||
height: 80vh;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.dot-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid;
|
||||
border-color: @primary-color;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.line {
|
||||
flex: 1;
|
||||
width: 2px;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
align-self: center;
|
||||
padding: 10px;
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
.date-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.date-padding {
|
||||
padding-left: 20%;
|
||||
}
|
||||
|
||||
.card-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.basic {
|
||||
flex-basis: 40%;
|
||||
}
|
||||
|
||||
.card {
|
||||
height: 100%;
|
||||
background-color: #fafafa;
|
||||
border: 1px solid #ebeef5;
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
margin: 30px;
|
||||
}
|
||||
|
||||
.node-box {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: 10px 0;
|
||||
|
||||
.icon {
|
||||
width: 6px;
|
||||
height: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.blue {
|
||||
background-color: #5e95ff;
|
||||
}
|
||||
|
||||
.green {
|
||||
background-color: #95f204;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sign {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.text {
|
||||
line-height: 22px;
|
||||
color: #999;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.color2 {
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
472
src/views/workflow/task/components/flow/FormInformation.vue
Normal file
472
src/views/workflow/task/components/flow/FormInformation.vue
Normal file
@ -0,0 +1,472 @@
|
||||
<template>
|
||||
<!-- 表单信息 -->
|
||||
<div class="form-container">
|
||||
<div class="box">
|
||||
<div class="form-left relative" :style="{ width: formLeft }" @mousedown="handleLeftDown">
|
||||
<div class="resize-shrink-sidebar" id="approval-form-left" title="收缩侧边栏">
|
||||
<span class="shrink-sidebar-text">⋮</span>
|
||||
</div>
|
||||
<div class="left-title">
|
||||
<NodeHead :nodeName="t('表单信息')" v-show="showPanel" />
|
||||
<div @click="changeShowPanel" class="in-or-out">
|
||||
<component :is="fewerPanelComponent" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-box">
|
||||
<div
|
||||
v-for="(item, index) in forms.configs"
|
||||
:key="index"
|
||||
:class="activeIndex == index ? 'form-name actived' : 'form-name'"
|
||||
>
|
||||
<span :class="item.validate ? 'dot' : 'dot validate'"></span>
|
||||
<div class="icon-box"> <IconFontSymbol icon="formItem" /> </div
|
||||
><span @click="changeActiveIndex(index)" v-show="showPanel">{{ item.formName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-right" :style="{ paddingLeft: formRight }">
|
||||
<div v-for="(item, index) in forms.configs" :key="index" :tab="item.formName">
|
||||
<div v-show="activeIndex == index">
|
||||
<SystemForm
|
||||
class="form-box"
|
||||
v-if="item.formType == FormType.SYSTEM"
|
||||
:systemComponent="item.systemComponent"
|
||||
:isViewProcess="props.disabled"
|
||||
:formModel="item.formModel"
|
||||
:workflowConfig="item"
|
||||
:ref="setItemRef"
|
||||
/>
|
||||
<SimpleForm
|
||||
v-else-if="item.formType == FormType.CUSTOM"
|
||||
class="form-box"
|
||||
:ref="setItemRef"
|
||||
:formProps="item.formProps"
|
||||
:formModel="item.formModel"
|
||||
:isWorkFlow="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import SimpleForm from '/@/components/SimpleForm/src/SimpleForm.vue';
|
||||
import { FewerLeft, FewerRight } from '/@/components/ModalPanel';
|
||||
import { NodeHead } from '/@/components/ModalPanel/index';
|
||||
import IconFontSymbol from '/@/components/IconFontSymbol/Index.vue';
|
||||
import { onBeforeUpdate, nextTick, onMounted, reactive, computed, ref } from 'vue';
|
||||
import { TaskApproveOpinion, ValidateForms } from '/@/model/workflow/bpmnConfig';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { GeneratorConfig } from '/@/model/generator/generatorConfig';
|
||||
import { FormEventColumnConfig } from '/@/model/generator/formEventConfig';
|
||||
import { changeFormJson } from '/@/hooks/web/useWorkFlowForm';
|
||||
import { SystemForm } from '/@/components/SystemForm/index';
|
||||
import { FormType } from '/@/enums/workflowEnum';
|
||||
import { createFormEvent, loadFormEvent, submitFormEvent } from '/@/hooks/web/useFormEvent';
|
||||
const { t } = useI18n();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
disabled: boolean | undefined;
|
||||
formInfos: Array<any>;
|
||||
opinions?: Array<TaskApproveOpinion> | undefined;
|
||||
opinionsComponents?: Array<string> | undefined;
|
||||
formAssignmentData?: null | Recordable;
|
||||
}>(),
|
||||
{
|
||||
disabled: false,
|
||||
formInfos: () => {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const emits = defineEmits(['getFormConfigs']);
|
||||
|
||||
let formLeft = ref('24%');
|
||||
let formRight = ref('calc(24% + 10px)');
|
||||
let showPanel = ref(true);
|
||||
let uploadComponent: { ids: Array<string> } = reactive({ ids: [] });
|
||||
let fewerPanelComponent = computed(() => {
|
||||
return showPanel.value ? FewerLeft : FewerRight;
|
||||
});
|
||||
|
||||
let activeIndex = ref(0);
|
||||
let itemRefs = ref([]) as any;
|
||||
const setItemRef = (el: never) => {
|
||||
itemRefs.value.push(el);
|
||||
};
|
||||
function showRightBox() {
|
||||
formLeft.value = '24%';
|
||||
formRight.value = 'calc(24% + 10px)';
|
||||
}
|
||||
function hideRightBox() {
|
||||
formLeft.value = '58px';
|
||||
formRight.value = '78px';
|
||||
}
|
||||
function changeShowPanel() {
|
||||
showPanel.value = !showPanel.value;
|
||||
if (showPanel.value) {
|
||||
showRightBox();
|
||||
} else {
|
||||
hideRightBox();
|
||||
}
|
||||
}
|
||||
onBeforeUpdate(() => {
|
||||
itemRefs.value = [];
|
||||
});
|
||||
let forms: {
|
||||
formModels: Array<Recordable>;
|
||||
configs: Array<{
|
||||
formName: string;
|
||||
formProps: {};
|
||||
formModel: Recordable;
|
||||
formKey: string;
|
||||
validate: boolean;
|
||||
formType: FormType;
|
||||
workflowPermissions?: Array<any>;
|
||||
opinions?: Array<any>;
|
||||
opinionsComponents?: Array<any>;
|
||||
systemComponent?: {
|
||||
functionalModule: string;
|
||||
functionName: string;
|
||||
functionFormName: string;
|
||||
};
|
||||
formJson?: Array<any>;
|
||||
isOldSystem?: boolean;
|
||||
}>;
|
||||
formEventConfigs: FormEventColumnConfig[];
|
||||
} = reactive({
|
||||
formModels: [],
|
||||
configs: [],
|
||||
formEventConfigs: [],
|
||||
});
|
||||
onMounted(async () => {
|
||||
for await (let element of props.formInfos) {
|
||||
let formModels = {};
|
||||
if (element.formData) {
|
||||
formModels = cloneDeep(element.formData);
|
||||
}
|
||||
// 参数赋值[赋值权限最大]
|
||||
if (props.formAssignmentData) {
|
||||
if (props.formAssignmentData[element.formConfig.formId]) {
|
||||
formModels = { ...formModels, ...props.formAssignmentData[element.formConfig.formId] };
|
||||
}
|
||||
}
|
||||
forms.formModels.push(formModels);
|
||||
// 系统表单
|
||||
if (element.formType == FormType.SYSTEM) {
|
||||
forms.configs.push({
|
||||
formName: element.formConfig.formName,
|
||||
formProps: {},
|
||||
formModel: formModels,
|
||||
formKey: element.formConfig.key,
|
||||
validate: true,
|
||||
formType: element.formType,
|
||||
workflowPermissions: element.formConfig.children,
|
||||
opinions: props.opinions,
|
||||
opinionsComponents: props.opinionsComponents,
|
||||
systemComponent: {
|
||||
functionalModule: element.functionalModule,
|
||||
functionName: element.functionName,
|
||||
functionFormName: 'Form',
|
||||
},
|
||||
formJson: element.formJson,
|
||||
isOldSystem: false,
|
||||
});
|
||||
// 上传组件Id集合
|
||||
setTimeout(() => {
|
||||
getSystemUploadComponentIds();
|
||||
}, 0);
|
||||
} else {
|
||||
const model = JSON.parse(element.formJson) as GeneratorConfig;
|
||||
const { formJson, formEventConfig } = model;
|
||||
if (formEventConfig) {
|
||||
forms.formEventConfigs.push(formEventConfig);
|
||||
|
||||
//初始化表单
|
||||
await createFormEvent(formEventConfig, formModels, true);
|
||||
//加载表单
|
||||
await loadFormEvent(formEventConfig, formModels, true);
|
||||
|
||||
//TODO 暂不放开 工作流没有获取表单数据这个步骤 获取表单数据
|
||||
// getFormDataEvent(formEventConfig, formModels,true);
|
||||
}
|
||||
let formKey = element.formConfig.key;
|
||||
|
||||
let config = {
|
||||
formName: element.formConfig.formName,
|
||||
formProps: {},
|
||||
formModel: {},
|
||||
formKey,
|
||||
validate: true,
|
||||
formType: element.formType,
|
||||
};
|
||||
let isViewProcess = props.disabled;
|
||||
let { buildOptionJson, uploadComponentIds } = changeFormJson(
|
||||
{
|
||||
formJson,
|
||||
formConfigChildren: element.formConfig.children,
|
||||
formConfigKey: element.formConfig.key,
|
||||
opinions: props.opinions,
|
||||
opinionsComponents: props.opinionsComponents,
|
||||
},
|
||||
isViewProcess,
|
||||
uploadComponent.ids,
|
||||
);
|
||||
uploadComponent.ids = uploadComponentIds;
|
||||
if (buildOptionJson.schemas) {
|
||||
config.formProps = buildOptionJson;
|
||||
forms.configs.push(config);
|
||||
}
|
||||
}
|
||||
// });
|
||||
}
|
||||
|
||||
await nextTick();
|
||||
setTimeout(() => {
|
||||
setFormModel();
|
||||
}, 0);
|
||||
emits('getFormConfigs', forms.configs.length ? forms.configs[activeIndex.value] : null);
|
||||
});
|
||||
|
||||
function setFormModel() {
|
||||
for (let index = 0; index < itemRefs.value.length; index++) {
|
||||
if (itemRefs.value[index]) {
|
||||
itemRefs.value[index].setFieldsValue(forms.formModels[index]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setFormData(formData) {
|
||||
await nextTick();
|
||||
forms.formModels = formData;
|
||||
setFormModel();
|
||||
}
|
||||
|
||||
function changeActiveIndex(index: number) {
|
||||
activeIndex.value = index;
|
||||
emits('getFormConfigs', forms.configs[activeIndex.value]);
|
||||
}
|
||||
function getSystemUploadComponentIds() {
|
||||
for (let index = 0; index < itemRefs.value.length; index++) {
|
||||
if (itemRefs.value[index] && itemRefs.value[index].getUploadComponentIds) {
|
||||
let ids = itemRefs.value[index].getUploadComponentIds();
|
||||
uploadComponent.ids = [...uploadComponent.ids, ...ids];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取上传组件的字段值集合
|
||||
function getUploadComponentIds() {
|
||||
return uploadComponent.ids;
|
||||
}
|
||||
async function validateForm() {
|
||||
let validateForms: ValidateForms = [];
|
||||
for (let index = 0; index < itemRefs.value.length; index++) {
|
||||
if (itemRefs.value[index]) {
|
||||
try {
|
||||
await itemRefs.value[index]?.validate();
|
||||
validateForms.push({
|
||||
validate: true,
|
||||
msgs: [],
|
||||
isOldSystem: forms.configs[index].isOldSystem,
|
||||
});
|
||||
forms.configs[index].validate = true;
|
||||
} catch (error: any | Array<{ errors: Array<string>; name: Array<string> }>) {
|
||||
validateForms.push({
|
||||
validate: false,
|
||||
msgs: error?.errorFields,
|
||||
});
|
||||
forms.configs[index].validate = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return validateForms;
|
||||
}
|
||||
|
||||
async function saveDraftData() {
|
||||
let formModes = {};
|
||||
|
||||
for (let index = 0; index < forms.configs.length; index++) {
|
||||
const ele = forms.configs[index];
|
||||
if (ele.formType == FormType.SYSTEM) {
|
||||
let values = await itemRefs.value[index].validate();
|
||||
formModes[ele.formKey] = values;
|
||||
} else {
|
||||
formModes[ele.formKey] = ele.formModel;
|
||||
}
|
||||
}
|
||||
return formModes;
|
||||
}
|
||||
async function getFormModels() {
|
||||
let formModes = {};
|
||||
|
||||
for (let index = 0; index < forms.configs.length; index++) {
|
||||
const ele = forms.configs[index];
|
||||
if (ele.formType == FormType.SYSTEM) {
|
||||
let values = await itemRefs.value[index].workflowSubmit();
|
||||
formModes[ele.formKey] = values;
|
||||
} else {
|
||||
formModes[ele.formKey] = ele.formModel;
|
||||
}
|
||||
}
|
||||
|
||||
// forms.configs.forEach((ele) => {
|
||||
// formModes[ele.formKey] = ele.formModel;
|
||||
// });
|
||||
forms.formEventConfigs.forEach(async (ele, i) => {
|
||||
//此组件 获取数据 就是为了提交表单 所以 表单提交数据 事件 就此处执行
|
||||
await submitFormEvent(ele, forms.configs[i]?.formModel);
|
||||
});
|
||||
return formModes;
|
||||
}
|
||||
function getSystemType() {
|
||||
let system = {};
|
||||
for (let index = 0; index < forms.configs.length; index++) {
|
||||
const ele = forms.configs[index];
|
||||
if (ele.formType == FormType.SYSTEM) {
|
||||
system[ele.formKey] = itemRefs.value[index].getIsOldSystem();
|
||||
}
|
||||
}
|
||||
return system;
|
||||
}
|
||||
function handleLeftDown(e) {
|
||||
let resize = document.getElementById('approval-form-left') as any;
|
||||
let startX = e.clientX;
|
||||
let left = resize?.offsetLeft || 0;
|
||||
|
||||
document.onmousemove = function (e) {
|
||||
let endX = e.clientX;
|
||||
let moveLen = left + (endX - startX);
|
||||
if (moveLen <= 110) {
|
||||
showPanel.value = false;
|
||||
} else {
|
||||
showPanel.value = true;
|
||||
}
|
||||
if (moveLen <= 58) moveLen = 58;
|
||||
formLeft.value = moveLen + 'px';
|
||||
formRight.value = moveLen + 20 + 'px';
|
||||
};
|
||||
document.onmouseup = function () {
|
||||
document.onmousemove = null;
|
||||
document.onmouseup = null;
|
||||
resize.releaseCapture && resize.releaseCapture();
|
||||
};
|
||||
}
|
||||
defineExpose({
|
||||
validateForm,
|
||||
getFormModels,
|
||||
saveDraftData,
|
||||
setFormData,
|
||||
getUploadComponentIds,
|
||||
getSystemType,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form-container {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 100%;
|
||||
|
||||
.form-left {
|
||||
float: left;
|
||||
height: 100vh;
|
||||
box-shadow: 5px 5px 5px rgb(0 0 0 / 10%);
|
||||
z-index: 9998;
|
||||
|
||||
.resize-shrink-sidebar {
|
||||
cursor: col-resize;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 9999;
|
||||
|
||||
.shrink-sidebar-text {
|
||||
padding: 0 2px;
|
||||
background: #f2f2f2;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.left-box {
|
||||
margin-right: 10px;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.form-name {
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
color: rgb(102 102 102 / 99.6%);
|
||||
margin-right: -2px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.actived {
|
||||
border-right: 1px solid #5e95ff;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.validate {
|
||||
background-color: @clear-color;
|
||||
}
|
||||
|
||||
.icon-box {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.left-title {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 10px 4px 10px 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
.in-or-out {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-box {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
|
||||
.form-right {
|
||||
width: 100%;
|
||||
padding-top: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
121
src/views/workflow/task/components/flow/Layout.vue
Normal file
121
src/views/workflow/task/components/flow/Layout.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<template>
|
||||
<div class="wrap">
|
||||
<div class="head"
|
||||
><div class="title"><DesignLogo /><slot name="title"></slot></div
|
||||
><div class="operation"> <slot name="close"></slot></div
|
||||
></div>
|
||||
<div v-if="hasFullSlot" class="full-box"><slot name="full"></slot></div>
|
||||
<div class="box" v-else>
|
||||
<div class="left-box" ref="left">
|
||||
<slot name="left"></slot>
|
||||
</div>
|
||||
<div class="right-box" ref="right">
|
||||
<div class="fewer-panel-box" @click="changeShowPanel">
|
||||
<component :is="fewerPanelComponent" />
|
||||
</div>
|
||||
<div v-show="showPanel" class="right"><slot name="right"></slot></div
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, useSlots } from 'vue';
|
||||
import { FewerLeft, FewerRight } from '/@/components/ModalPanel';
|
||||
import { DesignLogo } from '/@/components/ModalPanel/index';
|
||||
let left = ref();
|
||||
let right = ref();
|
||||
let showPanel = ref(true);
|
||||
const hasFullSlot = computed(() => {
|
||||
return !!useSlots().full;
|
||||
});
|
||||
let fewerPanelComponent = computed(() => {
|
||||
return showPanel.value ? FewerRight : FewerLeft;
|
||||
});
|
||||
function showRightBox() {
|
||||
left.value.style.width = 'calc(100% - 350px)';
|
||||
right.value.style.width = '350px';
|
||||
}
|
||||
function hideRightBox() {
|
||||
left.value.style.width = 'calc(100% - 60px)';
|
||||
right.value.style.width = '60px';
|
||||
}
|
||||
function changeShowPanel() {
|
||||
showPanel.value = !showPanel.value;
|
||||
if (showPanel.value) {
|
||||
showRightBox();
|
||||
} else {
|
||||
hideRightBox();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="less" scoped>
|
||||
.wrap {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 999;
|
||||
background-color: #fff;
|
||||
|
||||
.head {
|
||||
height: 50px;
|
||||
box-shadow: 5px 5px 5px rgb(0 0 0 / 10%);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.operation {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme='dark'] .wrap {
|
||||
background-color: #151515;
|
||||
}
|
||||
|
||||
.full-box {
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.box {
|
||||
display: flex;
|
||||
|
||||
.left-box {
|
||||
width: calc(100% - 350px);
|
||||
padding: 0 20px;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.right-box {
|
||||
box-shadow: -6px 2px 4px rgb(0 0 0 / 10%);
|
||||
padding: 0 10px;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.fewer-panel-box {
|
||||
width: 20px;
|
||||
position: fixed;
|
||||
top: 60px;
|
||||
right: 5px;
|
||||
z-index: 3;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.button-box) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
:deep(.button-box button) {
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
:deep(.clean-icon) {
|
||||
background-color: @clear-color;
|
||||
border-color: @clear-color;
|
||||
}
|
||||
</style>
|
||||
37
src/views/workflow/task/components/flow/LookRelationTask.vue
Normal file
37
src/views/workflow/task/components/flow/LookRelationTask.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<FlowPanel
|
||||
v-if="visible"
|
||||
:tab-position="position ? position : 'top'"
|
||||
:xml="data.xml"
|
||||
:taskRecords="data.taskRecords"
|
||||
:predecessorTasks="[]"
|
||||
:processId="props.processId"
|
||||
position="top"
|
||||
>
|
||||
<FormInformation
|
||||
:opinionsComponents="data.opinionsComponents"
|
||||
:opinions="data.opinions"
|
||||
:formInfos="data.formInfos"
|
||||
:disabled="true"
|
||||
/>
|
||||
</FlowPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FormInformation from './FormInformation.vue';
|
||||
import FlowPanel from './FlowPanel.vue';
|
||||
import { getRelationTaskInfo } from '/@/api/workflow/task';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import userTaskItem from './../../hooks/userTaskItem';
|
||||
let props = defineProps(['taskId', 'position', 'processId']);
|
||||
let visible = ref(false);
|
||||
const { data, initProcessData } = userTaskItem();
|
||||
onMounted(async () => {
|
||||
try {
|
||||
let res = await getRelationTaskInfo(props.taskId);
|
||||
initProcessData(res);
|
||||
visible.value = true;
|
||||
} catch (error) {}
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
37
src/views/workflow/task/components/flow/LookTask.vue
Normal file
37
src/views/workflow/task/components/flow/LookTask.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<FlowPanel
|
||||
v-if="visible"
|
||||
:tab-position="position ? position : 'top'"
|
||||
:xml="data.xml"
|
||||
:taskRecords="data.taskRecords"
|
||||
:predecessorTasks="[]"
|
||||
:processId="props.processId"
|
||||
position="top"
|
||||
>
|
||||
<FormInformation
|
||||
:opinionsComponents="data.opinionsComponents"
|
||||
:opinions="data.opinions"
|
||||
:formInfos="data.formInfos"
|
||||
:disabled="true"
|
||||
/>
|
||||
</FlowPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FormInformation from './FormInformation.vue';
|
||||
import FlowPanel from './FlowPanel.vue';
|
||||
import { getApprovalProcess } from '/@/api/workflow/task';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import userTaskItem from './../../hooks/userTaskItem';
|
||||
let props = defineProps(['position', 'processId', 'taskId']);
|
||||
let visible = ref(false);
|
||||
const { data, initProcessData } = userTaskItem();
|
||||
onMounted(async () => {
|
||||
try {
|
||||
let res = await getApprovalProcess(props.taskId, props.processId);
|
||||
initProcessData(res);
|
||||
visible.value = true;
|
||||
} catch (error) {}
|
||||
});
|
||||
</script>
|
||||
<style lang="less" scoped></style>
|
||||
47
src/views/workflow/task/components/flow/ProcessInfo.vue
Normal file
47
src/views/workflow/task/components/flow/ProcessInfo.vue
Normal file
@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="item"
|
||||
><span class="item-label">{{ t('模板编号:') }}</span
|
||||
><span>{{ item.code }}</span></div
|
||||
>
|
||||
<div class="item"
|
||||
><span class="item-label">{{ t('模板名称:') }}</span
|
||||
><span>{{ item.name }}</span></div
|
||||
>
|
||||
<div class="item"
|
||||
><span class="item-label">{{ t('模板分类:') }}</span
|
||||
><span>{{ item.categoryName }}</span></div
|
||||
>
|
||||
<div class="item"
|
||||
><span class="item-label">{{ t('备注:') }}</span
|
||||
><span>{{ item.remark }}</span></div
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
item: { code: string; name: string; categoryName: string; remark: string };
|
||||
}>(),
|
||||
{
|
||||
item: () => {
|
||||
return { code: '', name: '', categoryName: '', remark: '' };
|
||||
},
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.item {
|
||||
.item-label {
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
display: inline-flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
src/views/workflow/task/components/flow/ProcessInformation.vue
Normal file
145
src/views/workflow/task/components/flow/ProcessInformation.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<!-- 流程信息 -->
|
||||
<div class="flow-record-box">
|
||||
<div id="bpmnCanvas" class="canvas" ref="bpmnCanvas"></div>
|
||||
<div class="flow-record-mark"></div>
|
||||
</div>
|
||||
|
||||
<div class="fixed-bottom">
|
||||
<ZoomInOrOut @in="zoomViewport(false)" @out="zoomViewport(true)" />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import CustomModeler from '/@bpmn/modeler';
|
||||
import { ZoomInOrOut } from '/@/components/ModalPanel';
|
||||
import { getFinishedTask } from '/@/api/workflow/task';
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
xml: string;
|
||||
processId: string;
|
||||
}>(),
|
||||
{
|
||||
xml: '',
|
||||
processId: '',
|
||||
},
|
||||
);
|
||||
const bpmnCanvas = ref();
|
||||
let data: {
|
||||
bpmnViewer: any;
|
||||
zoom: number;
|
||||
xmlString: string;
|
||||
} = reactive({
|
||||
bpmnViewer: null,
|
||||
zoom: 1,
|
||||
xmlString: '',
|
||||
});
|
||||
onMounted(() => {
|
||||
data.xmlString = props.xml;
|
||||
if (data.xmlString) initBpmnModeler();
|
||||
});
|
||||
|
||||
async function initBpmnModeler() {
|
||||
data.bpmnViewer = await new CustomModeler({
|
||||
container: bpmnCanvas.value,
|
||||
additionalModules: [
|
||||
{
|
||||
labelEditingProvider: ['value', ''], //禁用节点编辑
|
||||
paletteProvider: ['value', ''], //禁用/清空左侧工具栏
|
||||
contextPadProvider: ['value', ''], //禁用图形菜单
|
||||
bendpoints: ['value', {}], //禁用连线拖动
|
||||
zoomScroll: ['value', ''], //禁用滚动
|
||||
moveCanvas: ['value', ''], //禁用拖动整个流程图
|
||||
move: ['value', ''], //禁用单个图形拖动
|
||||
},
|
||||
],
|
||||
});
|
||||
await redrawing();
|
||||
if (props.processId) {
|
||||
let res = await getFinishedTask(props.processId);
|
||||
setColors(
|
||||
res.finishedNodes ? res.finishedNodes : [],
|
||||
res.currentNodes ? res.currentNodes : [],
|
||||
);
|
||||
}
|
||||
}
|
||||
async function redrawing() {
|
||||
try {
|
||||
await data.bpmnViewer.importXML(data.xmlString);
|
||||
let canvas = data.bpmnViewer.get('canvas');
|
||||
canvas.zoom('fit-viewport', 'auto');
|
||||
} catch (err) {
|
||||
console.log('err: ', err);
|
||||
}
|
||||
}
|
||||
function setColors(finishedIds: Array<string>, currentIds: Array<string>) {
|
||||
// finishedIds 完成的节点id
|
||||
// currentIds 进行中节点id
|
||||
let modeling = data.bpmnViewer.get('modeling');
|
||||
const elementRegistry = data.bpmnViewer.get('elementRegistry');
|
||||
if (finishedIds.length > 0) {
|
||||
finishedIds.forEach((it) => {
|
||||
let Event = elementRegistry.get(it);
|
||||
|
||||
modeling.setColor(Event, {
|
||||
stroke: 'green',
|
||||
fill: 'white',
|
||||
});
|
||||
});
|
||||
}
|
||||
if (currentIds.length > 0) {
|
||||
currentIds.forEach((it) => {
|
||||
let Event = elementRegistry.get(it);
|
||||
modeling.setColor(Event, {
|
||||
stroke: '#409eff',
|
||||
fill: 'white',
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
function zoomViewport(zoomIn = true) {
|
||||
data.zoom = data.bpmnViewer.get('canvas').zoom();
|
||||
data.zoom += zoomIn ? 0.1 : -0.1;
|
||||
data.bpmnViewer.get('canvas').zoom(data.zoom);
|
||||
}
|
||||
</script>
|
||||
<style lang="less">
|
||||
@import '/@/assets/style/bpmn-js/diagram-js.css';
|
||||
@import '/@/assets/style/bpmn-js/bpmn-font/css/bpmn.css';
|
||||
@import '/@/assets/style/bpmn-js/bpmn-font/css/bpmn-codes.css';
|
||||
@import '/@/assets/style/bpmn-js/bpmn-font/css/bpmn-embedded.css';
|
||||
|
||||
.bjs-powered-by {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.flow-record-box {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
position: relative;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.flow-record-mark {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
/* 画布 */
|
||||
.canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 按钮(放大 缩小 清除) */
|
||||
.fixed-bottom {
|
||||
position: absolute;
|
||||
top: 110px;
|
||||
font-size: 30px;
|
||||
left: 40%;
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
195
src/views/workflow/task/components/flow/SelectApproveUser.vue
Normal file
195
src/views/workflow/task/components/flow/SelectApproveUser.vue
Normal file
@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div v-if="data.visible">
|
||||
<a-tabs>
|
||||
<a-tab-pane key="1" :tab="t('候选人')">
|
||||
<div class="list-page-box" v-if="data.approvedList.length > 0">
|
||||
<UserCard
|
||||
:class="data.approvedIds.includes(user.id) ? 'picked' : 'not-picked'"
|
||||
v-for="(user, userIndex) in data.approvedList"
|
||||
:key="userIndex"
|
||||
:item="user"
|
||||
@click="checkApprovedId(user)"
|
||||
:disabled="user.canRemove ? false : true"
|
||||
>
|
||||
<template #check>
|
||||
<a-checkbox
|
||||
size="small"
|
||||
:checked="data.approvedIds.includes(user.id)"
|
||||
:disabled="user.canRemove ? false : true"
|
||||
/>
|
||||
</template>
|
||||
</UserCard>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="2" :tab="t('已选人员')">
|
||||
<SelectUser
|
||||
v-if="hasMoreBtn"
|
||||
:selectedIds="data.selectedIds"
|
||||
:disabledIds="data.disabledIds"
|
||||
:multiple="true"
|
||||
@change="changeList"
|
||||
>
|
||||
<a-button type="primary">{{ t('更多人员添加') }}</a-button>
|
||||
</SelectUser>
|
||||
<div class="list-page-box" v-if="data.selectedList.length > 0">
|
||||
<UserCard
|
||||
:class="data.selectedIds.includes(user.id) ? 'picked' : 'not-picked'"
|
||||
v-for="(user, userIndex) in data.selectedList"
|
||||
:key="userIndex"
|
||||
:item="user"
|
||||
@click="checked(user)"
|
||||
:disabled="data.disabledIds.includes(user.id)"
|
||||
>
|
||||
<template #check>
|
||||
<a-checkbox size="small" :checked="data.selectedIds.includes(user.id)" />
|
||||
</template>
|
||||
</UserCard>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue';
|
||||
import { getUserMulti } from '/@/api/system/user';
|
||||
import { getApproveUserList } from '/@/api/workflow/task';
|
||||
import { SelectUser } from '/@/components/SelectOrganizational/index';
|
||||
import { UserCard } from '/@/components/SelectOrganizational/index';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
const props = defineProps({
|
||||
schemaId: String,
|
||||
taskId: String,
|
||||
hasMoreBtn: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
const emits = defineEmits(['update:selectIds']);
|
||||
// const emits = defineEmits('change');
|
||||
let data: {
|
||||
visible: boolean;
|
||||
approvedList: Array<{
|
||||
[x: string]: any;
|
||||
id: string;
|
||||
name: string;
|
||||
}>;
|
||||
selectedList: Array<{ id: string; name: string }>;
|
||||
approvedIds: Array<string>;
|
||||
disabledIds: Array<string>;
|
||||
selectedIds: Array<string>;
|
||||
} = reactive({
|
||||
visible: false,
|
||||
approvedList: [],
|
||||
selectedList: [],
|
||||
approvedIds: [],
|
||||
disabledIds: [],
|
||||
selectedIds: [],
|
||||
});
|
||||
onMounted(async () => {
|
||||
if (props.schemaId && props.taskId) {
|
||||
try {
|
||||
let userList = await getApproveUserList(props.schemaId, props.taskId);
|
||||
data.approvedList = cloneDeep(userList);
|
||||
data.approvedIds = data.approvedList.map((ele) => {
|
||||
return ele.id;
|
||||
});
|
||||
data.disabledIds = data.approvedList
|
||||
.filter((ele) => {
|
||||
return !ele.canRemove;
|
||||
})
|
||||
.map((ele) => {
|
||||
return ele.id;
|
||||
});
|
||||
|
||||
data.selectedList = cloneDeep(userList);
|
||||
data.selectedIds = data.selectedList.map((ele) => {
|
||||
return ele.id;
|
||||
});
|
||||
changeData();
|
||||
data.visible = true;
|
||||
} catch (_error) {}
|
||||
}
|
||||
});
|
||||
function checkApprovedId(user) {
|
||||
if (data.disabledIds.includes(user.id)) {
|
||||
return false;
|
||||
}
|
||||
if (data.approvedIds.includes(user.id)) {
|
||||
data.approvedIds.splice(
|
||||
data.approvedIds.findIndex((item) => item === user.id),
|
||||
1,
|
||||
);
|
||||
data.selectedIds.splice(
|
||||
data.selectedIds.findIndex((item) => item === user.id),
|
||||
1,
|
||||
);
|
||||
data.selectedList.splice(
|
||||
data.selectedList.findIndex((item) => item.id === user.id),
|
||||
1,
|
||||
);
|
||||
} else {
|
||||
data.approvedIds.push(user.id);
|
||||
data.selectedIds.push(user.id);
|
||||
data.selectedList.push(user);
|
||||
}
|
||||
changeData();
|
||||
}
|
||||
function checked(user) {
|
||||
if (data.disabledIds.includes(user.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data.selectedIds.includes(user.id)) {
|
||||
data.selectedList.splice(
|
||||
data.selectedList.findIndex((item) => item.id === user.id),
|
||||
1,
|
||||
);
|
||||
data.selectedIds = data.selectedIds.filter((o) => {
|
||||
return o != user.id;
|
||||
});
|
||||
} else {
|
||||
data.selectedList.push(user);
|
||||
data.selectedIds.push(user.id);
|
||||
}
|
||||
|
||||
if (data.approvedIds.includes(user.id)) {
|
||||
data.approvedIds.splice(
|
||||
data.approvedIds.findIndex((item) => item === user.id),
|
||||
1,
|
||||
);
|
||||
} else {
|
||||
data.approvedIds.push(user.id);
|
||||
}
|
||||
changeData();
|
||||
}
|
||||
async function changeList(userIds: Array<string>) {
|
||||
data.selectedList = await getUserMulti(userIds.join(','));
|
||||
data.selectedIds = userIds;
|
||||
userIds.forEach((id) => {
|
||||
if (!data.approvedIds.includes(id)) {
|
||||
data.approvedIds.push(id);
|
||||
}
|
||||
});
|
||||
changeData();
|
||||
}
|
||||
function changeData() {
|
||||
emits('update:selectIds', data.selectedIds);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.box {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.list-page-box {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: auto;
|
||||
padding: 10px 0;
|
||||
}
|
||||
</style>
|
||||
198
src/views/workflow/task/components/flow/SummaryOfAttachments.vue
Normal file
198
src/views/workflow/task/components/flow/SummaryOfAttachments.vue
Normal file
@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 附件汇总 -->
|
||||
<EmptyBox
|
||||
v-if="data.dataSource.length == 0"
|
||||
:title="t('当前无相关附件记录')"
|
||||
:desc="t('流程需发起并在表单中上传附件才会有附件记录产生')"
|
||||
/>
|
||||
<a-table
|
||||
v-else
|
||||
class="p-4"
|
||||
:pagination="false"
|
||||
:dataSource="data.dataSource"
|
||||
:columns="configColumns"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'components'"> {{ t('上传') }} </template>
|
||||
<template v-if="column.dataIndex === 'operation'">
|
||||
<div class="flex">
|
||||
<a-button size="small" type="primary" class="mr-2" @click="preview(record.fileUrl)">{{
|
||||
t('预览')
|
||||
}}</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="download(record.fileUrl, record.fileName)"
|
||||
>{{ t('下载') }}</a-button
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
<a-modal
|
||||
v-model:visible="data.showPreview"
|
||||
:title="t('预览文件')"
|
||||
width="100%"
|
||||
wrap-class-name="full-modal"
|
||||
:footer="null"
|
||||
@ok="data.showPreview = false"
|
||||
>
|
||||
<iframe v-if="data.showPreview" :src="data.fileUrl" class="iframe-box"></iframe>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive } from 'vue';
|
||||
import EmptyBox from './EmptyBox.vue';
|
||||
import { getFileList } from '/@/api/system/file';
|
||||
import { FilePageListModel } from '/@/api/system/file/model';
|
||||
import { downloadByUrl } from '/@/utils/file/download';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
import { getAppEnvConfig } from '/@/utils/env';
|
||||
const { t } = useI18n();
|
||||
const props = withDefaults(defineProps<{ processId: string }>(), {
|
||||
processId: '',
|
||||
});
|
||||
|
||||
const configColumns = [
|
||||
{
|
||||
title: t('序号'),
|
||||
align: 'center',
|
||||
customRender: ({ index }) => `${index + 1}`, // 显示每一行的序号
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: t('附件名称'),
|
||||
dataIndex: 'fileName',
|
||||
sorter: {
|
||||
multiple: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: t('附件格式'),
|
||||
dataIndex: 'fileType',
|
||||
},
|
||||
{
|
||||
title: t('所属组件'),
|
||||
dataIndex: 'components',
|
||||
},
|
||||
{
|
||||
title: t('上传人员'),
|
||||
dataIndex: 'createUserName',
|
||||
},
|
||||
{
|
||||
title: t('上传时间'),
|
||||
dataIndex: 'createDate',
|
||||
},
|
||||
{
|
||||
title: t('操作'),
|
||||
dataIndex: 'operation',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
];
|
||||
const data: {
|
||||
dataSource: Array<FilePageListModel>;
|
||||
showPreview: boolean;
|
||||
fileUrl: string;
|
||||
} = reactive({
|
||||
dataSource: [],
|
||||
showPreview: false,
|
||||
fileUrl: '',
|
||||
});
|
||||
onMounted(async () => {
|
||||
if (props.processId) {
|
||||
try {
|
||||
let res = await getFileList({ processId: props.processId });
|
||||
data.dataSource = res;
|
||||
} catch (error) {}
|
||||
}
|
||||
});
|
||||
async function preview(fileUrl: string) {
|
||||
data.fileUrl = getAppEnvConfig().VITE_GLOB_UPLOAD_PREVIEW + encodeURIComponent(encode(fileUrl));
|
||||
data.showPreview = true;
|
||||
}
|
||||
async function download(fileUrl: string, fileName: string) {
|
||||
downloadByUrl({
|
||||
url: fileUrl,
|
||||
fileName: fileName,
|
||||
});
|
||||
}
|
||||
|
||||
function encode(input: string) {
|
||||
let _keyStr = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||
let output = '';
|
||||
let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
|
||||
let i = 0;
|
||||
input = utf8_encode(input);
|
||||
while (i < input.length) {
|
||||
chr1 = input.charCodeAt(i++);
|
||||
chr2 = input.charCodeAt(i++);
|
||||
chr3 = input.charCodeAt(i++);
|
||||
enc1 = chr1 >> 2;
|
||||
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
|
||||
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
|
||||
enc4 = chr3 & 63;
|
||||
if (isNaN(chr2)) {
|
||||
enc3 = enc4 = 64;
|
||||
} else if (isNaN(chr3)) {
|
||||
enc4 = 64;
|
||||
}
|
||||
output =
|
||||
output +
|
||||
_keyStr.charAt(enc1) +
|
||||
_keyStr.charAt(enc2) +
|
||||
_keyStr.charAt(enc3) +
|
||||
_keyStr.charAt(enc4);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function utf8_encode(input: string) {
|
||||
input = input.replace(/\r\n/g, '\n');
|
||||
let tufters = '';
|
||||
for (let n = 0; n < input.length; n++) {
|
||||
let c = input.charCodeAt(n);
|
||||
if (c < 128) {
|
||||
tufters += String.fromCharCode(c);
|
||||
} else if (c > 127 && c < 2048) {
|
||||
tufters += String.fromCharCode((c >> 6) | 192);
|
||||
tufters += String.fromCharCode((c & 63) | 128);
|
||||
} else {
|
||||
tufters += String.fromCharCode((c >> 12) | 224);
|
||||
tufters += String.fromCharCode(((c >> 6) & 63) | 128);
|
||||
tufters += String.fromCharCode((c & 63) | 128);
|
||||
}
|
||||
}
|
||||
return tufters;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.iframe-box {
|
||||
width: 100%;
|
||||
height: 80vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.full-modal {
|
||||
.ant-modal {
|
||||
max-width: 100%;
|
||||
top: 0;
|
||||
padding-bottom: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 80vh;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
src/views/workflow/task/components/flow/TransferUser.vue
Normal file
46
src/views/workflow/task/components/flow/TransferUser.vue
Normal file
@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<SelectUser :selectedIds="[]" :multiple="false" @change="submit">
|
||||
<a-button type="primary" style="width: 100%" class="mr-2">{{ t('转办') }}</a-button>
|
||||
</SelectUser>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SelectUser } from '/@/components/SelectOrganizational/index';
|
||||
import { postTransfer } from '/@/api/workflow/task';
|
||||
import { notification } from 'ant-design-vue';
|
||||
import { useI18n } from '/@/hooks/web/useI18n';
|
||||
const { t } = useI18n();
|
||||
const props = withDefaults(defineProps<{ taskId: string | undefined }>(), {
|
||||
taskId: '',
|
||||
});
|
||||
const emits = defineEmits(['close']);
|
||||
async function submit(ids: Array<string>) {
|
||||
if (ids.length === 1) {
|
||||
try {
|
||||
let res = await postTransfer(props.taskId, ids[0]);
|
||||
if (res) {
|
||||
notification.open({
|
||||
type: 'success',
|
||||
message: t('转办'),
|
||||
description: t('转办成功'),
|
||||
});
|
||||
emits('close');
|
||||
}
|
||||
} catch (error) {
|
||||
notification.open({
|
||||
type: 'error',
|
||||
message: t('转办'),
|
||||
description: t('转办失败:') + error,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
notification.open({
|
||||
type: 'error',
|
||||
message: t('转办'),
|
||||
description: t('转办失败'),
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user