初始版本提交

This commit is contained in:
yaoyn
2024-02-05 09:15:37 +08:00
parent b52d4414be
commit 445292105f
1848 changed files with 236859 additions and 75 deletions

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>