初始版本提交

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,185 @@
<template>
<PageLayout
:layoutClass="
currentRoute.path == '/workflow/launch'
? '!p-2'
: currentRoute.path == '/task/processtasks'
? '!p-0 !pr-7px'
: ''
"
:title="t('流程模板列表')"
:searchConfig="searchConfig"
@search="search"
@scroll-height="scrollHeight"
>
<template #left>
<BasicTree
search
:title="t('流程模板分类')"
:clickRowToExpand="true"
:treeData="data.treeData"
:fieldNames="{ key: 'id', title: 'name' }"
@select="handleSelect"
/>
</template>
<template #search> </template>
<template #right>
<div
v-if="data.list.length > 0"
:style="{ overflowY: 'auto', height: tableOptions.scrollHeight + 30 + 'px' }"
>
<div class="list-page-box">
<TemplateCard
v-for="(item, index) in data.list"
:key="index"
:item="item"
@click="launchProcess(item.id)"
/>
</div>
<div class="page-box">
<a-pagination
v-model:current="data.pagination.current"
:total="data.pagination.total"
v-model:pageSize="data.pagination.pageSize"
:pageSizeOptions="['18', '36', '54', '72']"
:showSizeChanger="true"
:show-total="(total) => t(`共 {total} 条数据`, { total })"
@change="getList"
/></div>
</div>
<div v-else>
<EmptyBox />
</div>
<LaunchProcess
v-if="visibleLaunchProcess"
:schemaId="data.schemaId"
@close="visibleLaunchProcess = false"
/>
</template>
</PageLayout>
</template>
<script setup lang="ts">
import { onMounted, reactive, ref, unref, watch } from 'vue';
import TemplateCard from '/@bpmn/components/card/TemplateCard.vue';
import { EmptyBox } from '/@/components/ModalPanel/index';
import { getDesignPage } from '/@/api/workflow/design';
import { WorkflowPageModel } from '/@/api/workflow/model';
import { getDicDetailList } from '/@/api/system/dic';
import { BasicTree, TreeItem } from '/@/components/Tree';
import { PageLayout } from '/@/components/ModalPanel';
import LaunchProcess from './components/LaunchProcess.vue';
import { FlowCategory } from '/@/enums/workflowEnum';
import userTableScrollHeight from '/@/hooks/setting/userTableScrollHeight';
import { useRouter } from 'vue-router';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const { currentRoute } = useRouter();
const searchConfig = [
{
field: 'code',
label: t('模板编码'),
type: 'input',
},
{
field: 'name',
label: t('模板名称'),
type: 'input',
},
];
const { tableOptions, scrollHeight } = userTableScrollHeight();
let visibleLaunchProcess = ref(false);
let data: {
list: Array<WorkflowPageModel>;
treeData: Array<TreeItem>;
schemaId: string;
selectDeptId: string;
pagination: { current: number; total: number; pageSize: number };
} = reactive({
list: [],
treeData: [],
schemaId: '',
selectDeptId: '',
pagination: {
current: 1,
total: 0,
pageSize: 18,
},
});
watch(
() => unref(currentRoute),
(val) => {
if (val.path == '/workflow/launch') init();
},
{ deep: true },
);
onMounted(() => {
getCategoryTree();
init();
});
async function init() {
data.pagination.current = 1;
data.pagination.total = 0;
data.selectDeptId = '';
await getList();
}
async function getCategoryTree() {
let res = (await getDicDetailList({
itemId: FlowCategory.ID,
})) as unknown as TreeItem[];
data.treeData = res.map((ele) => {
ele.icon = 'ant-design:tags-outlined';
return ele;
});
}
async function getList(params?: any) {
const searchParams = {
...{
limit: data.pagination.current,
size: data.pagination.pageSize,
},
...{ category: data.selectDeptId },
...params,
...{ enabledMark: 1 }, // 流程发起list 需要隐藏 禁用的模板[enabledMark=1]
};
try {
let res = await getDesignPage(searchParams);
data.pagination.total = res.total;
data.list = res.list;
} catch (error) {}
}
function handleSelect(deptIds) {
data.selectDeptId = deptIds[0];
getList();
}
async function launchProcess(id: string) {
data.schemaId = id;
visibleLaunchProcess.value = true;
}
function search(params: any) {
data.pagination.current = 1;
getList(params);
}
</script>
<style lang="less" scoped>
.list-page-box {
display: flex;
flex-wrap: wrap;
}
:deep(.list-item) {
max-width: 320px;
}
.page-box {
position: absolute;
bottom: 20px;
right: 20px;
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<PageWrapper dense contentFullHeight fixedHeight>
<BasicTable @register="registerTable">
<template #toolbar>
<a-button @click.stop="add" type="primary" v-auth="'delegate:add'">{{
t('新增')
}}</a-button>
</template>
<template #action="{ record }">
<TableAction
:actions="[
{
icon: 'clarity:note-edit-line',
auth: 'delegate:edit',
onClick: edit.bind(null, record.id),
},
{
icon: 'ant-design:delete-outlined',
auth: 'delegate:delete',
color: 'error',
popConfirm: {
title: t('是否确认删除'),
confirm: handleDelete.bind(null, record.id),
},
},
]"
/>
</template>
</BasicTable>
<DelegateProcess v-if="delegateData.visible" :id="delegateData.id" @close="close" />
</PageWrapper>
</template>
<script lang="ts" setup>
import { reactive } from 'vue';
import DelegateProcess from './components/DelegateProcess.vue';
import { getProcessDelegatePage, deleteDelegate } from '/@/api/workflow/delegate';
import { notification } from 'ant-design-vue';
import { BasicTable, useTable, TableAction, FormSchema, BasicColumn } from '/@/components/Table';
import { PageWrapper } from '/@/components/Page';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const configColumns: BasicColumn[] = [
{
title: t('被委托人'),
dataIndex: 'delegateUserNames',
width: 180,
align: 'left',
sorter: {
multiple: 1,
},
},
{
title: t('开始时间'),
dataIndex: 'startTime',
width: 160,
align: 'left',
sorter: {
multiple: 2,
},
},
{
title: t('结束时间'),
dataIndex: 'endTime',
width: 160,
align: 'left',
sorter: {
multiple: 3,
},
},
{
title: t('委托人'),
dataIndex: 'delegator',
width: 120,
align: 'left',
sorter: {
multiple: 3,
},
},
{
title: t('创建时间'),
align: 'left',
dataIndex: 'createDate',
},
{
title: t('委托说明'),
align: 'left',
dataIndex: 'remark',
},
];
const searchFormSchema: FormSchema[] = [
{
field: 'keyword',
label: t('关键字'),
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: t('请输入关键字'),
},
},
];
let delegateData = reactive({
id: '',
visible: false,
});
const [registerTable, { reload }] = useTable({
title: t('流程委托列表'),
api: getProcessDelegatePage,
columns: configColumns,
formConfig: {
rowProps: {
gutter: 16,
},
schemas: searchFormSchema,
showResetButton: false,
},
striped: false,
useSearchForm: true,
showTableSetting: true,
actionColumn: {
width: 80,
title: t('操作'),
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
tableSetting: {
size: false,
setting: false,
},
});
function add() {
delegateData.id = '';
delegateData.visible = true;
}
function edit(id: string) {
delegateData.id = id;
delegateData.visible = true;
}
function close() {
delegateData.visible = false;
reload();
}
async function handleDelete(id: string) {
try {
let res = await deleteDelegate([id]);
if (res) {
notification.open({
type: 'success',
message: t('删除'),
description: t('删除成功'),
});
reload();
} else {
notification.open({
type: 'error',
message: t('删除'),
description: t('删除失败'),
});
}
} catch (error) {}
}
</script>

View File

@ -0,0 +1,307 @@
<template>
<PageWrapper dense contentFullHeight fixedHeight contentClass="flex">
<div
class="w-1/4 xl:w-1/5 overflow-hidden bg-white h-full"
:style="{ 'border-right': '1px solid #e5e7eb' }"
>
<BasicTree
:title="t('流程监控状态')"
:clickRowToExpand="true"
:treeData="treeData"
:fieldNames="{ key: 'id', title: 'name' }"
@select="handleSelect"
:selectedKeys="selectedKeys"
/>
</div>
<BasicTable
@register="registerTable"
class="w-3/4 xl:w-4/5"
@selection-change="selectionChange"
>
<template #toolbar>
<LookProcess :taskId="taskId" :processId="processId" @close="reload"
><a-button v-auth="'monitor:view'">{{ t('查看') }}</a-button></LookProcess
>
<a-button v-auth="'monitor:appointedAuditor'" @click="approveUser" v-if="data.type === 0">{{
t('指派审核人')
}}</a-button>
<a-button v-auth="'monitor:pending'" @click="setSuspended" v-if="data.type === 0">{{
suspendedTitle
}}</a-button>
<a-button v-auth="'monitor:delete'" @click="deleteFlow" v-if="data.type === 0">{{
t('删除流程')
}}</a-button>
</template>
<template #status="{ record }">
<Tag color="warning" v-if="record.status == ProcessMonitorStatus.SUSPENDED">{{
t('挂起')
}}</Tag>
<Tag color="processing" v-if="record.status == ProcessMonitorStatus.ACTIVE">{{
t('活动中')
}}</Tag>
<Tag color="success" v-if="record.status == ProcessMonitorStatus.COMPLETED">{{
t('完成')
}}</Tag>
<Tag color="error" v-if="record.status == ProcessMonitorStatus.INTERNALLY_TERMINATED">{{
t('内部终止')
}}</Tag>
</template>
<template #currentProgress="{ record }">
<a-progress v-if="record.currentProgress" :percent="record.currentProgress" size="small" />
</template>
</BasicTable>
<!-- 指派审核人 -->
<ApproveProcessMonitorUser
v-if="data.approvedUserVisible"
:taskId="taskId"
:schemaId="schemaId"
:title="approvedUserTitle"
@close="
() => {
data.approvedUserVisible = false;
reload();
}
"
/>
</PageWrapper>
</template>
<script lang="ts" setup>
import { createVNode, reactive, ref } from 'vue';
import { BasicTree } from '/@/components/Tree';
import { PageWrapper } from '/@/components/Page';
import LookProcess from './components/LookProcess.vue';
import ApproveProcessMonitorUser from './components/flow/ApproveProcessMonitorUser.vue';
import { deleteWorkflow, getProcessMonitorPage, postSetSuspended } from '/@/api/workflow/monitor';
import { Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import { notification, Tag } from 'ant-design-vue';
import { ProcessMonitorStatus } from '/@/enums/workflowEnum';
import { BasicTable, useTable, FormSchema, BasicColumn } from '/@/components/Table';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const configColumns: BasicColumn[] = [
{
title: t('流水号'),
dataIndex: 'serialNumber',
sorter: {
multiple: 1,
},
align: 'left',
},
{
title: t('任务'),
dataIndex: 'currentTaskName',
width: 120,
sorter: {
multiple: 2,
},
align: 'left',
},
{
title: t('标题'),
dataIndex: 'schemaName',
width: 120,
sorter: {
multiple: 3,
},
align: 'left',
},
{
title: t('状态'),
dataIndex: 'status',
sorter: {
multiple: 4,
},
align: 'left',
slots: { customRender: 'status' },
},
// {
// title: '状态详情',
// dataIndex: 'statusMessage',
// },
{
title: t('当前进度'),
dataIndex: 'currentProgress',
align: 'left',
slots: { customRender: 'currentProgress' },
},
{
title: t('发起人'),
dataIndex: 'originator',
align: 'left',
},
{
title: t('时间'),
width: 160,
dataIndex: 'createDate',
align: 'left',
},
];
const searchFormSchema: FormSchema[] = [
{
field: 'keyword',
label: t('关键字'),
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: t('请输入关键字'),
},
},
{
field: 'searchDate',
label: t('时间范围'),
component: 'RangePicker',
colProps: { span: 8 },
},
];
const treeData = [
{
key: 0,
id: 0,
name: t('未完成'),
icon: 'ant-design:profile-outlined',
},
{
key: 1,
id: 1,
name: t('已完成'),
icon: 'ant-design:profile-outlined',
},
];
const selectedKeys = ref([0]);
const [registerTable, { reload, getSelectRows, clearSelectedRowKeys }] = useTable({
title: t('流程监控列表'),
api: getProcessMonitorPage,
rowKey: 'processId',
columns: configColumns,
formConfig: {
rowProps: {
gutter: 16,
},
schemas: searchFormSchema,
fieldMapToTime: [['searchDate', ['startTime', 'endTime'], 'YYYY-MM-DD', true]],
showResetButton: false,
},
rowSelection: {
type: 'radio',
},
beforeFetch: (params) => {
//发送请求默认新增 左边树结构所选机构id
return { ...params, type: data.type };
},
useSearchForm: true,
showTableSetting: true,
striped: false,
pagination: {
pageSize: 18,
},
tableSetting: {
size: false,
setting: false,
},
});
let data: {
type: number;
approvedUserVisible: boolean;
} = reactive({
type: 0,
approvedUserVisible: false,
});
const processId = ref('');
const taskId = ref('');
const schemaId = ref('');
const approvedUserTitle = ref(t('任务'));
const suspendedTitle = ref(t('挂起'));
function selectionChange({ keys, rows }) {
if (keys?.length > 0) {
processId.value = rows[0].processId;
taskId.value = rows[0].taskId;
schemaId.value = rows[0].schemaId;
approvedUserTitle.value = t('任务-') + rows[0].schemaName + '-' + rows[0].currentTaskName;
suspendedTitle.value =
rows[0].status && rows[0].status === ProcessMonitorStatus.SUSPENDED ? t('恢复') : t('挂起');
} else {
processId.value = '';
taskId.value = '';
schemaId.value = '';
approvedUserTitle.value = t('任务');
suspendedTitle.value = t('挂起');
}
}
async function setSuspended() {
let row = checkSelectSingleRow();
if (row) {
let res = await postSetSuspended(row.processId);
if (res) {
reload();
notification.open({
type: 'success',
message: suspendedTitle.value,
description: t('成功'),
});
}
}
clearSelectedRowKeys();
}
function deleteFlow() {
let row = checkSelectSingleRow('请先选择一行后再进行操作。');
if (row) {
Modal.confirm({
title: t('提示'),
icon: createVNode(ExclamationCircleOutlined),
content: t('请确认是否删除该流程?删除后无法进行恢复。'),
okText: t('确定'),
okType: 'danger',
cancelText: t('取消'),
onOk() {
if (row.processId) {
deleteWorkflow(row.processId).then((res) => {
if (res) {
reload();
notification.open({
type: 'success',
message: t('删除成功'),
description: t('成功'),
});
}
});
}
},
onCancel() {},
});
}
clearSelectedRowKeys();
}
function handleSelect(_, e) {
clearSelectedRowKeys();
data.type = e.node.key;
selectedKeys.value = [e.node.key];
reload();
}
function approveUser() {
let row = checkSelectSingleRow();
if (row) {
if (row.taskId) {
data.approvedUserVisible = true;
}
}
}
function checkSelectSingleRow(tip?) {
const selectRows: any = getSelectRows();
if (selectRows.length > 0) {
return selectRows[0];
} else {
notification.open({
type: 'error',
message: t('流程'),
description: t(tip || '请选择一个流程进行'),
});
return false;
}
}
</script>

View File

@ -0,0 +1,124 @@
<template>
<PageWrapper dense contentFullHeight fixedHeight contentClass="flex">
<div
class="w-1/4 xl:w-1/5 overflow-hidden bg-white h-full"
:style="{ 'border-right': '1px solid #e5e7eb' }"
>
<BasicTree
:title="t('流程任务状态')"
:clickRowToExpand="true"
:treeData="treeData"
:fieldNames="{ key: 'id', title: 'name' }"
@select="handleSelect"
:selectedKeys="selectedKeys"
/>
</div>
<div class="w-3/4 xl:w-4/5">
<component :is="data.componentName" />
</div>
</PageWrapper>
</template>
<script setup lang="ts">
import { reactive, shallowRef, defineAsyncComponent, unref, ref } from 'vue';
import { PageWrapper } from '/@/components/Page';
import { BasicTree } from '/@/components/Tree';
import ToDoTasks from './components/processTasks/ToDoTasks.vue';
import { useRouter } from 'vue-router';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const { currentRoute } = useRouter();
const FlowLaunch = defineAsyncComponent({
loader: () => import('./FlowLaunch.vue'),
});
const MyProcess = defineAsyncComponent({
loader: () => import('./components/processTasks/MyProcess.vue'),
});
const TaskDone = defineAsyncComponent({
loader: () => import('./components/processTasks/TaskDone.vue'),
});
const RecycleBin = defineAsyncComponent({
loader: () => import('./components/processTasks/RecycleBin.vue'),
});
const MyCirculation = defineAsyncComponent({
loader: () => import('./components/processTasks/MyCirculation.vue'),
});
const Drafts = defineAsyncComponent({
loader: () => import('./components/processTasks/Drafts.vue'),
});
const componentByType: Map<string, any> = new Map([
['ToDoTasks', ToDoTasks],
['FlowLaunch', FlowLaunch],
['TaskDone', TaskDone],
['MyProcess', MyProcess],
['RecycleBin', RecycleBin],
['MyCirculation', MyCirculation],
['Drafts', Drafts],
]);
const treeData = [
{
key: 'ToDoTasks',
id: 'ToDoTasks',
name: t('待办任务'),
icon: 'ant-design:profile-outlined',
},
{
key: 'FlowLaunch',
id: 'FlowLaunch',
name: t('发起流程'),
icon: 'ant-design:profile-outlined',
},
{
key: 'TaskDone',
id: 'TaskDone',
name: t('已办任务'),
icon: 'ant-design:profile-outlined',
},
{
key: 'MyProcess',
id: 'MyProcess',
name: t('我的流程'),
icon: 'ant-design:profile-outlined',
},
{
key: 'MyCirculation',
id: 'MyCirculation',
name: t('我的传阅'),
icon: 'ant-design:profile-outlined',
},
{
key: 'Drafts',
id: 'Drafts',
name: t('草稿箱'),
icon: 'ant-design:profile-outlined',
},
{
key: 'RecycleBin',
id: 'RecycleBin',
name: t('回收站'),
icon: 'ant-design:profile-outlined',
},
];
const selectedKeys = ref(['ToDoTasks']);
let id = unref(currentRoute).query.name;
let data = reactive({
componentName: shallowRef(ToDoTasks),
});
if (id) {
selectedKeys.value = [id.toString()];
handleSelect([id]);
}
function handleSelect(componentRow) {
if (componentRow.length > 0 && componentRow[0]) {
data.componentName = componentByType.has(componentRow[0])
? componentByType.get(componentRow[0])
: ToDoTasks;
}
}
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,393 @@
<template>
<PageWrapper dense contentFullHeight fixedHeight>
<BasicTable @register="registerTable" @selection-change="selectionChange">
<template #toolbar>
<a-button @click.stop="add" type="primary" v-auth="'PublicStamp:add'">{{
t('新增')
}}</a-button>
<a-button @click="setEnabled" type="primary" v-auth="'PublicStamp:disable'">{{
enabledTitle
}}</a-button>
<a-button
v-if="stampInfo.type == StampType.PRIVATE_SIGNATURE"
@click.stop="setDefault"
type="primary"
v-auth="'PublicStamp:defaultStamp'"
>{{ t('设置默认签章') }}</a-button
>
<span v-if="stampInfo.type == StampType.PUBLIC_SIGNATURE && userName == 'admin'">
<SelectUser
style="display: inline"
:visible="stampInfo.id ? true : false"
:selectedIds="maintainIds"
:multiple="true"
@change="designated"
>
<a-button
type="primary"
@click="checkSingleRow"
v-auth="'PublicStamp:designMaintainPersonnel'"
>{{ t('指定维护人员') }}
</a-button>
</SelectUser>
</span>
</template>
<template #action="{ record }">
<TableAction
:actions="[
{
icon: 'ant-design:user-add-outlined',
auth: 'PublicStamp:addPeople',
disabled: isDisabled(record.maintain),
onClick: handleAddUser.bind(null, record),
ifShow: stampInfo.type == StampType.PUBLIC_SIGNATURE,
},
{
icon: 'clarity:note-edit-line',
auth: 'PublicStamp:edit',
disabled: isDisabled(record.maintain),
onClick: edit.bind(null, record),
},
{
icon: 'ant-design:delete-outlined',
auth: 'PublicStamp:delete',
color: 'error',
disabled: isDisabled(record.maintain),
popConfirm: {
title: t('是否确认删除'),
confirm: handleDelete.bind(null, record.id),
},
},
]"
/>
</template>
</BasicTable>
<StampDetail
v-if="stampInfo.visible"
:info="stampInfo.info"
:type="stampInfo.type"
:id="stampInfo.id"
@close="close"
/>
<SelectMember
v-if="visible"
:visible="visible"
:multiple="true"
:selectedIds="selectedUserIds"
@close="
() => {
visible = false;
}
"
@change="handleUserpost"
/>
</PageWrapper>
</template>
<script lang="ts" setup>
import StampDetail from './components/stamp/StampInfo.vue';
import {
getStampList,
deleteStamp,
EnabledStamp,
setDefaultStamp,
addMaintain,
getStampMember,
addMember,
} from '/@/api/workflow/stamp';
import { StampInfo } from '/@/api/workflow/model';
import { SelectUser, SelectMember } from '/@/components/SelectOrganizational/index';
import { StampType } from '/@/enums/workflowEnum';
import { useRouter } from 'vue-router';
import { notification, Switch, Tag, Image } from 'ant-design-vue';
import { ref, onMounted, computed, reactive, h } from 'vue';
import { BasicTable, useTable, TableAction, FormSchema, BasicColumn } from '/@/components/Table';
import { PageWrapper } from '/@/components/Page';
import { useUserStore } from '/@/store/modules/user';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const configColumns: BasicColumn[] = [
{
title: t('签章名字'),
dataIndex: 'name',
width: 180,
align: 'left',
sorter: {
multiple: 1,
},
},
{
title: t('签章分类'),
dataIndex: 'stampCategoryName',
width: 160,
align: 'left',
sorter: {
multiple: 2,
},
},
{
title: t('图片'),
dataIndex: 'fileUrl',
width: 160,
align: 'left',
customRender: ({ record }) => {
return h(Image, {
src: record.fileUrl,
width: '60px',
height: '22px',
});
},
},
{
title: t('状态'),
dataIndex: 'enabledMark',
width: 120,
sorter: {
multiple: 3,
},
align: 'left',
customRender: ({ record }) => {
return h(Switch, {
checked: record.enabledMark === 1,
checkedChildren: t('已启用'),
unCheckedChildren: t('已禁用'),
});
},
},
{
title: t('默认签章'),
dataIndex: 'isDefault',
width: 120,
align: 'left',
customRender: ({ record }) => {
return record.isDefault == 1 ? h(Tag, { color: 'processing' }, () => t('默认')) : h('div');
},
},
{
title: t('备注'),
align: 'left',
dataIndex: 'remark',
},
];
const searchFormSchema: FormSchema[] = [
{
field: 'keyword',
label: t('关键字'),
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: t('请输入关键字'),
},
},
];
const visible = ref<boolean>(false);
const selectedUserIds = ref<string[]>([]);
const userStore = useUserStore();
const title = computed(() => {
return stampInfo.type == StampType.PRIVATE_SIGNATURE ? t('电子签章列表') : t('公共签章列表');
});
const actionColumn = computed(() => {
return {
width: stampInfo.type == StampType.PRIVATE_SIGNATURE ? 80 : 120,
title: t('操作'),
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
};
});
const userName = computed(() => {
return userStore.getUserInfo.userName;
});
let stampInfo: {
type: StampType;
id: string;
visible: boolean;
info: StampInfo | undefined;
} = reactive({
id: '',
info: undefined,
visible: false,
type: StampType.PRIVATE_SIGNATURE,
});
const [registerTable, { reload, getSelectRows, clearSelectedRowKeys, setColumns, setProps }] =
useTable({
title: title,
api: getStampList,
rowKey: 'id',
columns: [],
formConfig: {
rowProps: {
gutter: 16,
},
schemas: searchFormSchema,
showResetButton: false,
},
rowSelection: {
type: 'radio',
},
beforeFetch: (params) => {
//发送请求默认新增 左边树结构所选区域id
return { ...params, stampType: stampInfo.type };
},
striped: false,
useSearchForm: true,
showTableSetting: true,
tableSetting: {
size: false,
setting: false,
},
});
onMounted(async () => {
const { currentRoute } = useRouter();
stampInfo.type =
currentRoute.value.name == 'PublicStamp'
? StampType.PUBLIC_SIGNATURE
: StampType.PRIVATE_SIGNATURE;
let column = configColumns;
if (currentRoute.value.name == 'PublicStamp') {
column = column.filter((o) => {
return o.dataIndex != 'isDefault';
});
}
setProps({
actionColumn: actionColumn.value,
});
setColumns(column);
});
const maintainIds = ref<string[]>([]);
const enabledTitle = ref<string>(t('启用'));
function isDisabled(ids) {
if (stampInfo.type == 1) {
let arr = ids ? ids.split(',') : [];
return userName.value == 'admin' ? false : !arr.includes(userStore.getUserInfo.id);
} else {
return false;
}
}
function selectionChange({ keys, rows }) {
if (keys?.length > 0) {
enabledTitle.value = rows[0].enabledMark === 1 ? t('禁用') : t('启用');
} else {
enabledTitle.value = t('启用');
}
}
function add() {
stampInfo.id = '';
stampInfo.info = undefined;
stampInfo.visible = true;
}
function edit(row: StampInfo) {
if (row.id) stampInfo.id = row.id;
stampInfo.info = row;
stampInfo.visible = true;
}
function close() {
stampInfo.visible = false;
reload();
}
async function handleDelete(id: string) {
try {
let res = await deleteStamp([id]);
save(res, t('删除'));
} catch (error) {}
reload();
}
async function setEnabled() {
let rows = warning();
if (!rows) {
return;
}
try {
let res = await EnabledStamp(rows.id);
save(res, enabledTitle.value);
clearSelectedRowKeys();
} catch (error) {}
reload();
}
async function setDefault() {
let rows = warning();
if (!rows) {
return;
}
try {
let res = await setDefaultStamp(rows.id);
save(res, t('默认签章'));
clearSelectedRowKeys();
} catch (error) {}
reload();
}
async function designated(ids) {
try {
let res = await addMaintain(stampInfo.id, ids);
save(res, t('指定维护人员'));
clearSelectedRowKeys();
} catch (error) {}
reload();
}
function save(res: boolean, title: string) {
if (res) {
notification.open({
type: 'success',
message: title,
description: t('成功'),
});
} else {
notification.open({
type: 'error',
message: title,
description: t('失败'),
});
}
}
function checkSingleRow() {
let rows = warning();
if (rows) {
stampInfo.id = rows.id;
maintainIds.value = rows.maintain ? rows.maintain.split(',') : [];
} else {
stampInfo.id = '';
maintainIds.value = [];
}
}
function warning() {
const selectRows = getSelectRows();
if (selectRows.length === 0) {
notification.warning({
message: t('警告'),
description: t('必须选中一行!'),
}); //提示消息
return false;
} else {
return selectRows[0];
}
}
function handleAddUser(record: Recordable) {
stampInfo.id = record.id;
getStampMember(record.id).then((res) => {
selectedUserIds.value = res.map((it) => {
return it.id;
});
visible.value = true;
});
}
function handleUserpost(v) {
addMember(stampInfo.id, v).then(() => {
notification.success({
message: t('添加成员成功'),
description: t('成功'),
});
});
}
</script>

View File

@ -0,0 +1,581 @@
<template>
<span @click="approval"
><slot></slot>
<LoadingBox v-if="showLoading" />
<ProcessLayout class="wrap" v-if="showVisible" @click.stop="">
<template #title> {{ t('审批流程') }}{{ data.item.name }} </template>
<template #close>
<a-button type="primary" class="clean-icon" @click.stop="close">{{ t('关闭') }}</a-button>
</template>
<template #left>
<FlowPanel
:xml="data.xml"
:taskRecords="data.taskRecords"
:predecessorTasks="selectedPredecessorTasks"
:processId="props.processId"
position="top"
>
<FormInformation
:opinionsComponents="data.opinionsComponents"
:opinions="data.opinions"
:disabled="false"
:formInfos="data.formInfos"
:formAssignmentData="data.formAssignmentData"
ref="formInformation"
@get-form-configs="(config) => (formConfigs = config)"
/>
</FlowPanel>
</template>
<template #right>
<a-tabs>
<a-tab-pane key="1" :tab="t('审批信息')">
<NodeHead :nodeName="t('基础信息')" />
<div class="description-box">
<ProcessInfo class="item-box" :item="data.item">
<NodeHead :nodeName="t('审批信息')" />
<div
class="text-box"
v-if="approvalData.buttonConfigs && approvalData.buttonConfigs.length > 0"
>
<div class="text-label">{{ t('审批结果:') }}</div>
<span class="flex-1">
<a-radio-group
class="approve-group"
v-model:value="approvedType"
name="approvedType"
@change="changeApprovedType"
>
<span v-for="(item, index) in approvalData.buttonConfigs" :key="index">
<a-radio
v-if="item.approveType !== ApproveType.OTHER"
:value="item.approveType"
>
{{ item.buttonName }}
</a-radio>
</span>
</a-radio-group>
<a-radio-group
class="approve-group"
v-model:value="approvedType"
name="buttonCode"
>
<span v-for="(item, index) in approvalData.buttonConfigs" :key="index">
<a-radio
v-if="item.approveType === ApproveType.OTHER"
:value="item.buttonCode"
@change="changeButtonCodeType"
>
{{ item.buttonName }}
</a-radio>
</span>
</a-radio-group>
</span>
</div>
<div class="text-box" v-if="approvalData.approvedType === ApproveType.REJECT">
<div class="text-label">{{ t('驳回节点:') }}</div>
<a-select class="w-full flex-1" v-model:value="approvalData.rejectNodeActivityId">
<a-select-option
v-for="(item, index) in approvalData.rejectNodeActivityIds"
:key="index"
:value="item.activityId"
>{{ item.activityName }}</a-select-option
>
</a-select>
</div>
<div class="text-box">
<div class="text-label">{{ t('审批内容:') }}</div>
<a-textarea
class="flex-1"
v-model:value="approvalData.approvedContent"
:rows="6"
:maxlength="100"
/>
</div>
</ProcessInfo>
</div>
<a-form
class="approval-form"
:model="approvalData.stampInfo"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
@finish="onFinish"
@finish-failed="onFinishFailed"
>
<!-- 电子签章 -->
<a-form-item
:label="t('电子签章')"
name="stampId"
:rules="[{ required: true, message: t('请选择电子签章') }]"
v-if="data.hasStamp"
>
<SelectStamp
v-if="data.hasStamp"
v-model:stampId="approvalData.stampInfo.stampId"
/>
</a-form-item>
<a-form-item
:label="t('签章密码')"
name="password"
:rules="[{ required: true, message: t('请输入签章密码') }]"
v-if="data.hasStampPassword"
>
<a-input-password v-model:value="approvalData.stampInfo.password" />
</a-form-item>
<ApproveUser
v-if="approveUserData.visible"
:taskList="approveUserData.list"
:schemaId="approveUserData.schemaId"
@change="changeApproveUserData"
/>
<div class="button-box">
<a-button
type="primary"
html-type="submit"
class="mr-2"
:loading="data.submitLoading"
>{{ t('审批') }}</a-button
>
<!-- 转办 -->
<TransferUser :taskId="props.taskId" @close="close" />
<!-- 加签减签 -->
<AddOrSubtract
v-if="approvalData.isAddOrSubSign"
:schemaId="props.schemaId"
:taskId="props.taskId"
/>
</div>
</a-form>
</a-tab-pane>
<a-tab-pane key="2" :tab="t('传阅信息')" force-render>
<MemberTable
v-model:memberList="approvalData.circulateConfigs"
:isCommonType="true"
:isApiApprover="true"
/>
</a-tab-pane>
<a-tab-pane key="3" :tab="t('打印表单')" force-render>
<NodeHead :nodeName="t('打印属性')" />
<a-form
:model="printData"
:label-col="{ span: 8 }"
:wrapper-col="{ span: 16 }"
@finish="printForm"
>
<a-form-item :label="t('边框颜色')" name="borderColor">
<ColorPicker v-model:value="printData.borderColor" />
</a-form-item>
<a-form-item :label="t('页面风格')" name="style">
<a-select v-model:value="printData.style" style="width: 100%">
<a-select-option value="1">默认风格</a-select-option>
<a-select-option value="2">公文风格</a-select-option>
<a-select-option value="3">发文稿纸风格</a-select-option>
<a-select-option value="4">下划线风格</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="标题下划线" name="underline">
<a-select v-model:value="printData.underline" style="width: 100%">
<a-select-option value="1">是,标题带有下划线</a-select-option>
<a-select-option value="0">否,标题不带下划线</a-select-option>
</a-select>
</a-form-item>
<div class="button-box">
<a-button
type="primary"
html-type="submit"
class="mr-2"
:loading="printData.submitLoading"
>{{ t('打印当前页表单') }}</a-button
>
</div>
</a-form>
</a-tab-pane>
</a-tabs>
</template>
</ProcessLayout>
<FormPrint
v-if="isShowPrint"
v-model:isShowPrint="isShowPrint"
:formConfigs="formConfigs"
:printData="printData"
/>
</span>
</template>
<script setup lang="ts">
import { computed, reactive, ref, defineAsyncComponent, onMounted, provide, Ref } from 'vue';
import ProcessLayout from './flow/Layout.vue';
import FormInformation from './flow/FormInformation.vue';
import FlowPanel from './flow/FlowPanel.vue';
import ProcessInfo from './flow/ProcessInfo.vue';
import FormPrint from './print/FormPrint.vue';
import { NodeHead } from '/@/components/ModalPanel/index';
import { getApprovalProcess, getRejectNode, postApproval } from '/@/api/workflow/task';
import userTaskItem from './../hooks/userTaskItem';
import { ApproveTask, PostApprovalData, rejectNodeItem } from '/@/model/workflow/bpmnConfig';
import { LoadingBox } from '/@/components/ModalPanel/index';
import { ButtonConfigItem } from '/@/model/workflow/workflowConfig';
import { MemberConfig } from '/@/model/workflow/memberSetting';
import { ApproveCode, ApproveType } from '/@/enums/workflowEnum';
import { separator } from '../../design/bpmn/config/info';
import { useI18n } from '/@/hooks/web/useI18n';
import { notification } from 'ant-design-vue';
import { ColorPicker } from '/@/components/ColorPicker';
const { t } = useI18n();
const ApproveUser = defineAsyncComponent(() => import('./flow/ApproveUser.vue'));
const AddOrSubtract = defineAsyncComponent(() => import('./flow/AddOrSubtract.vue'));
const TransferUser = defineAsyncComponent(() => import('./flow/TransferUser.vue'));
const MemberTable = defineAsyncComponent(
() => import('/@bpmn/components/member/MemberTable.vue'),
);
const SelectStamp = defineAsyncComponent(() => import('./stamp/SelectStamp.vue'));
const props = withDefaults(
defineProps<{
schemaId: string;
processId: string;
taskId: string;
visible?: boolean;
}>(),
{
schemaId: '',
processId: '',
taskId: '',
visible: false,
},
);
let emits = defineEmits(['close']);
let formInformation = ref();
let showVisible = ref(false);
let showLoading = ref(false);
let approvedType = ref(ApproveType.AGREE);
let approvalData: {
isCountersign: Boolean; //是否会签节点
isAddOrSubSign: Boolean; //是否加签减签
stampInfo: {
stampId: string;
password: string;
};
buttonConfigs: Array<ButtonConfigItem>;
approvedType: ApproveType;
approvedContent: string;
approvedResult: string;
rejectNodeActivityId: string;
rejectNodeActivityIds: Array<rejectNodeItem>;
circulateConfigs: Array<MemberConfig>;
} = reactive({
isCountersign: false,
isAddOrSubSign: false,
stampInfo: {
stampId: '',
password: '',
},
buttonConfigs: [],
approvedType: ApproveType.AGREE,
approvedResult: ApproveCode.AGREE,
approvedContent: '',
rejectNodeActivityId: '',
rejectNodeActivityIds: [],
circulateConfigs: [],
});
const printData = ref({
borderColor: '#000000',
style: '1',
underline: '0',
submitLoading: false,
});
const formConfigs = ref();
const tabActiveKey = ref<number>(0);
const isShowPrint = ref<boolean>(false);
provide<Ref<number>>('tabActiveKey', tabActiveKey);
const { data, approveUserData, initProcessData, notificationError, notificationSuccess } =
userTaskItem();
const validateSuccess = ref(false);
onMounted(() => {
if (props.visible) {
approval();
}
});
const selectedPredecessorTasks = computed(() => {
return data.predecessorTasks.filter((ele) => {
return ele.taskId;
});
});
// 审批
async function approval() {
showLoading.value = true;
reset();
if (props.taskId) {
try {
let res = await getApprovalProcess(props.taskId, props.processId);
initProcessData(res);
if (res.buttonConfigs) {
approvalData.buttonConfigs = res.buttonConfigs;
}
if (res.relationTasks) {
data.predecessorTasks = res.relationTasks;
}
if (res.isAddOrSubSign) {
approvalData.isAddOrSubSign = res.isAddOrSubSign;
}
approvalData.approvedType = ApproveType.AGREE;
approvedType.value = ApproveType.AGREE;
approvalData.approvedContent = '';
approvalData.rejectNodeActivityId = '';
approvalData.rejectNodeActivityIds = [];
approvalData.circulateConfigs = [];
showLoading.value = false;
showVisible.value = true;
} catch (error) {
showLoading.value = false;
emits('close');
}
} else {
// 只能选一个
showLoading.value = false;
showVisible.value = false;
notification.open({
type: 'error',
message: t('审批'),
description: t('请选择一个流程进行审批'),
});
}
}
async function changeApprovedType() {
approvalData.approvedType = approvedType.value;
if (approvedType.value == ApproveType.AGREE) {
approvalData.approvedResult = ApproveCode.AGREE;
} else if (approvedType.value == ApproveType.REJECT) {
approvalData.rejectNodeActivityIds = await getRejectNode(props.processId, props.taskId);
approvalData.approvedResult = ApproveCode.REJECT;
} else if (approvedType.value == ApproveType.DISAGREE) {
approvalData.approvedResult = ApproveCode.DISAGREE;
} else if (approvedType.value == ApproveType.FINISH) {
approvalData.approvedResult = ApproveCode.FINISH;
} else {
approvalData.approvedResult = '';
}
}
function changeButtonCodeType(v) {
approvalData.approvedType = ApproveType.OTHER;
approvalData.approvedResult = v.target.value;
}
function getUploadFileFolderIds(formModels) {
let fileFolderIds: Array<string> = [];
let uploadComponentIds = formInformation.value.getUploadComponentIds();
uploadComponentIds.forEach((ids) => {
if (ids.includes(separator)) {
let arr = ids.split(separator);
if (arr.length == 2 && formModels[arr[0]][arr[1]]) {
fileFolderIds.push(formModels[arr[0]][arr[1]]);
} else if (
arr.length == 3 &&
formModels[arr[0]][arr[1]] &&
Array.isArray(formModels[arr[0]][arr[1]])
) {
formModels[arr[0]][arr[1]].forEach((o) => {
fileFolderIds.push(o[arr[2]]);
});
}
}
});
return fileFolderIds;
}
const onFinish = async (values: any) => {
await submit();
try {
if (validateSuccess.value) {
let formModels = await formInformation.value.getFormModels();
let system = formInformation.value.getSystemType();
let fileFolderIds: Array<string> = getUploadFileFolderIds(formModels);
let params: PostApprovalData = {
approvedType: approvalData.approvedType,
approvedResult: approvalData.approvedResult, // approvalData.approvedType 审批结果 如果为 4 就需要传buttonCode
approvedContent: approvalData.approvedContent,
formData: formModels,
rejectNodeActivityId: approvalData.rejectNodeActivityId,
taskId: props.taskId,
fileFolderIds,
circulateConfigs: approvalData.circulateConfigs,
stampId: values.stampId,
stampPassword: values.password,
isOldSystem: system,
};
let res = await postApproval(params);
// 下一节点审批人
let taskList: Array<ApproveTask> = [];
if (res && res.length > 0) {
taskList = res
.filter((ele) => {
return ele.isMultiInstance == false && ele.isAppoint == true;
})
.map((ele) => {
return {
taskId: ele.taskId,
taskName: ele.taskName,
provisionalApprover: ele.provisionalApprover,
selectIds: [],
};
});
if (taskList.length > 0) {
approveUserData.list = taskList;
approveUserData.schemaId = props.schemaId;
approveUserData.visible = true;
data.submitLoading = false;
} else {
close();
data.submitLoading = false;
save(true, t('审批流程'));
}
} else {
close();
data.submitLoading = false;
save(true, t('审批流程'));
}
}
} catch (error) {}
};
const onFinishFailed = () => {
submit();
};
async function submit() {
data.submitLoading = true;
validateSuccess.value = false;
try {
let validateForms = await formInformation.value.validateForm();
if (validateForms.length > 0) {
let successValidate = validateForms.filter((ele) => {
return ele.validate;
});
if (successValidate.length == validateForms.length) {
validateSuccess.value = true;
data.submitLoading = false;
} else {
data.submitLoading = false;
notificationError(t('审批流程'), t('表单校验未通过'));
}
}
} catch (error) {
data.submitLoading = false;
notificationError(t('审批流程'), t('审批流程失败'));
}
}
function changeApproveUserData() {
approveUserData.visible = false;
close();
}
function save(res: boolean, title: string) {
if (res) {
notificationSuccess(title);
close();
} else {
notificationError(title);
}
}
function close() {
showVisible.value = false;
emits('close');
printData.value = {
borderColor: '#000000',
style: '1',
underline: '0',
submitLoading: false,
};
}
function reset() {
approvalData.isAddOrSubSign = false;
approvalData.stampInfo = {
stampId: '',
password: '',
};
approvalData.buttonConfigs = [];
approvalData.approvedType = ApproveType.AGREE;
approvalData.approvedContent = '';
approvalData.rejectNodeActivityId = '';
approvalData.rejectNodeActivityIds = [];
approvalData.circulateConfigs = [];
}
function printForm() {
isShowPrint.value = true;
}
</script>
<style lang="less" scoped>
.description-box {
display: flex;
flex-direction: column;
padding-top: 10px;
align-items: center;
justify-content: center;
color: rgb(102 102 102 / 99.6%);
margin-bottom: 20px;
.title {
align-self: flex-start;
margin-bottom: 20px;
}
.item-box {
align-self: flex-start;
width: 100%;
}
.text-box {
display: flex;
margin: 10px 0;
.text-label {
width: 80px;
height: 40px;
display: inline-flex;
justify-content: flex-end;
margin-right: 4px;
}
}
}
// 传阅人
:deep(.opr-box) {
flex-direction: column !important;
.header-box {
flex-basis: 40px !important;
}
.button-box {
flex-direction: row !important;
}
}
.approve-group {
.ant-radio-wrapper {
margin-right: 0;
}
}
:deep(span.ant-radio + *) {
padding-right: 12px;
padding-left: 4px;
}
:deep(.ant-form) {
&.approval-form {
.ant-form-item-label label {
width: 90px;
margin-left: -9px;
margin-right: 14px;
}
.ant-form-item-control-input {
margin-left: 2px;
}
}
}
</style>

View File

@ -0,0 +1,76 @@
<template>
<BasicModal
v-bind="$attrs"
width="1000px"
@register="registerModal"
:title="t('批量审核')"
:cancel-text="t('关闭')"
@cancel="handlesubmit"
:show-ok-btn="false"
:closable="false"
>
<BasicTable @register="registerTable" />
</BasicModal>
</template>
<script setup lang="ts">
import { Tag } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import { BasicTable, useTable, BasicColumn } from '/@/components/Table';
import { useI18n } from '/@/hooks/web/useI18n';
import { h } from 'vue';
const { t } = useI18n();
const columns: BasicColumn[] = [
{
title: t('流程任务名称'),
dataIndex: 'schemaName',
align: 'left',
},
{
title: t('当前审批节点'),
dataIndex: 'currentNodeName',
align: 'left',
},
{
title: t('发起人'),
dataIndex: 'startUserName',
align: 'left',
},
{
title: t('审核结果'),
dataIndex: 'approveResult',
align: 'left',
customRender: ({ record }) => {
return h(
Tag,
{
color: record.approveResult === '审核成功' ? 'green' : 'red',
},
() => t(`${record.approveResult}`),
);
},
},
{
title: t('审核详情'),
dataIndex: 'approveDetail',
align: 'left',
},
];
const emit = defineEmits(['success', 'register']);
const [registerTable, { setTableData }] = useTable({
title: t('审核明细'),
columns,
useSearchForm: false,
showTableSetting: false,
striped: false,
pagination: false,
});
const [registerModal, { closeModal }] = useModalInner((data) => {
setTableData(data || []);
});
function handlesubmit() {
closeModal();
emit('success');
}
</script>

View File

@ -0,0 +1,211 @@
<template>
<span @click.stop="show">
<slot></slot>
<a-modal
v-model:visible="data.visible"
:width="800"
:title="t('批量审批')"
@ok="submit"
@cancel="cancel"
:okText="t('确定')"
:cancelText="t('取消')"
>
<div class="model-box">
<NodeHead :nodeName="t('审批流程')" />
<a-table
class="box"
:pagination="false"
:dataSource="props.selectedRows"
:columns="configColumns"
:scroll="{ y: '160px' }"
/>
<div class="mt-2 mb-2">
<NodeHead :nodeName="t('审批信息')" />
<div class="text-box">
<div class="text-label">{{ t('审批结果:') }}</div>
<a-radio-group v-model:value="data.approvedType" name="approvedType" class="flex-1">
<a-radio :value="ApproveType.AGREE">{{ t('同意') }}</a-radio>
<a-radio :value="ApproveType.DISAGREE">{{ t('拒绝') }}</a-radio>
</a-radio-group>
</div>
<div class="text-box">
<div class="text-label">{{ t('审批内容:') }}</div>
<a-textarea
v-model:value="data.approvedContent"
:rows="6"
:maxlength="100"
class="flex-1"
/>
</div>
</div>
<div v-if="needStampRef" class="mt-2 mb-2">
<NodeHead :nodeName="t('签章')" />
<a-form :model="data" :label-col="{ span: 3 }" :wrapper-col="{ span: 21 }" ref="formData">
<!-- 电子签章 -->
<a-form-item
:label="t('电子签章')"
name="stampId"
:rules="[{ required: true, message: t('请选择电子签章') }]"
v-if="data.hasStamp"
>
<SelectStamp v-if="data.hasStamp" v-model:stampId="data.stampId" />
</a-form-item>
<a-form-item
:label="t('签章密码')"
name="password"
:rules="[{ required: true, message: t('请输入签章密码') }]"
v-if="data.hasStampPassword"
>
<a-input-password v-model:value="data.password" />
</a-form-item>
</a-form>
</div>
</div>
</a-modal>
</span>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { NodeHead } from '/@/components/ModalPanel/index';
import { TasksModel } from '/@/api/workflow/model';
import { ApproveType } from '/@/enums/workflowEnum';
import { getBatchApprovalInfo, postBatchApproval } from '/@/api/workflow/task';
import SelectStamp from './stamp/SelectStamp.vue';
import { GetBatchApprovalInfo, PostBatchApprovalData } from '/@/model/workflow/bpmnConfig';
import { message } from 'ant-design-vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const props = withDefaults(
defineProps<{
selectedRows: Array<TasksModel>;
}>(),
{
selectedRows: () => {
return [];
},
},
);
let emits = defineEmits(['close']);
const formData = ref();
const configColumns = [
{
title: '序号',
align: 'center',
customRender: ({ index }) => `${index + 1}`, // 显示每一行的序号
width: 80,
},
{
title: t('流水号'),
dataIndex: 'serialNumber',
sorter: {
multiple: 1,
},
},
{
title: t('流程名称'),
dataIndex: 'processName',
sorter: {
multiple: 2,
},
},
{
title: t('当前任务名称'),
dataIndex: 'taskName',
width: 160,
sorter: {
multiple: 3,
},
},
{
title: t('流程发起人'),
dataIndex: 'startUserName',
},
];
const data: {
visible: boolean;
hasStamp: boolean;
hasStampPassword: boolean;
stampId: string;
password: string;
approvedType: ApproveType;
approvedContent: string;
dataSource: Array<TasksModel>;
} = reactive({
visible: false,
hasStamp: true,
hasStampPassword: true,
stampId: '',
password: '',
dataSource: [],
approvedType: ApproveType.AGREE,
approvedContent: '',
});
const needStampRef = ref(true);
async function show() {
data.visible = true;
let ids = props.selectedRows
.map((ele) => {
return ele.taskId;
})
.join(',');
const param: GetBatchApprovalInfo = { taskIds: ids };
const info = await getBatchApprovalInfo(param);
needStampRef.value = info.needStamp;
data.hasStampPassword = info.needPassword ? true : false;
}
async function submit() {
let ids: Array<string> = props.selectedRows.map((ele) => {
return ele.taskId;
});
let params: PostBatchApprovalData = {
approvedContent: data.approvedContent,
approvedType: data.approvedType,
stampId: data.stampId,
taskIds: ids,
};
if (data.password) {
params.stampPassword = data.password;
}
try {
if (needStampRef.value) {
await formData.value.validate();
}
let res = await postBatchApproval(params);
if (res) {
message.success(t('批量审批成功'));
data.visible = false;
emits('close', res);
} else {
message.error(t('批量审批失败'));
}
} catch (error) {}
}
function cancel() {
data.visible = false;
emits('close');
}
</script>
<style lang="less" scoped>
.model-box {
padding: 10px 20px;
}
.text-box {
display: flex;
margin: 10px 0;
.text-label {
width: 80px;
display: inline-flex;
justify-content: flex-end;
margin-right: 4px;
}
}
</style>

View File

@ -0,0 +1,284 @@
<template>
<a-modal
v-model:visible="data.visible"
:maskClosable="false"
:width="900"
:title="title"
@ok="submit"
@cancel="close"
>
<div class="box" v-if="data.visible">
<NodeHead class="mb-3 mt-3" :nodeName="t('基础信息')" />
<div class="item">
<label><em class="text-red-600">*</em>{{ t('被委托人:') }}</label>
<SelectUser
:selectedIds="data.delegateUserIds"
:multiple="true"
@change="
(ids) => {
data.delegateUserIds = ids;
}
"
@change-names="
(names) => {
data.delegateUserNames = names;
}
"
>
<a-input
:value="data.delegateUserNames"
:placeholder="t('请选择委托人')"
style="width: 100%"
/>
</SelectUser>
</div>
<div class="item">
<label><em class="text-red-600">*</em>{{ t('时间区间:') }}</label>
<a-range-picker v-model:value="data.searchDate" style="width: 100%" />
</div>
<div class="item">
<label>{{ t('委托说明:') }}</label>
<a-textarea
v-model:value="data.remark"
:placeholder="t('请填写委托说明')"
:auto-size="{ minRows: 2, maxRows: 5 }"
style="width: 100%"
/>
</div>
<NodeHead class="mb-3 mt-3" :nodeName="t('模板列表')" />
<SearchBox
:searchConfig="{
field: 'keyword',
label: t('模板名称'),
type: 'input',
}"
@search="
(v) => {
keyword = v;
getList();
}
"
@scroll-height="$emit('scrollHeight')"
/>
<template v-if="data.list.length > 0">
<div class="list-page-box">
<TemplateCard
v-for="(item, index) in data.list"
:class="data.checkSchemaIds.includes(item.id) ? 'picked' : ''"
@click="check(item.id)"
:key="index"
:item="item"
/>
</div>
<div class="page-box">
<a-pagination
v-model:current="data.pagination.current"
:pageSize="data.pagination.pageSize"
:total="data.pagination.total"
show-less-items
@change="getList"
/></div>
</template>
<div v-else>
<EmptyBox />
</div>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
import { SelectUser } from '/@/components/SelectOrganizational/index';
import { NodeHead } from '/@/components/ModalPanel/index';
import TemplateCard from '/@bpmn/components/card/TemplateCard.vue';
import { EmptyBox } from '/@/components/ModalPanel/index';
import { getDesignPage } from '/@/api/workflow/design';
import { postDelegate, putDelegate, getDelegateInfo } from '/@/api/workflow/delegate';
import { notification } from 'ant-design-vue';
import dayjs, { Dayjs } from 'dayjs';
import { SearchBox } from '/@/components/ModalPanel/index';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
let props = defineProps({
id: String,
});
let emits = defineEmits(['close']);
const title = computed(() => {
return props.id == '' ? t('新增流程委托') : t('编辑流程委托');
});
const keyword = ref('');
const data: {
visible: boolean;
delegateUserNames: string;
delegateUserIds: Array<string>;
schemaIds: string;
startTime: string;
endTime: string;
remark: string;
searchDate: [Dayjs | null, Dayjs | null];
pagination: { current: number; total: number; pageSize: number };
list: Array<any>;
checkSchemaIds: Array<string>;
} = reactive({
visible: false,
delegateUserNames: '',
delegateUserIds: [],
schemaIds: '',
startTime: '',
endTime: '',
remark: '',
searchDate: [null, null],
list: [],
checkSchemaIds: [],
pagination: {
current: 1,
total: 0,
pageSize: 6,
},
});
onMounted(() => {
open();
});
async function open() {
if (props.id) {
try {
let res = await getDelegateInfo(props.id);
if (res.delegateUserIds) {
data.delegateUserIds = res.delegateUserIds.split(',');
}
if (res.schemaIds) {
data.checkSchemaIds = res.schemaIds.split(',');
}
if (res.remark) {
data.remark = res.remark;
}
if (res.startTime && res.endTime) {
data.searchDate = [dayjs(res.startTime), dayjs(res.endTime)];
}
} catch (error) {}
}
await getList();
data.visible = true;
}
function close() {
data.visible = false;
emits('close');
}
function check(id: string) {
if (data.checkSchemaIds.includes(id)) {
data.checkSchemaIds.splice(
data.checkSchemaIds.findIndex((itemId) => itemId === id),
1,
);
} else {
data.checkSchemaIds.push(id);
}
}
async function getList() {
const searchParams = {
limit: data.pagination.current,
size: data.pagination.pageSize,
keyword: keyword.value,
enabledMark: 1,
};
try {
let res = await getDesignPage(searchParams);
data.pagination.total = res.total;
data.list = res.list;
} catch (error) {}
}
async function submit() {
if (data.delegateUserIds.length == 0) {
notification.open({
type: 'error',
message: t('流程委托'),
description: t('请选择被委托人'),
});
return false;
}
if (data.checkSchemaIds.length == 0) {
notification.open({
type: 'error',
message: t('流程委托'),
description: t('请选择模板'),
});
return false;
}
if (!data.searchDate[0] || !data.searchDate[1]) {
notification.open({
type: 'error',
message: t('流程委托'),
description: t('请选择时间区间'),
});
return false;
}
try {
let res = false;
let params = {
delegateUserIds: data.delegateUserIds.join(','),
schemaIds: data.checkSchemaIds.join(','),
remark: data.remark,
startTime: data.searchDate[0],
endTime: data.searchDate[1],
};
if (props.id) {
res = await putDelegate(props.id, params);
} else {
res = await postDelegate(params);
}
if (res) {
notification.open({
type: 'success',
message: t('流程委托'),
description: title.value + t('成功'),
});
close();
} else {
notification.open({
type: 'error',
message: t('流程委托'),
description: title.value + t('失败'),
});
}
} catch (error) {}
}
</script>
<style lang="less" scoped>
.box {
position: relative;
padding: 10px;
height: 578px;
.item {
display: flex;
align-items: center;
margin: 8px;
label {
width: 90px;
}
}
}
.list-page-box {
display: flex;
flex-wrap: wrap;
overflow-y: auto;
height: 240px;
}
.page-box {
position: absolute;
bottom: 20px;
right: 20px;
}
.picked {
border-width: 3px;
border-style: dotted;
border-color: #5332f5;
}
</style>

View File

@ -0,0 +1,336 @@
<template>
<LoadingBox v-if="showLoading" />
<ProcessLayout class="wrap" v-if="visible">
<template #title> {{ t('发起流程') }}{{ data.item.name }} </template>
<template #close>
<a-button type="primary" class="clean-icon" @click.stop="$emit('close')">{{
t('关闭')
}}</a-button>
</template>
<template #left>
<FlowPanel
:xml="data.xml"
:taskRecords="[]"
:predecessorTasks="selectedPredecessorTasks"
processId=""
position="top"
>
<FormInformation
:opinionsComponents="data.opinionsComponents"
:opinions="data.opinions"
:disabled="false"
:formInfos="data.formInfos"
:formAssignmentData="data.formAssignmentData"
ref="formInformation"
/>
</FlowPanel>
</template>
<template #right>
<div class="launch-box">
<div class="description-box">
<NodeHead :nodeName="t('发起流程')" class="title" />
<ProcessInfo class="item-box" :item="data.item" />
</div>
<PredecessorTask
v-if="data.relationTasks && data.relationTasks.length > 0"
@change="changePredecessorTasks"
:schemaId="schemaId"
:relationTasks="data.predecessorTasks"
/>
<ApproveUser
v-if="approveUserData.visible"
:taskList="approveUserData.list"
:schemaId="approveUserData.schemaId"
@change="changeApproveUserData"
/>
<div class="button-box">
<a-button
type="primary"
class="mr-2"
:disabled="isPublish"
:loading="data.submitLoading"
@click="saveLaunch"
>{{ t('发起') }}</a-button
>
<a-button class="mr-2" @click="saveDraft">{{ t('保存草稿') }}</a-button>
</div>
</div>
</template>
</ProcessLayout>
</template>
<script setup lang="ts">
import ProcessLayout from './flow/Layout.vue';
import FlowPanel from './flow/FlowPanel.vue';
import FormInformation from './flow/FormInformation.vue';
import { LoadingBox } from '/@/components/ModalPanel/index';
import ProcessInfo from './flow/ProcessInfo.vue';
import PredecessorTask from './PredecessorTask.vue';
import { NodeHead } from '/@/components/ModalPanel/index';
import ApproveUser from './flow/ApproveUser.vue';
import { postDraft, putDraft } from '/@/api/workflow/process';
import {
postLaunch,
getStartProcessInfo,
getReStartProcessInfo,
reLaunch,
} from '/@/api/workflow/task';
import { computed, onMounted, ref, toRaw, nextTick } from 'vue';
import { ApproveTask, SchemaTaskItem } from '/@/model/workflow/bpmnConfig';
import userTaskItem from './../hooks/userTaskItem';
import { separator } from '../../design/bpmn/config/info';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const props = defineProps({
schemaId: {
type: String,
required: true,
},
draftsJsonStr: {
type: String,
},
draftsId: {
type: String,
},
taskId: {
type: String,
required: false,
default: '',
},
processId: {
type: String,
required: false,
default: '',
},
formData: {
type: Object,
},
formId: {
type: String,
},
rowKeyData: {
type: String,
},
});
let emits = defineEmits(['close']);
let formInformation = ref();
let visible = ref(false);
let showLoading = ref(false);
const { data, approveUserData, initProcessData, notificationError, notificationSuccess } =
userTaskItem();
const selectedPredecessorTasks = computed(() => {
return data.predecessorTasks.filter((ele) => {
return ele.taskId;
});
});
const isPublish = ref(true);
onMounted(async () => {
try {
if (props.processId) {
// 重新发起
let res = await getReStartProcessInfo(props.taskId, props.processId);
res.taskApproveOpinions = [];
initProcessData(res);
showLoading.value = false;
} else if (props.schemaId && props.formId) {
let res = await getStartProcessInfo(props.schemaId);
if (props.formData && props.formId) {
res.formInfos.map((m) => {
if (m.formConfig.formId === props.formId) {
m.formData = toRaw(props.formData);
}
});
}
initProcessData(res);
showLoading.value = false;
} else {
// 发起流程
let res = await getStartProcessInfo(props.schemaId);
initProcessData(res);
showLoading.value = false;
}
} catch (error) {
showLoading.value = false;
}
showLoading.value = false;
visible.value = true;
await nextTick();
initDraftsFormData();
});
async function initDraftsFormData() {
isPublish.value = Object.keys(data.formInfos).length > 0 ? false : true;
if (props.draftsJsonStr) {
let formDataJson = JSON.parse(props.draftsJsonStr);
let formData: Array<any> = [];
data.formInfos.forEach((item) => {
if (
formDataJson &&
item.formConfig &&
item.formConfig.key &&
formDataJson[item.formConfig.key]
) {
formData.push(item.formConfig.key ? formDataJson[item.formConfig.key] : {});
}
});
await formInformation.value.setFormData(formData);
}
}
function getUploadFileFolderIds(formModels) {
let fileFolderIds: Array<string> = [];
let uploadComponentIds = formInformation.value.getUploadComponentIds();
uploadComponentIds.forEach((ids) => {
if (ids.includes(separator)) {
let arr = ids.split(separator);
if (arr.length == 2 && formModels[arr[0]][arr[1]]) {
fileFolderIds.push(formModels[arr[0]][arr[1]]);
} else if (
arr.length == 3 &&
formModels[arr[0]][arr[1]] &&
Array.isArray(formModels[arr[0]][arr[1]])
) {
formModels[arr[0]][arr[1]].forEach((o) => {
fileFolderIds.push(o[arr[2]]);
});
}
}
});
return fileFolderIds;
}
async function saveLaunch() {
data.submitLoading = true;
try {
let validateForms = await formInformation.value.validateForm();
let system = formInformation.value.getSystemType();
if (validateForms.length > 0) {
let successValidate = validateForms.filter((ele) => {
return ele.validate;
});
if (successValidate.length == validateForms.length) {
let formModels = await formInformation.value.getFormModels();
let relationTasks: Array<{
schemaId: string;
taskId: string;
}> = [];
if (data.predecessorTasks && data.predecessorTasks.length > 0) {
relationTasks = data.predecessorTasks.map((ele) => {
return { taskId: ele.taskId, schemaId: ele.schemaId };
});
}
let fileFolderIds: Array<string> = getUploadFileFolderIds(formModels);
//如果传入了processId 代表是重新发起流程
let res;
if (props.processId) {
res = await reLaunch(
props.processId,
props.schemaId,
formModels,
relationTasks,
fileFolderIds,
system,
);
} else {
res = await postLaunch(
props.schemaId,
formModels,
relationTasks,
fileFolderIds,
system,
);
}
// 下一节点审批人
let taskList: Array<ApproveTask> = [];
if (res && res.length > 0) {
taskList = res
.filter((ele) => {
return ele.isMultiInstance == false && ele.isAppoint == true;
})
.map((ele) => {
return {
taskId: ele.taskId,
taskName: ele.taskName,
provisionalApprover: ele.provisionalApprover,
selectIds: [],
};
});
if (taskList.length > 0) {
approveUserData.list = taskList;
approveUserData.schemaId = props.schemaId;
approveUserData.visible = true;
data.submitLoading = false;
} else {
data.submitLoading = false;
save(true, t('发起流程'));
}
} else {
data.submitLoading = false;
save(true, t('发起流程'));
}
} else {
data.submitLoading = false;
notificationError(t('发起流程'), t('表单校验未通过'));
}
}
} catch (error) {
data.submitLoading = false;
notificationError(t('发起流程'), t('发起流程失败'));
}
}
async function saveDraft() {
try {
let formModels = await formInformation.value.saveDraftData();
if (props.draftsId) {
let res = await putDraft(props.schemaId, formModels, props.draftsId, props.rowKeyData);
save(res, t('保存草稿'));
} else {
let res = await postDraft(props.schemaId, formModels, props.rowKeyData);
save(res, t('保存草稿'));
}
} catch (error) {
notificationError(t('保存草稿'));
}
}
function changePredecessorTasks(list: Array<SchemaTaskItem>) {
data.predecessorTasks = list;
}
function save(res: boolean, title: string) {
if (res) {
notificationSuccess(title);
emits('close');
} else {
notificationError(title);
}
}
function changeApproveUserData() {
approveUserData.visible = false;
emits('close');
}
</script>
<style lang="less" scoped>
.description-box {
display: flex;
flex-direction: column;
padding-top: 10px;
align-items: center;
justify-content: center;
color: rgb(102 102 102 / 99.6%);
margin-bottom: 20px;
.title {
align-self: flex-start;
margin-bottom: 20px;
}
.item-box {
align-self: flex-start;
}
}
</style>

View File

@ -0,0 +1,62 @@
<template>
<span @click.stop="look"
><slot></slot>
<LoadingBox v-if="showLoading" />
<ProcessLayout class="wrap" v-if="visible" @click.stop="">
<template #title> {{ t('查看流程') }} </template>
<template #close>
<a-button type="primary" class="clean-icon" @click.stop="close">{{ t('关闭') }}</a-button>
</template>
<template #full>
<LookTask v-if="visible" :taskId="props.taskId" :processId="props.processId" />
</template>
</ProcessLayout>
</span>
</template>
<script setup lang="ts">
import ProcessLayout from './flow/Layout.vue';
import LookTask from './flow/LookTask.vue';
import { LoadingBox } from '/@/components/ModalPanel/index';
import { notification } from 'ant-design-vue';
import { onMounted, ref } from 'vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const props = withDefaults(
defineProps<{
processId: string;
taskId: string;
visible?: boolean;
}>(),
{
processId: '',
taskId: '',
visible: false,
},
);
let emits = defineEmits(['close']);
let visible = ref(false);
let showLoading = ref(false);
onMounted(() => {
if (props.visible) {
look();
}
});
async function look() {
if (props.processId) {
showLoading.value = false;
visible.value = true;
} else {
showLoading.value = false;
notification.open({
type: 'error',
message: t('查看'),
description: t('请选择一个流程进行查看'),
});
}
}
function close() {
visible.value = false;
emits('close');
}
</script>

View File

@ -0,0 +1,170 @@
<template>
<div class="box">
<NodeHead nodeName="前置任务" class="title" />
<div v-for="(item, index) in props.relationTasks" :key="index" class="task-box">
<div class="label-box">
<span>任务名称</span>
<span>{{ item.schemaName }}</span>
</div>
<a-input
:value="item.taskName"
placeholder="点击选择前置任务"
@click="open(index, item.schemaId)"
style="width: 100%"
>
<template #suffix>
<Icon icon="ant-design:ellipsis-outlined" />
</template>
</a-input>
</div>
<a-modal
:width="1000"
v-model:visible="visible"
title="选择前置任务"
:maskClosable="false"
@ok="submit"
@cancel="close"
>
<div class="p-5">
<a-table
v-if="visible"
:dataSource="data.dataSource"
:columns="columns"
rowKey="processId"
:row-selection="{
selectedRowKeys: data.selectedRowKeys,
onChange: onSelectChange,
type: 'radio',
}"
:pagination="data.pagination"
:scroll="{ y: '400px' }"
/>
</div>
</a-modal>
</div>
</template>
<script setup lang="ts">
import { NodeHead } from '/@/components/ModalPanel/index';
import { Icon } from '/@/components/Icon';
import { reactive, ref } from 'vue';
import { PredecessorTaskItem, SchemaTaskItem, TaskItem } from '/@/model/workflow/bpmnConfig';
import { getRelationTasks } from '/@/api/workflow/task';
const emit = defineEmits(['change']);
const props = withDefaults(
defineProps<{ relationTasks: Array<SchemaTaskItem>; schemaId: string }>(),
{
relationTasks: () => {
return [];
},
},
);
let visible = ref(false);
let data: {
dataSource: Array<PredecessorTaskItem>;
selectedRowKeys: string[];
selectedTask: TaskItem;
pagination: { current: number; total: number; pageSize: number };
selectIndex: number;
} = reactive({
dataSource: [],
selectedRowKeys: [],
selectedTask: {
taskId: '',
taskName: '',
processId: '',
},
pagination: {
current: 1,
total: 0,
pageSize: 15,
},
selectIndex: -1,
});
let columns = [
{
title: '序号',
customRender: ({ index }) => `${index + 1}`, // 显示每一行的序号
width: 60,
align: 'center',
},
{
title: '任务',
dataIndex: 'schemaName',
key: 'schemaName',
},
{
title: '标题',
dataIndex: 'taskName',
key: 'taskName',
ellipsis: true,
},
// {
// title: '等级',
// dataIndex: 'level',
// key: 'level',
// },
{
title: '发起人',
dataIndex: 'originator',
key: 'originator',
},
{
title: '时间',
dataIndex: 'createTime',
key: 'createTime',
},
];
async function open(index: number, relationSchemaId: string) {
data.selectIndex = index;
let res = await getRelationTasks(props.schemaId, relationSchemaId, {
limit: data.pagination.current,
size: data.pagination.pageSize,
});
data.dataSource = res.list;
data.pagination.total = res.total;
visible.value = true;
}
function close() {
data.selectIndex = -1;
data.dataSource = [];
data.pagination.total = 0;
visible.value = false;
}
function submit() {
let list = props.relationTasks;
list[data.selectIndex].taskId = data.selectedTask.taskId;
list[data.selectIndex].taskName = data.selectedTask.taskName;
list[data.selectIndex].processId = data.selectedTask.processId;
emit('change', list);
close();
}
const onSelectChange = (selectedRowKeys: string[], selectedRows: Array<PredecessorTaskItem>) => {
let { taskId, taskName, processId } = selectedRows[0];
data.selectedTask.taskId = taskId;
data.selectedTask.taskName = taskName;
data.selectedTask.processId = processId;
data.selectedRowKeys = selectedRowKeys;
};
</script>
<style lang="less" scoped>
.box {
margin-bottom: 10px;
.title {
margin-bottom: 10px;
}
.task-box {
margin: 8px 0;
.label-box {
margin-bottom: 8px;
color: rgba(102, 102, 102, 0.996);
}
}
}
</style>

View File

@ -0,0 +1,147 @@
<template>
<span @click.stop="rejectNode"
><slot></slot>
<a-modal
v-model:visible="data.visible"
:title="t('请选择撤回到的节点')"
width="700px"
@ok="submit"
@cancel="close"
:okText="t('确认')"
:cancelText="t('取消')"
@click.stop=""
>
<div class="box" v-if="data.visible">
<div
class="item"
:class="data.checkedIds.includes(item.activityId) ? 'activity' : ''"
v-for="(item, index) in data.list"
:key="index"
@click="check(item.activityId)"
>{{ item.activityName }}</div
>
</div>
<a-alert
message="外部流程节点、子流程内部节点、会签节点都不支持撤回;撤回到开始节点需要二次确认!"
type="warning"
/>
</a-modal>
</span>
</template>
<script setup lang="ts">
import { reactive } from 'vue';
import { getRejectNodeList, withdraw } from '/@/api/workflow/task';
import { notification } from 'ant-design-vue';
import { getStartNodeId } from '/@bpmn/config/property';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const props = withDefaults(defineProps<{ processId: string; taskId: string }>(), {
processId: '',
taskId: '',
});
let emits = defineEmits(['close', 'restart']);
const data: {
visible: boolean;
checkedIds: Array<string>;
list: Array<{
activityId: string;
activityName: string;
}>;
} = reactive({
visible: false,
list: [],
checkedIds: [],
});
async function submit() {
if (data.checkedIds.length > 0) {
if (getStartNodeId == data.checkedIds[0]) {
emits('restart');
data.visible = false;
} else {
try {
let res = await withdraw(props.processId, data.checkedIds[0]);
if (res) {
notification.open({
type: 'success',
message: t('撤回'),
description: t('撤回成功'),
});
close();
} else {
notification.open({
type: 'error',
message: t('撤回'),
description: t('撤回失败'),
});
}
} catch (error) {
close();
}
}
} else {
notification.open({
type: 'error',
message: t('撤回'),
description: t('请选择一个节点进行撤回'),
});
}
}
async function rejectNode() {
if (props.processId) {
try {
let res = await getRejectNodeList(props.processId, props.taskId);
if (res && Array.isArray(res) && res.length > 0) {
data.visible = true;
data.list = res;
}
} catch (error) {
close();
}
} else {
notification.open({
type: 'error',
message: t('撤回'),
description: t('请选择一个流程进行撤回'),
});
}
}
function check(activityId) {
if (data.checkedIds.includes(activityId)) {
data.checkedIds = [];
} else {
data.checkedIds = [activityId];
}
}
function close() {
data.visible = false;
emits('close');
}
</script>
<style lang="less" scoped>
.box {
display: flex;
flex-wrap: wrap;
padding: 10px;
height: 300px;
overflow: auto;
.item {
width: 84px;
height: 84px;
border: 1px solid rgb(198 226 255 / 100%);
margin-right: 10px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
color: rgb(0 0 0 / 60%);
}
.activity {
color: rgb(158 203 251);
border: 1px solid rgb(198 226 255 / 100%);
background: rgb(236 245 255 / 100%);
}
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<span @click.stop="restart"
><slot></slot>
<LaunchProcess
v-if="visible"
:schemaId="schemaId"
:taskId="taskId"
:processId="processId"
@close="close"
/>
</span>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import LaunchProcess from './LaunchProcess.vue';
const props = defineProps({
schemaId: {
type: String,
required: true,
},
taskId: {
type: String,
},
processId: {
type: String,
},
});
let emits = defineEmits(['close']);
let visible = ref(false);
function restart() {
if (props.taskId) {
visible.value = true;
} else {
// 只能选一个
visible.value = false;
}
}
function close() {
visible.value = false;
emits('close');
}
</script>

View File

@ -0,0 +1,408 @@
<template>
<ProcessLayout class="wrap" v-if="showVisible" @click.stop="">
<template #title> {{ t('审批流程') }}{{ data.item.name }} </template>
<template #close>
<a-button type="primary" class="clean-icon" @click.stop="close">{{ t('关闭') }}</a-button>
</template>
<template #left>
<FlowPanel
:xml="data.xml"
:taskRecords="data.taskRecords"
:predecessorTasks="selectedPredecessorTasks"
:processId="props.processId"
position="top"
>
<FormInformation
:opinionsComponents="data.opinionsComponents"
:opinions="data.opinions"
:disabled="false"
:formInfos="data.formInfos"
:formAssignmentData="data.formAssignmentData"
ref="formInformation"
/>
</FlowPanel>
</template>
<template #right>
<a-tabs style="width: 280px">
<a-tab-pane key="1" :tab="t('审批信息')">
<NodeHead :nodeName="t('基础信息')" />
<div class="description-box">
<ProcessInfo class="item-box" :item="data.item">
<NodeHead :nodeName="t('审批信息')" />
<div
class="text-box"
v-if="approvalData.buttonConfigs && approvalData.buttonConfigs.length > 0"
>
<div class="text-label">{{ t('审批结果:') }}</div>
<span>
<a-radio-group
v-model:value="approvalData.approvedType"
name="approvedType"
@change="changeApprovedType"
>
<span v-for="(item, index) in approvalData.buttonConfigs" :key="index">
<a-radio
v-if="item.approveType !== ApproveType.OTHER"
:value="item.approveType"
>{{ item.buttonName }}</a-radio
>
</span>
</a-radio-group>
<a-radio-group v-model:value="approvalData.approvedResult" name="buttonCode">
<span v-for="(item, index) in approvalData.buttonConfigs" :key="index">
<a-radio
v-if="item.approveType === ApproveType.OTHER"
:value="item.buttonCode"
@change="changeButtonCodeType"
>{{ item.buttonName }}</a-radio
>
</span>
</a-radio-group></span
>
</div>
<div class="text-box" v-if="approvalData.approvedType === ApproveType.REJECT">
<div class="text-label">{{ t('驳回节点:') }}</div>
<a-select style="width: 100%" v-model:value="approvalData.rejectNodeActivityId">
<a-select-option
v-for="(item, index) in approvalData.rejectNodeActivityIds"
:key="index"
:value="item.activityId"
>{{ item.activityName }}</a-select-option
>
</a-select>
</div>
<div class="text-box">
<div class="text-label">{{ t('审批内容:') }}</div>
<a-textarea
v-model:value="approvalData.approvedContent"
:rows="6"
:maxlength="100"
/>
</div>
</ProcessInfo>
</div>
<!-- 电子签章 -->
<a-form-item :label="t('电子签章')" name="password" v-if="data.hasStamp">
<SelectStamp v-if="data.hasStamp" v-model:stampId="approvalData.stampInfo.stampId" />
</a-form-item>
<a-form-item :label="t('签章密码')" name="password" v-if="data.hasStampPassword">
<a-input-password v-model:value="approvalData.stampInfo.password" style="width: 100%" />
</a-form-item>
<ApproveUser
v-if="approveUserData.visible"
:taskList="approveUserData.list"
:schemaId="approveUserData.schemaId"
@change="changeApproveUserData" />
<div class="button-box">
<a-button type="primary" class="mr-2" :loading="data.submitLoading" @click="submit">{{
t('审批')
}}</a-button>
<!-- 转办 -->
<TransferUser :taskId="props.taskId" @close="close" />
<!-- 加签减签 -->
<AddOrSubtract
v-if="approvalData.isAddOrSubSign"
:schemaId="props.schemaId"
:taskId="props.taskId"
/> </div
></a-tab-pane>
<a-tab-pane key="2" :tab="t('传阅信息')" force-render>
<MemberTable v-model:memberList="approvalData.circulateConfigs" :isCommonType="true" />
</a-tab-pane>
</a-tabs>
</template>
</ProcessLayout>
</template>
<script setup lang="ts">
import { computed, reactive, ref, defineAsyncComponent, onMounted } from 'vue';
import ProcessLayout from './flow/Layout.vue';
import FormInformation from './flow/FormInformation.vue';
import FlowPanel from './flow/FlowPanel.vue';
import ProcessInfo from './flow/ProcessInfo.vue';
import { NodeHead } from '/@/components/ModalPanel/index';
import { getApprovalProcess, getRejectNode, postApproval } from '/@/api/workflow/task';
import userTaskItem from './../hooks/userTaskItem';
import { ApproveTask, PostApprovalData, rejectNodeItem } from '/@/model/workflow/bpmnConfig';
import { ButtonConfigItem } from '/@/model/workflow/workflowConfig';
import { MemberConfig } from '/@/model/workflow/memberSetting';
import { ApproveCode, ApproveType } from '/@/enums/workflowEnum';
import { separator } from '../../design/bpmn/config/info';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const ApproveUser = defineAsyncComponent(() => import('./flow/ApproveUser.vue'));
const AddOrSubtract = defineAsyncComponent(() => import('./flow/AddOrSubtract.vue'));
const TransferUser = defineAsyncComponent(() => import('./flow/TransferUser.vue'));
const MemberTable = defineAsyncComponent(
() => import('/@bpmn/components/member/MemberTable.vue'),
);
const SelectStamp = defineAsyncComponent(() => import('./stamp/SelectStamp.vue'));
const props = withDefaults(
defineProps<{
schemaId: string;
processId: string;
taskId: string;
visible: boolean;
}>(),
{
schemaId: '',
processId: '',
taskId: '',
visible: false,
},
);
let emits = defineEmits(['close']);
let formInformation = ref();
let showVisible = ref(false);
let showLoading = ref(false);
let approvalData: {
isAddOrSubSign: Boolean; //是否加签减签
stampInfo: {
stampId: string;
password: string;
};
buttonConfigs: Array<ButtonConfigItem>;
approvedType: ApproveType;
approvedContent: string;
approvedResult: string;
rejectNodeActivityId: string;
rejectNodeActivityIds: Array<rejectNodeItem>;
circulateConfigs: Array<MemberConfig>;
} = reactive({
isAddOrSubSign: false,
stampInfo: {
stampId: '',
password: '',
},
buttonConfigs: [],
approvedType: ApproveType.AGREE,
approvedResult: ApproveCode.AGREE,
approvedContent: '',
rejectNodeActivityId: '',
rejectNodeActivityIds: [],
circulateConfigs: [],
});
const { data, approveUserData, initProcessData, notificationError, notificationSuccess } =
userTaskItem();
onMounted(() => {
if (props.visible) {
approval();
}
});
const selectedPredecessorTasks = computed(() => {
return data.predecessorTasks.filter((ele) => {
return ele.taskId;
});
});
// 审批
async function approval() {
showLoading.value = true;
reset();
if (props.taskId) {
try {
let res = await getApprovalProcess(props.taskId, props.processId);
initProcessData(res);
if (res.buttonConfigs) {
approvalData.buttonConfigs = res.buttonConfigs;
}
if (res.relationTasks) {
data.predecessorTasks = res.relationTasks;
}
if (res.isAddOrSubSign) {
approvalData.isAddOrSubSign = res.isAddOrSubSign;
}
approvalData.approvedType = ApproveType.AGREE;
approvalData.approvedContent = '';
approvalData.rejectNodeActivityId = '';
approvalData.rejectNodeActivityIds = [];
approvalData.circulateConfigs = [];
showLoading.value = false;
showVisible.value = true;
} catch (error) {
showLoading.value = false;
emits('close');
}
} else {
// 只能选一个
showLoading.value = false;
showVisible.value = false;
}
}
async function changeApprovedType() {
if (approvalData.approvedType == ApproveType.AGREE) {
approvalData.approvedResult = ApproveCode.AGREE;
} else if (approvalData.approvedType == ApproveType.REJECT) {
approvalData.rejectNodeActivityIds = await getRejectNode(props.processId, props.taskId);
approvalData.approvedResult = ApproveCode.REJECT;
} else if (approvalData.approvedType == ApproveType.DISAGREE) {
approvalData.approvedResult = ApproveCode.DISAGREE;
} else if (approvalData.approvedType == ApproveType.FINISH) {
approvalData.approvedResult = ApproveCode.FINISH;
} else {
approvalData.approvedResult = '';
}
}
function changeButtonCodeType() {
approvalData.approvedType = ApproveType.OTHER;
}
function getUploadFileFolderIds(formModels) {
let fileFolderIds: Array<string> = [];
let uploadComponentIds = formInformation.value.getUploadComponentIds();
uploadComponentIds.forEach((ids) => {
if (ids.includes(separator)) {
let arr = ids.split(separator);
if (arr.length == 2 && formModels[arr[0]][arr[1]]) {
fileFolderIds.push(formModels[arr[0]][arr[1]]);
}
}
});
return fileFolderIds;
}
async function submit() {
data.submitLoading = true;
try {
let validateForms = await formInformation.value.validateForm();
if (validateForms.length > 0) {
let successValidate = validateForms.filter((ele) => {
return ele.validate;
});
if (successValidate.length == validateForms.length) {
let formModels = await formInformation.value.getFormModels();
let fileFolderIds: Array<string> = getUploadFileFolderIds(formModels);
let params: PostApprovalData = {
approvedType: approvalData.approvedResult
? ApproveType.OTHER
: approvalData.approvedType,
approvedResult: approvalData.approvedResult, // approvalData.approvedType 审批结果 如果为 4 就需要传buttonCode
approvedContent: approvalData.approvedContent,
formData: formModels,
rejectNodeActivityId: approvalData.rejectNodeActivityId,
taskId: props.taskId,
fileFolderIds,
circulateConfigs: approvalData.circulateConfigs,
stampId: approvalData.stampInfo.stampId,
stampPassword: approvalData.stampInfo.password,
};
let res = await postApproval(params);
// 下一节点审批人
let taskList: Array<ApproveTask> = [];
if (res && res.length > 0) {
taskList = res
.filter((ele) => {
return ele.isMultiInstance == false && ele.isAppoint == true;
})
.map((ele) => {
return {
taskId: ele.taskId,
taskName: ele.taskName,
provisionalApprover: ele.provisionalApprover,
selectIds: [],
};
});
if (taskList.length > 0) {
approveUserData.list = taskList;
approveUserData.schemaId = props.schemaId;
approveUserData.visible = true;
data.submitLoading = false;
} else {
close();
data.submitLoading = false;
save(true, t('审批流程'));
}
} else {
close();
data.submitLoading = false;
save(true, t('审批流程'));
}
} else {
data.submitLoading = false;
notificationError(t('审批流程'), t('表单校验未通过'));
}
}
} catch (error) {
data.submitLoading = false;
notificationError(t('审批流程'), t('审批流程失败'));
}
}
function changeApproveUserData() {
approveUserData.visible = false;
close();
}
function save(res: boolean, title: string) {
if (res) {
notificationSuccess(title);
close();
} else {
notificationError(title);
}
}
function close() {
showVisible.value = false;
emits('close');
}
function reset() {
approvalData.isAddOrSubSign = false;
approvalData.stampInfo = {
stampId: '',
password: '',
};
approvalData.buttonConfigs = [];
approvalData.approvedType = ApproveType.AGREE;
approvalData.approvedContent = '';
approvalData.rejectNodeActivityId = '';
approvalData.rejectNodeActivityIds = [];
approvalData.circulateConfigs = [];
}
</script>
<style lang="less" scoped>
.description-box {
display: flex;
flex-direction: column;
padding-top: 10px;
align-items: center;
justify-content: center;
color: rgba(102, 102, 102, 0.996);
margin-bottom: 20px;
.title {
align-self: flex-start;
margin-bottom: 20px;
}
.item-box {
align-self: flex-start;
}
.text-box {
display: flex;
margin: 10px 0;
.text-label {
width: 80px;
height: 40px;
display: inline-flex;
justify-content: flex-end;
margin-right: 4px;
}
}
}
// 传阅人
:deep(.opr-box) {
flex-direction: column !important;
.header-box {
flex-basis: 40px !important;
}
.button-box {
flex-direction: row !important;
}
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<ProcessLayout class="wrap" @click.stop="">
<template #title> {{ t('查看流程') }} </template>
<template #close>
<a-button type="primary" class="clean-icon" @click.stop="close">{{ t('关闭') }}</a-button>
</template>
<template #full>
<LookTask :processId="props.processId" />
</template>
</ProcessLayout>
</template>
<script setup lang="ts">
import ProcessLayout from './flow/Layout.vue';
import LookTask from './flow/LookTask.vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
let props = defineProps({
processId: String,
});
let emits = defineEmits(['close']);
function close() {
emits('close');
}
</script>

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>

View File

@ -0,0 +1,262 @@
<template>
<a-modal
v-model:visible="visible"
title="打印预览"
width="100%"
wrap-class-name="full-modal"
:bodyStyle="{ padding: '10px' }"
@cancel="emits('update:isShowPrint', false)"
>
<div class="btn-box">
<a-button style="color: #606266; font-size: 13px" @click="handlePrint">
<template #icon>
<PrinterOutlined />
</template>
打印
</a-button>
</div>
<div v-if="printData?.style === '1' && !!configs?.formProps">
<SimpleForm
class="print-style1-box"
:formProps="configs?.formProps"
:formModel="configs?.formModel"
:isWorkFlow="true"
/>
</div>
<div :class="`print-style${props.printData?.style}-box`" v-else>
<template v-for="(item, index) in formInfo" :key="index">
<TableStyle :item="item" :componentType="item.type" />
</template>
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, computed, Ref, StyleValue, provide, inject, onMounted } from 'vue';
import SimpleForm from '/@/components/SimpleForm/src/SimpleForm.vue';
import TableStyle from './TableStyle.vue';
import { getUserMulti } from '/@/api/system/user';
import { getDicItemDetail } from '/@/api/system/dic';
import { getDepartment } from '/@/api/system/department';
import { getAreaMulti } from '/@/api/system/area';
import { PrinterOutlined } from '@ant-design/icons-vue';
import { buildComponentType } from '/@/utils/helper/designHelper';
import html2canvas from 'html2canvas';
import printJS from 'print-js';
import { cloneDeep, isNil } from 'lodash-es';
import domtoimage from 'dom-to-image';
const props = defineProps({
formConfigs: Object,
printData: Object,
isShowPrint: Boolean,
});
const emits = defineEmits(['update:isShowPrint']);
const configs = ref({
formProps: {} as any,
formModel: {},
});
const formInfo = ref<any>([]);
const activeKey = inject<Ref<number>>('tabActiveKey');
const visible = ref<boolean>(props.isShowPrint);
const borderColorStyle = computed(() => {
return { 'border-color': `${props.printData?.borderColor} !important` };
});
const underlineStyle = computed(() => {
return { 'border-bottom': props.printData?.underline === '1' ? '1px solid' : 0 };
});
provide<Ref<StyleValue>>('borderColorStyle', borderColorStyle);
provide<Ref<StyleValue>>('underlineStyle', underlineStyle);
onMounted(async () => {
configs.value.formProps = cloneDeep(props.formConfigs?.formProps);
formInfo.value = [];
if (props.printData?.style !== '1') {
await changeFormat(configs.value.formProps.schemas, formInfo.value);
}
});
configs.value.formModel = cloneDeep(props.formConfigs?.formModel);
const handlePrint = async () => {
let element: HTMLElement = window.document.querySelector(
`.print-style${props.printData?.style || 1}-box`,
)!;
let url;
if (props.printData?.style === '1') {
url = await domtoimage.toPng(element);
} else {
let canvas = await html2canvas(element, {
backgroundColor: null,
useCORS: true,
windowHeight: document.body.scrollHeight,
});
url = canvas.toDataURL();
}
printJS({
printable: url,
type: 'image',
documentTitle: '打印',
});
};
const changeFormat = async (schemas, info) => {
if (!configs.value) return;
const keys = Object.keys(configs.value.formModel);
for (const item of schemas) {
if (['tab', 'grid', 'card'].includes(item.type)) {
const layoutChildren = {};
for (let index = 0; index < item.children.length; index++) {
const name =
item.type === 'grid'
? index
: item.type === 'card'
? item.componentProps?.title
: item.children[index].name;
layoutChildren[name] = [];
if ((index === activeKey?.value && item.type === 'tab') || item.type !== 'tab') {
await changeFormat(item.children[index].list, layoutChildren[name]);
}
}
info.push({
type: item.type,
value: layoutChildren,
});
} else if (item.type === 'form') {
if (keys.includes(item.componentProps.mainKey)) {
// const children: object[] = [];
// configs.value.formModel[item.componentProps.mainKey].forEach((sub) => {
// const subInfo = {};
// item.componentProps.columns.forEach((col) => {
// if (Object.keys(sub).includes(col.dataIndex) && !!col.show) {
// subInfo[col.title] = sub[col.dataIndex];
// }
// });
// children.push(subInfo);
// });
const columns: any[] = [];
const value: any[] = [];
configs.value.formModel[item.componentProps.mainKey].forEach((sub) => {
let val = {};
item.componentProps.columns.forEach(async (col) => {
if (Object.keys(sub).includes(col.dataIndex) && !!col.show) {
if (!columns.map((x) => x.key).includes(col.dataIndex)) {
columns.push({
title: col.title,
key: col.dataIndex,
dataIndex: col.dataIndex,
});
}
val[col.dataIndex] = await changeValue(col, sub[col.dataIndex]);
}
});
value.push(val);
});
info.push({
type: item.type,
label: item.label,
value,
columns,
});
}
} else if (keys.includes(item.field) && !!item.show) {
let value = await changeValue(item, configs.value.formModel[item.field]);
info.push({
label: item.label,
type: item.type,
componentProps: item.componentProps,
value,
});
}
}
};
const changeValue = async (item, fieldValue) => {
const type = item.type ? buildComponentType(item.type) : item.componentType;
if (isNil(fieldValue)) return fieldValue;
if (type === 'User' || (type === 'Info' && item.componentProps?.infoType === 0)) {
const res = await getUserMulti(fieldValue);
return res?.map((x) => x.name).toString();
} else if (type === 'Dept' || (type === 'Info' && item.componentProps?.infoType === 1)) {
const res = await getDepartment(fieldValue);
return res?.name;
} else if (type === 'Area') {
const res = await getAreaMulti(fieldValue);
return res?.map((x) => x.name).join(' / ');
} else if (item.componentProps?.datasourceType === 'dic') {
const res = await getDicItemDetail(item.componentProps?.params.itemId, fieldValue);
return res?.map((x) => x.name).toString();
}
return fieldValue;
};
</script>
<style lang="less" scoped>
.btn-box {
width: 100%;
text-align: right;
margin-bottom: 10px;
}
.borderClass {
border: 0 !important;
}
.print-style2-box,
.print-style3-box,
.print-style4-box {
margin: 10px 20px;
:deep(.ant-row) {
font-size: 16px;
border: 1px solid !important;
.ant-col {
padding: 15px 5px;
}
}
}
.print-style3-box {
:deep(.ant-col:last-child) {
border-left: 0 !important;
}
}
.print-style2-box {
:deep(.ant-col:first-child) {
border-right: 1px solid;
}
}
.print-style4-box {
:deep(.ant-row) {
border-left: 0 !important;
border-right: 0 !important;
border-top: 0 !important;
}
}
</style>
<style lang="less">
.full-modal {
.ant-modal {
max-width: 100%;
top: 0 !important;
padding-bottom: 0;
margin: 0;
height: 100%;
}
.ant-modal-content {
display: flex;
flex-direction: column;
min-height: 100%;
}
.ant-modal-body {
flex: 1;
}
.ant-modal-footer {
display: none !important;
}
}
</style>

View File

@ -0,0 +1,176 @@
<template>
<template v-if="componentType === 'form'">
<a-row :style="borderColorStyle">
<a-col :span="3" :style="borderColorStyle">
{{ item?.label }}
</a-col>
<a-col :span="21" :style="borderColorStyle">
<a-table
:dataSource="item?.value"
:columns="item?.columns"
:pagination="false"
tableLayout="fixed"
v-if="item?.value.length"
/>
<!-- <table :cellpadding="10" :cellspacing="10" v-if="item?.value.length">
<thead>
<tr>
<th :style="borderColorStyle" v-for="title in Object.keys(item?.value[0])">
{{ title }}
</th>
</tr>
</thead>
<tbody>
<tr v-for="value in item?.value">
<td :style="borderColorStyle" v-for="subFormInfo in Object.values(value)">
{{ subFormInfo }}
</td>
</tr>
</tbody>
</table> -->
</a-col>
</a-row>
</template>
<table
:cellpadding="10"
:cellspacing="10"
v-else-if="item?.type === 'tab' || item?.type === 'card'"
>
<template v-if="Object.keys(item?.value)">
<thead>
<tr>
<th
:style="borderColorStyle"
v-for="(title, index) in Object.keys(item?.value)"
:key="index"
>
{{ title }}
</th>
</tr>
</thead>
<tbody>
<template v-for="(value, index) in item?.value" :key="index">
<tr v-if="!!value.length">
<td :style="borderColorStyle" :colspan="Object.keys(item?.value).length">
<template v-for="(info, idx) in Object.values(value)" :key="idx">
<TableStyle :item="info" :componentType="info.type" />
</template>
</td>
</tr>
</template>
</tbody>
</template>
</table>
<div class="grid-box" v-else-if="item?.type === 'grid'">
<div class="grid-box-left">
<TableStyle
v-for="(gridItem, index) in item.value[0]"
:key="index"
:item="gridItem"
:componentType="gridItem.type"
/>
</div>
<div class="w-1/2">
<TableStyle
v-for="(gridItem, index) in item.value[1]"
:key="index"
:item="gridItem"
:componentType="gridItem.type"
/>
</div>
</div>
<div
v-else-if="noBorderComponent.includes(item?.type)"
:style="item?.type === 'title' ? underlineStyle : ''"
class="noborder-box"
>
<a-divider
v-if="item?.type === 'divider'"
:orientation="item?.componentProps?.orientation"
:style="item?.componentProps?.style"
>
{{ item?.label }}
</a-divider>
<h2
v-else-if="item?.type === 'title'"
:align="item.componentProps.align"
:style="{
fontWeight: 'bold',
fontSize: item.componentProps.fontSize + 'px',
color: item.componentProps.color,
...item.componentProps.style,
}"
>
{{ item?.label }}
</h2>
</div>
<template v-else>
<a-row :style="borderColorStyle">
<a-col :span="3" :style="borderColorStyle">
{{ item?.label }}
</a-col>
<a-col :span="21">
<span>{{ item?.value }}</span>
</a-col>
</a-row>
</template>
</template>
<script lang="ts" setup>
import { inject, StyleValue, Ref } from 'vue';
defineProps({
item: Object,
componentType: String,
});
const borderColorStyle = inject<Ref<StyleValue>>('borderColorStyle')!;
const underlineStyle = inject<Ref<StyleValue>>('underlineStyle')!;
const noBorderComponent = ['title', 'divider'];
</script>
<style lang="less" scoped>
table {
width: 100%;
thead {
text-align: left;
}
td,
th {
border: 1px solid;
}
}
:deep(.ant-table-thead) tr th {
border: 1px solid;
background: #fff;
}
:deep(.ant-table-tbody) tr td {
border: 1px solid;
border-bottom: 1px solid #000;
}
.noborder-box {
width: 100%;
padding: 10px;
}
.grid-box {
display: flex;
border: 1px solid;
width: 100%;
.grid-box-left {
border-right: 1px solid;
width: 50%;
}
.ant-row {
border: 0 !important;
}
.ant-row:not(:last-child) {
border-bottom: 1px solid !important;
}
}
</style>

View File

@ -0,0 +1,123 @@
<template>
<BasicTable @register="registerTable">
<template #action="{ record }">
<TableAction
:actions="[
{
icon: 'clarity:note-edit-line',
auth: 'processtasks:edit',
onClick: handleEdit.bind(null, record),
},
{
icon: 'ant-design:delete-outlined',
auth: 'processtasks:delete',
color: 'error',
popConfirm: {
title: t('是否确认删除'),
confirm: handleDelete.bind(null, record),
},
},
]"
/>
</template>
</BasicTable>
<LaunchProcess
v-if="processData.visible"
:draftsId="processData.draftsId"
:schemaId="processData.schemaId"
:draftsJsonStr="processData.draftsJsonStr"
@close="processData.visible = false"
/>
</template>
<script setup lang="ts">
import userTaskTable from './../../hooks/userTaskTable';
import LaunchProcess from './../LaunchProcess.vue';
import { BasicTable, useTable, TableAction, BasicColumn } from '/@/components/Table';
import { deleteDraft, getDraftInfo, getSchemaTask } from '/@/api/workflow/process';
import { reactive } from 'vue';
import { notification } from 'ant-design-vue';
import { TaskTypeUrl } from '/@/enums/workflowEnum';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const configColumns: BasicColumn[] = [
{
title: t('流程名称'),
dataIndex: 'schemaName',
align: 'left',
},
{
title: t('发起者'),
dataIndex: 'originator',
sorter: true,
align: 'left',
},
{
title: t('发起时间'),
dataIndex: 'createDate',
align: 'left',
},
];
const processData = reactive({
visible: false,
schemaId: '',
draftsJsonStr: '',
draftsId: '',
});
const { formConfig } = userTaskTable();
const [registerTable, { reload }] = useTable({
title: t('草稿箱列表'),
api: getSchemaTask,
rowKey: 'id',
columns: configColumns,
formConfig: formConfig('Drafts'),
beforeFetch: (params) => {
return { data: params, taskUrl: TaskTypeUrl.DRAFT };
},
useSearchForm: true,
showTableSetting: true,
striped: false,
pagination: {
pageSize: 18,
},
actionColumn: {
width: 80,
title: t('操作'),
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
async function handleEdit(record: Recordable) {
try {
let res = await getDraftInfo(record.id);
processData.draftsId = record.id;
processData.schemaId = res.schemaId;
processData.draftsJsonStr = res.formData;
processData.visible = true;
} catch (error) {}
}
async function handleDelete(record: Recordable) {
try {
let res = await deleteDraft([record.id]);
if (res) {
notification.open({
type: 'success',
message: t('删除'),
description: t('删除成功'),
});
reload();
} else {
notification.open({
type: 'error',
message: t('删除'),
description: t('删除失败'),
});
}
} catch (error) {}
}
</script>
<style scoped></style>

View File

@ -0,0 +1,87 @@
<template>
<BasicTable @register="registerTable" @selection-change="selectionChange">
<template #toolbar>
<LookProcess :taskId="taskId" :processId="processId" @close="reload"
><a-button v-auth="'processtasks:view'">{{ t('查看') }}</a-button></LookProcess
>
</template>
<template #currentProgress="{ record }">
<a-progress v-if="record.currentProgress" :percent="record.currentProgress" size="small" />
</template>
</BasicTable>
</template>
<script setup lang="ts">
import userTaskTable from './../../hooks/userTaskTable';
import LookProcess from './../LookProcess.vue';
import { BasicTable, useTable, BasicColumn } from '/@/components/Table';
import { getSchemaTask } from '/@/api/workflow/process';
import { TaskTypeUrl } from '/@/enums/workflowEnum';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const configColumns: BasicColumn[] = [
{
title: t('流水号'),
dataIndex: 'serialNumber',
width: 50,
sorter: true,
},
{
title: t('流程名称'),
dataIndex: 'processName',
width: '32%',
align: 'left',
},
{
title: t('任务名称'),
dataIndex: 'taskName',
sorter: true,
width: '17%',
align: 'left',
},
{
title: t('当前进度'),
dataIndex: 'currentProgress',
sorter: true,
width: '17%',
slots: { customRender: 'currentProgress' },
},
{
title: t('发起人'),
dataIndex: 'originator',
align: 'left',
width: 80,
},
{
title: t('发起时间'),
width: 120,
dataIndex: 'createTime',
align: 'left',
},
];
const { formConfig, processId, taskId, selectionChange } = userTaskTable();
const [registerTable, { reload }] = useTable({
title: t('我的传阅列表'),
api: getSchemaTask,
rowKey: 'taskId',
columns: configColumns,
formConfig: formConfig(),
beforeFetch: (params) => {
return { data: params, taskUrl: TaskTypeUrl.CIRCULATED };
},
rowSelection: {
type: 'radio',
},
useSearchForm: true,
showTableSetting: true,
striped: false,
pagination: {
pageSize: 18,
},
});
</script>
<style scoped></style>

View File

@ -0,0 +1,181 @@
<template>
<BasicTable @register="registerTable" @selection-change="selectionChange">
<template #toolbar>
<RejectProcess
:processId="processId"
:taskId="taskId"
@close="reload"
@restart="restartProcess"
><a-button v-auth="'processtasks:withdraw'">{{ t('撤回') }}</a-button></RejectProcess
>
<LookProcess :taskId="taskId" :processId="processId" @close="reload"
><a-button v-auth="'processtasks:view'">{{ t('查看') }}</a-button></LookProcess
>
<a-button v-auth="'processtasks:relaunch'" @click="restartProcess">{{
t('重新发起')
}}</a-button>
</template>
<template #currentProgress="{ record }">
<a-progress v-if="record.currentProgress" :percent="record.currentProgress" size="small" />
</template>
<template #action="{ record }">
<TableAction
:actions="[
{
icon: 'ant-design:delete-outlined',
auth: 'processtasks:delete',
color: 'error',
popConfirm: {
title: t('移入回收站'),
confirm: handleDelete.bind(null, record),
},
},
]"
/>
</template>
</BasicTable>
<LaunchProcess
v-if="restartProcessVisible"
:schemaId="schemaId"
:taskId="taskId"
:processId="processId"
@close="restartProcessClose"
/>
</template>
<script setup lang="ts">
import userTaskTable from './../../hooks/userTaskTable';
import { ref, unref, watch } from 'vue';
import LookProcess from './../LookProcess.vue';
import LaunchProcess from './../LaunchProcess.vue';
import RejectProcess from './../RejectProcess.vue';
import { BasicTable, useTable, TableAction, BasicColumn } from '/@/components/Table';
import { getSchemaTask, moveRecycle } from '/@/api/workflow/process';
import { notification } from 'ant-design-vue';
import { TaskTypeUrl } from '/@/enums/workflowEnum';
import { useI18n } from '/@/hooks/web/useI18n';
import { useRouter } from 'vue-router';
const { t } = useI18n();
const restartProcessVisible = ref(false);
const configColumns: BasicColumn[] = [
{
title: t('流水号'),
dataIndex: 'serialNumber',
width: 50,
sorter: true,
},
{
title: t('流程名称'),
dataIndex: 'processName',
align: 'left',
width: '32%',
sorter: true,
},
{
title: t('任务名称'),
dataIndex: 'currentTaskName',
sorter: true,
width: '17%',
align: 'left',
},
{
title: t('当前进度'),
dataIndex: 'currentProgress',
sorter: true,
width: '17%',
slots: { customRender: 'currentProgress' },
},
{
title: t('发起人'),
dataIndex: 'originator',
align: 'left',
width: 80,
},
{
title: t('发起时间'),
align: 'left',
width: 120,
dataIndex: 'createTime',
},
];
const { formConfig, processId, taskId, schemaId, selectionChange } = userTaskTable();
const [registerTable, { reload }] = useTable({
title: t('我的流程列表'),
api: getSchemaTask,
rowKey: 'id',
columns: configColumns,
formConfig: formConfig('MyProcess'),
rowSelection: {
type: 'radio',
},
beforeFetch: (params) => {
return { data: params, taskUrl: TaskTypeUrl.MY_PROCESS };
},
useSearchForm: true,
showTableSetting: true,
striped: false,
pagination: {
pageSize: 18,
},
actionColumn: {
width: 60,
title: t('操作'),
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
function restartProcess() {
if (processId.value) {
restartProcessVisible.value = true;
} else {
notification.open({
type: 'error',
message: t('提示'),
description: t('请选择一个流程重新发起'),
});
}
}
function restartProcessClose() {
restartProcessVisible.value = false;
reload();
}
async function handleDelete(record: Recordable) {
if (record.processId) {
try {
let res = await moveRecycle(record.processId);
if (res) {
notification.open({
type: 'success',
message: t('移入回收站'),
description: t('移入回收站成功'),
});
reload();
} else {
notification.open({
type: 'error',
message: t('移入回收站'),
description: t('移入回收站失败'),
});
}
} catch (error) {}
}
}
const { currentRoute } = useRouter();
watch(
() => unref(currentRoute),
(val) => {
if (val.name == 'ProcessTasks') reload();
},
{ deep: true },
);
</script>
<style scoped></style>

View File

@ -0,0 +1,136 @@
<template>
<BasicTable @register="registerTable" @selection-change="selectionChange">
<template #toolbar>
<LookProcess :taskId="taskId" :processId="processId" @close="reload">
<a-button v-auth="'processtasks:view'">{{ t('查看') }} </a-button>
</LookProcess>
<RestartProcess :schemaId="schemaId" :taskId="taskId" :processId="processId" @close="reload">
<a-button v-auth="'processtasks:relaunch'">{{ t('重新发起') }}</a-button>
</RestartProcess>
</template>
<template #currentProgress="{ record }">
<a-progress v-if="record.currentProgress" :percent="record.currentProgress" size="small" />
</template>
<template #action="{ record }">
<TableAction
:actions="[
{
icon: 'ant-design:delete-outlined',
auth: 'processtasks:delete',
color: 'error',
popConfirm: {
title: t('是否确认删除'),
confirm: handleDelete.bind(null, record),
},
},
]"
/>
</template>
</BasicTable>
</template>
<script setup lang="ts">
import userTaskTable from './../../hooks/userTaskTable';
import LookProcess from './../LookProcess.vue';
import RestartProcess from './../RestartProcess.vue';
import { BasicTable, useTable, TableAction, BasicColumn } from '/@/components/Table';
import { deleteRecycle, getSchemaTask } from '/@/api/workflow/process';
import { notification } from 'ant-design-vue';
import { TaskTypeUrl } from '/@/enums/workflowEnum';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const configColumns: BasicColumn[] = [
{
title: t('流水号'),
dataIndex: 'serialNumber',
width: 80,
},
{
title: t('流程名称'),
dataIndex: 'processName',
width: '32%',
align: 'left',
},
{
title: t('任务名称'),
dataIndex: 'currentTaskName',
width: '17%',
align: 'left',
},
{
title: t('当前进度'),
dataIndex: 'currentProgress',
width: '17%',
align: 'left',
slots: { customRender: 'currentProgress' },
},
{
title: t('发起人'),
dataIndex: 'originator',
width: 80,
align: 'left',
},
{
title: t('发起时间'),
width: 140,
dataIndex: 'createTime',
align: 'left',
},
];
const { formConfig, processId, taskId, schemaId, selectionChange } = userTaskTable();
const [registerTable, { reload }] = useTable({
title: t('回收站列表'),
api: getSchemaTask,
rowKey: 'taskId',
columns: configColumns,
formConfig: formConfig(),
rowSelection: {
type: 'radio',
},
beforeFetch: (params) => {
return { data: params, taskUrl: TaskTypeUrl.RECYCLE };
},
useSearchForm: true,
showTableSetting: true,
striped: false,
pagination: {
pageSize: 18,
},
actionColumn: {
width: 60,
title: t('操作'),
dataIndex: 'action',
slots: { customRender: 'action' },
fixed: undefined,
},
});
async function handleDelete(record: Recordable) {
if (record.processId) {
try {
let res = await deleteRecycle([record.processId]);
if (res) {
notification.open({
type: 'success',
message: t('移入回收站'),
description: t('移入回收站成功'),
});
reload();
} else {
notification.open({
type: 'error',
message: t('移入回收站'),
description: t('移入回收站失败'),
});
}
} catch (error) {}
}
}
</script>
<style scoped></style>

View File

@ -0,0 +1,117 @@
<template>
<BasicTable @register="registerTable" @selection-change="selectionChange">
<template #toolbar>
<div class="button-box">
<RejectProcess
:taskId="taskId"
:processId="processId"
@close="reload"
@restart="restartProcess"
class="mr-2"
><a-button v-auth="'processtasks:withdraw'">{{ t('撤回') }}</a-button></RejectProcess
>
<LookProcess :taskId="taskId" :processId="processId" @close="reload"
><a-button v-auth="'processtasks:view'">{{ t('查看') }}</a-button></LookProcess
>
</div>
</template>
<template #currentProgress="{ record }">
<a-progress v-if="record.currentProgress" :percent="record.currentProgress" size="small" />
</template>
</BasicTable>
<LaunchProcess
v-if="restartProcessVisible"
:schemaId="schemaId"
:taskId="taskId"
@close="restartProcessClose"
/>
</template>
<script setup lang="ts">
import userTaskTable from './../../hooks/userTaskTable';
import { ref } from 'vue';
import LookProcess from './../LookProcess.vue';
import LaunchProcess from './../LaunchProcess.vue';
import RejectProcess from './../RejectProcess.vue';
import { BasicTable, useTable, BasicColumn } from '/@/components/Table';
import { getSchemaTask } from '/@/api/workflow/process';
import { TaskTypeUrl } from '/@/enums/workflowEnum';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
const restartProcessVisible = ref(false);
const configColumns: BasicColumn[] = [
{
title: t('流水号'),
dataIndex: 'serialNumber',
width: 80,
},
{
title: t('流程名称'),
dataIndex: 'processName',
width: '32%',
align: 'left',
},
{
title: t('任务名称'),
dataIndex: 'currentTaskName',
width: '17%',
align: 'left',
},
{
title: t('当前进度'),
dataIndex: 'currentProgress',
width: '17%',
slots: { customRender: 'currentProgress' },
},
{
title: t('发起人'),
dataIndex: 'originator',
align: 'left',
width: 80,
},
{
title: t('发起时间'),
dataIndex: 'createTime',
align: 'left',
width: 120,
},
];
const { formConfig, processId, taskId, schemaId, selectionChange } = userTaskTable();
const [registerTable, { reload }] = useTable({
title: t('已办任务列表'),
api: getSchemaTask,
rowKey: 'id',
columns: configColumns,
formConfig: formConfig(),
beforeFetch: (params) => {
return { data: params, taskUrl: TaskTypeUrl.FINISHED_TASKS };
},
rowSelection: {
type: 'radio',
},
useSearchForm: true,
showTableSetting: true,
striped: false,
pagination: {
pageSize: 18,
},
indexColumnProps: {
width: 50,
},
});
function restartProcess() {
restartProcessVisible.value = true;
}
function restartProcessClose() {
restartProcessVisible.value = false;
reload();
}
</script>
<style scoped></style>

View File

@ -0,0 +1,132 @@
<template>
<BasicTable @register="registerTable" @selection-change="selectionChange">
<template #toolbar>
<BatchApprovalProcess
v-if="showBatchApproval"
@close="BatchClearHandler"
:selectedRows="data.selectedRows"
>
<a-button v-auth="'processtasks:batchApproval'">{{ t('批量审批') }}</a-button>
</BatchApprovalProcess>
<ApprovalProcess
v-else
:taskId="taskId"
:processId="processId"
:schemaId="schemaId"
@close="clearHandler"
:visible="false"
>
<a-button v-auth="'processtasks:approve'">{{ t('审批') }}</a-button>
</ApprovalProcess>
<LookProcess :taskId="taskId" :processId="processId" @close="clearHandler">
<a-button v-auth="'processtasks:view'">{{ t('查看') }}</a-button>
</LookProcess>
</template>
<template #currentProgress="{ record }">
<a-progress v-if="record.currentProgress" :percent="record.currentProgress" size="small" />
</template>
</BasicTable>
<InfoModal @register="registerModal" @success="reload" />
</template>
<script setup lang="ts">
import userTaskTable from './../../hooks/userTaskTable';
import { useModal } from '/@/components/Modal';
import LookProcess from './../LookProcess.vue';
import ApprovalProcess from './../ApprovalProcess.vue';
import BatchApprovalProcess from './../BatchApprovalProcess.vue';
import InfoModal from '../BatchApprovalInfo.vue';
import { BasicTable, useTable, BasicColumn } from '/@/components/Table';
import { getSchemaTask } from '/@/api/workflow/process';
import { TaskTypeUrl } from '/@/enums/workflowEnum';
import { useI18n } from '/@/hooks/web/useI18n';
import { unref, watch } from 'vue';
import { useRouter } from 'vue-router';
const { t } = useI18n();
const configColumns: BasicColumn[] = [
{
title: t('流水号'),
dataIndex: 'serialNumber',
width: 80,
align: 'left',
},
{
title: t('流程名称'),
dataIndex: 'processName',
width: '32%',
align: 'left',
},
{
title: t('任务名称'),
dataIndex: 'taskName',
width: '17%',
align: 'left',
},
{
title: t('当前进度'),
dataIndex: 'currentProgress',
width: '17%',
align: 'left',
slots: { customRender: 'currentProgress' },
},
{
title: t('发起人'),
dataIndex: 'startUserName',
width: 80,
align: 'left',
},
{
title: t('发起时间'),
width: 120,
dataIndex: 'startTime',
align: 'left',
},
];
const [registerModal, { openModal }] = useModal();
const { formConfig, data, processId, taskId, schemaId, selectionChange, showBatchApproval } =
userTaskTable();
function BatchClearHandler(v) {
if (v) {
openModal(true, v);
}
clearSelectedRowKeys();
}
const clearHandler = () => {
clearSelectedRowKeys();
reload();
};
const [registerTable, { reload, clearSelectedRowKeys }] = useTable({
title: t('待办任务列表'),
api: getSchemaTask,
rowKey: 'id',
columns: configColumns,
formConfig: formConfig(),
beforeFetch: (params) => {
return { data: params, taskUrl: TaskTypeUrl.PENDING_TASKS };
},
rowSelection: {
type: 'checkbox',
},
useSearchForm: true,
showTableSetting: true,
striped: false,
pagination: {
pageSize: 18,
},
tableSetting: {
size: false,
setting: false,
},
});
const { currentRoute } = useRouter();
watch(
() => unref(currentRoute),
(val) => {
if (val.name == 'ProcessTasks') reload();
},
{ deep: true },
);
</script>
<style scoped></style>

View File

@ -0,0 +1,69 @@
<template>
<a-input-group compact class="box" @click.stop="">
<a-select
:value="props.stampId"
:placeholder="t('请选择电子签章')"
allowClear
style="width: calc(100% - 48px)"
@change="changeIds"
>
<a-select-option :value="bind.id" v-for="bind in data.list" :key="bind.id">
{{ bind.name }}
</a-select-option>
</a-select>
<a-button class="fixed" @click.stop="data.visible = true">
<Icon icon="ant-design:plus-outlined"
/></a-button>
<StampDetail v-if="data.visible" :type="StampType.PRIVATE_SIGNATURE" @close="close" />
</a-input-group>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue';
import { Icon } from '/@/components/Icon';
import StampDetail from './StampInfo.vue';
import { getStampPage } from '/@/api/workflow/stamp';
import { StampInfo } from '/@/api/workflow/model';
import { StampType } from '/@/enums/workflowEnum';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
let emits = defineEmits(['update:stampId']);
const props = defineProps({
stampId: String,
});
let data: { visible: boolean; list: Array<StampInfo> } = reactive({
visible: false,
list: [],
});
onMounted(async () => {
await getList();
});
function close() {
data.visible = false;
getList();
}
async function getList() {
let options = await getStampPage(StampType.PRIVATE_SIGNATURE, {
limit: 1,
size: 100,
});
data.list = options.list;
data.list.forEach((o) => {
if (o.isDefault === 1) {
emits('update:stampId', o.id);
}
});
}
function changeIds(val) {
console.log('val: ', val);
emits('update:stampId', val);
}
</script>
<style scoped>
.fixed {
font-weight: 700;
width: 49px;
}
</style>

View File

@ -0,0 +1,100 @@
<template>
<div @click.stop="open" class="sign-box">
<img v-if="data.imgUrl" class="imgCanvas" :src="data.imgUrl" />
<span v-else>{{ t('点击手写签名') }}</span>
</div>
<a-modal
v-model:visible="data.visible"
:width="600"
:title="t('签名')"
@ok="confirm"
@cancel="cancel"
>
<vue-esign
v-if="data.visible"
ref="canvas"
:width="800"
:height="300"
:isCrop="signInfo.isCrop"
:lineWidth="signInfo.lineWidth"
:lineColor="signInfo.lineColor"
v-model:bgColor="signInfo.bgColor"
/>
</a-modal>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue';
import vueEsign from 'vue-esign';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
let emits = defineEmits(['submit']);
let props = defineProps(['src']);
const canvas = ref();
const data: {
visible: boolean;
imgUrl: string;
} = reactive({
visible: false,
imgUrl: '',
});
const signInfo = reactive({
lineWidth: 6,
lineColor: '#000000',
bgColor: '',
resultImg: '',
isCrop: false,
});
onMounted(() => {
if (props.src) {
data.imgUrl = props.src;
}
});
function open() {
data.visible = true;
if (props.src) {
data.imgUrl = props.src;
}
}
function cancel() {
canvas.value.reset();
}
function confirm() {
canvas.value
.generate()
.then((res) => {
if (res) {
emits('submit', res);
data.imgUrl = res;
data.visible = false;
}
})
.catch((err) => {
console.log('err: ', err);
});
}
</script>
<style lang="less" scoped>
.sign-box {
width: 104px;
height: 104px;
margin-right: 8px;
margin-bottom: 8px;
text-align: center;
vertical-align: top;
background-color: #fafafa;
border: 1px dashed #d9d9d9;
border-radius: 2px;
cursor: pointer;
transition: border-color 0.3s;
display: flex;
justify-content: center;
align-items: center;
}
.sign {
width: 100%;
height: 300px;
border: 1px solid rgb(0 0 0 / 20%);
padding: 10px;
}
</style>

View File

@ -0,0 +1,300 @@
<template>
<a-modal
v-model:visible="data.visible"
:maskClosable="false"
:width="600"
:title="title"
@ok="submit"
@cancel="close"
:okText="t('确认')"
:cancelText="t('取消')"
>
<div class="box" v-if="data.visible">
<a-form
:model="data.info"
ref="formRef"
name="basic"
:label-col="{ span: 4 }"
:wrapper-col="{ span: 20 }"
autocomplete="off"
>
<a-form-item
:label="t('签章名称')"
name="name"
:rules="[{ required: true, message: t('请填写签章名称!') }]"
>
<a-input
:placeholder="t('请填写签章名称')"
v-model:value="data.info.name"
style="width: 100%"
/>
</a-form-item>
<a-form-item
:label="t('印章分类')"
name="stampCategory"
:rules="[{ required: true, message: t('请填写印章分类!') }]"
>
<a-select
v-model:value="data.info.stampCategory"
:placeholder="t('请选择印章分类')"
style="width: 100%"
>
<a-select-option v-for="item in data.categoryOptions" :key="item.id" :value="item.id">
{{ item.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item
:label="t('签章密码')"
name="password"
:rules="[{ required: true, message: t('请填写签章密码!') }]"
>
<a-input-password
:placeholder="t('请填写签章密码')"
v-model:value="data.info.password"
style="width: 100%"
/>
</a-form-item>
<a-form-item
:label="t('签章排序')"
name="sortCode"
:rules="[{ required: true, message: t('请填写签章排序!') }]"
>
<a-input-number
:placeholder="t('请填写签章排序')"
v-model:value="data.info.sortCode"
style="width: 100%"
/>
</a-form-item>
<a-form-item
:label="t('签章类型')"
name="fileType"
:rules="[{ required: true, message: t('请填写签章类型!') }]"
>
<a-radio-group v-model:value="data.info.fileType" name="radioGroup">
<a-radio :value="StampFileTypeAttributes.UPLOAD_PICTURES">{{ t('上传照片') }}</a-radio>
<a-radio :value="StampFileTypeAttributes.HANDWRITTEN_SIGNATURE">{{
t('手写签名')
}}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
:label="t('上传照片')"
name="fileUrl"
v-if="data.info.fileType === StampFileTypeAttributes.UPLOAD_PICTURES"
>
<a-upload
v-if="data.info.fileType === StampFileTypeAttributes.UPLOAD_PICTURES"
name="file"
accept="image/*"
:headers="data.headers"
:max-count="1"
:showUploadList="false"
:action="data.action"
list-type="picture-card"
@change="photoChange"
>
<img v-if="data.photoUrl" :src="data.photoUrl" />
<div v-else>{{ t('点击上传照片') }}</div>
</a-upload>
</a-form-item>
<a-form-item
:label="t('手写签名')"
name="fileUrl"
v-if="data.info.fileType === StampFileTypeAttributes.HANDWRITTEN_SIGNATURE"
>
<Sign
v-if="data.info.fileType === StampFileTypeAttributes.HANDWRITTEN_SIGNATURE"
:src="data.signUrl"
@submit="uploadSign"
/>
</a-form-item>
<a-form-item :label="t('签章备注')" name="remark">
<a-textarea
v-model:value="data.info.remark"
:placeholder="t('请填写签章备注')"
:auto-size="{ minRows: 2, maxRows: 5 }"
style="width: 100%"
/>
</a-form-item>
</a-form>
</div>
</a-modal>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue';
import { notification } from 'ant-design-vue';
import { StampInfo } from '/@/api/workflow/model';
import { TreeItem } from '/@/components/Tree';
import Sign from './Sign.vue';
import { getDicDetailList } from '/@/api/system/dic';
import { uploadSrc, uploadBlobApi } from '/@/api/sys/upload';
import { postStamp, putStamp } from '/@/api/workflow/stamp';
import { StampCategory } from '/@/enums/workflowEnum';
import { getAppEnvConfig } from '/@/utils/env';
import { getToken } from '/@/utils/auth';
import type { UploadChangeParam } from 'ant-design-vue';
import { message } from 'ant-design-vue';
import { cloneDeep } from 'lodash-es';
import { StampType, StampFileTypeAttributes } from '/@/enums/workflowEnum';
import { dataURLtoBlob } from '/@/utils/file/base64Conver';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
let props = defineProps(['id', 'type', 'info']);
const formRef = ref();
let emits = defineEmits(['close']);
const title = computed(() => {
return props.id == '' ? t('新增签章') : t('编辑签章');
});
const defaultInfo: StampInfo = {
enabledMark: 0,
fileType: StampFileTypeAttributes.UPLOAD_PICTURES,
fileUrl: '',
password: '',
stampCategory: undefined,
stampType: StampType.PUBLIC_SIGNATURE,
name: '',
sortCode: 0,
remark: '',
id: '',
maintain: '',
};
const data: {
visible: boolean;
categoryOptions: TreeItem[];
info: StampInfo;
action: string;
headers: { Authorization: string };
photoUrl: string;
signUrl: string;
} = reactive({
visible: false,
categoryOptions: [],
info: defaultInfo,
action: '',
headers: { Authorization: '' },
photoUrl: '',
signUrl: '',
});
const onCheck = async () => {
try {
await formRef.value.validateFields();
return true;
} catch (errorInfo) {
return false;
}
};
onMounted(async () => {
if (props.id) {
data.info = { ...defaultInfo, ...cloneDeep(props.info) };
if (data.info.fileType === StampFileTypeAttributes.UPLOAD_PICTURES) {
data.photoUrl = data.info.fileUrl;
} else {
data.signUrl = data.info.fileUrl;
}
} else {
data.info = defaultInfo;
}
data.action = getAppEnvConfig().VITE_GLOB_API_URL + uploadSrc;
data.headers.Authorization = `Bearer ${getToken()}`;
open();
});
async function open() {
data.categoryOptions = (await getDicDetailList({
itemId: StampCategory.ID,
})) as unknown as TreeItem[];
if (props.id) {
}
data.info.stampType = props.type;
data.visible = true;
}
function close() {
data.visible = false;
emits('close');
}
async function uploadSign(base64) {
data.info.fileUrl = base64;
const blob = dataURLtoBlob(base64);
const fileUrl = await uploadBlobApi(blob, t('手写签名.png'));
if (fileUrl) {
data.signUrl = fileUrl;
message.success(t('手写签章上传成功'));
} else {
message.error(t('手写签章上传失败'));
}
}
async function submit() {
let valid = await onCheck();
let res = false;
if (valid) {
if (data.info.fileType === StampFileTypeAttributes.UPLOAD_PICTURES) {
if (!data.photoUrl) {
message.error(t('照片未上传'));
return false;
} else {
data.info.fileUrl = data.photoUrl;
}
} else {
if (!data.signUrl) {
message.error(t('签名未上传'));
return false;
} else {
data.info.fileUrl = data.signUrl;
}
}
try {
if (props.id) {
res = await putStamp(props.id, props.type, data.info);
} else {
res = await postStamp(props.type, data.info);
}
} catch (error) {
return false;
}
} else {
return false;
}
if (res) {
notification.open({
type: 'success',
message: t('签章'),
description: title.value + t('成功'),
});
close();
} else {
notification.open({
type: 'error',
message: t('签章'),
description: title.value + t('失败'),
});
}
}
function photoChange(info: UploadChangeParam) {
if (info.file.status !== 'uploading') {
}
if (info.file.status === 'done') {
if (info.file && info.file.response && info.file.response.code == 0) {
message.success(t(`{name}上传成功!`, { name: info.file.name }));
console.log(info, t('上传成功'));
data.photoUrl = info.file.response.data.fileUrl;
} else {
message.error(t('上传照片失败'));
}
} else if (info.file.status === 'error') {
message.error(t(`{name}上传失败.`, { name: info.file.name }));
}
}
</script>
<style lang="less" scoped>
.box {
padding: 10px;
}
</style>

View File

@ -0,0 +1,138 @@
import { reactive } from 'vue';
import { ApproveTask, BpmnFlowForm, FlowInfo } from '/@/model/workflow/bpmnConfig';
import { notification } from 'ant-design-vue';
import { getDicDetailList } from '/@/api/system/dic';
import { TreeItem } from '/@/components/Tree';
import { ElectronicSignatureVerification, FlowCategory } from '/@/enums/workflowEnum';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
export default function () {
const approveUserData: {
visible: boolean;
list: Array<ApproveTask>;
schemaId: string;
} = reactive({
visible: false,
list: [],
schemaId: '',
});
const data: BpmnFlowForm = reactive({
xml: '',
item: { id: '', code: '', categoryName: '', name: '', remark: '' }, //工作流模板信息
formInfos: [],
relationTasks: [],
taskRecords: [],
taskApproveOpinions: [],
predecessorTasks: [],
opinions: [],
opinionsComponents: [],
hasStamp: false,
hasStampPassword: false,
submitLoading: false,
formAssignmentData: null,
});
function initProcessData(res: FlowInfo) {
data.item.id = res.schemaInfo.id;
data.item.name = res.schemaInfo.name;
data.item.code = res.schemaInfo.code;
data.item.remark = res.schemaInfo.remark;
data.taskApproveOpinions = [];
data.predecessorTasks = [];
data.opinions = [];
data.opinionsComponents = [];
data.hasStamp = false;
data.hasStampPassword = false;
data.submitLoading = false;
data.xml = '';
if (res.schemaInfo.xmlContent) {
data.xml = res.schemaInfo.xmlContent;
}
data.formInfos = [];
if (res.formInfos) {
data.formInfos = res.formInfos;
}
data.taskRecords = [];
if (res.taskRecords) {
data.taskRecords.push({
records: res.taskRecords,
schemaName: '当前流程',
});
}
if (res.otherProcessApproveRecord) {
data.taskRecords = data.taskRecords.concat(res.otherProcessApproveRecord);
}
data.taskApproveOpinions = [];
if (res.taskApproveOpinions) {
data.taskApproveOpinions = res.taskApproveOpinions;
}
if (res.formAssignmentData) {
data.formAssignmentData = res.formAssignmentData;
}
if (res.relationTasks) {
data.relationTasks = res.relationTasks;
data.relationTasks.forEach((element) => {
data.predecessorTasks.push({
schemaId: element.schemaId,
schemaName: element.schemaName,
taskId: '',
taskName: '',
processId: '',
});
});
}
if (res.opinionConfig) {
if (res.opinionConfig.enabled) {
data.hasStamp = true;
if (res.opinionConfig.signature === ElectronicSignatureVerification.PASSWORD_REQUIRED) {
data.hasStampPassword = true;
}
if (res.opinionConfig.component && res.opinionConfig.component.length > 0) {
data.opinionsComponents = res.opinionConfig.component;
getOpinionFormData();
}
}
}
if (res.formAssignmentData) {
data.formAssignmentData = res.formAssignmentData;
}
setCategoryName(res.schemaInfo.category);
}
async function setCategoryName(id: string) {
const res = (await getDicDetailList({
itemId: FlowCategory.ID,
})) as unknown as TreeItem[];
const categoryItem = res.filter((ele) => {
return ele.id === id;
});
data.item.categoryName =
categoryItem.length > 0 ? (categoryItem[0].name ? categoryItem[0].name : '') : '';
}
function notificationError(title: string, description = t('失败')) {
notification.open({
type: 'error',
message: title,
description,
});
}
function notificationSuccess(title: string) {
notification.open({
type: 'success',
message: title,
description: title + t('成功'),
});
}
function getOpinionFormData() {
data.opinions = data.taskApproveOpinions;
}
return {
data,
approveUserData,
initProcessData,
notificationError,
notificationSuccess,
};
}

View File

@ -0,0 +1,124 @@
import { reactive, h, ref } from 'vue';
import type { FormProps } from '/@/components/Form';
import { TasksModel } from '/@/api/workflow/model';
import { FormSchema } from '/@/components/Table';
import SelectUser from '/@/components/Form/src/components/SelectUser.vue';
import { useI18n } from '/@/hooks/web/useI18n';
const { t } = useI18n();
export default function () {
let searchConfig: FormSchema[] = [
{
field: 'serialNumber',
label: t('流水号'),
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: t('请输入流水号'),
},
},
{
field: 'name',
label: t('流程名称'),
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: t('请输入流程名称'),
},
},
{
field: 'taskName',
label: t('任务名称'),
component: 'Input',
colProps: { span: 8 },
componentProps: {
placeholder: t('请输入任务名称'),
},
},
{
field: 'originator',
label: t('发起人'),
component: 'Input',
colProps: { span: 8 },
render: ({ model, field }) => {
return h(SelectUser, {
placeholder: t('请选择发起人'),
value: model[field],
suffix: 'ant-design:user-outlined',
onSelectedId: (v) => {
model[field] = v;
},
});
},
},
{
field: 'searchDate',
label: t('时间范围'),
component: 'RangePicker',
colProps: { span: 8 },
componentProps: {
getPopupContainer: () => document.body,
placeholder: [t('请选择开始日期'), t('请选择结束日期')],
},
},
];
const data: {
rowKey: string;
selectedRows: TasksModel[];
} = reactive({
rowKey: 'taskId',
selectedRows: [],
});
const processId = ref('');
const taskId = ref('');
const schemaId = ref('');
const showBatchApproval = ref(false);
function selectionChange({ keys, rows }) {
data.selectedRows = rows;
if (keys?.length > 1) {
showBatchApproval.value = true;
} else {
showBatchApproval.value = false;
}
if (keys?.length > 0) {
processId.value = rows[0].processId;
taskId.value = rows[0].taskId;
schemaId.value = rows[0].schemaId;
} else {
processId.value = '';
taskId.value = '';
schemaId.value = '';
}
}
function formConfig(type?) {
if (type == 'MyProcess') {
searchConfig = searchConfig.filter((o) => {
return o.field !== 'taskName' && o.field !== 'originator';
});
}
if (type == 'Drafts') {
searchConfig = searchConfig.filter((o) => {
return o.field !== 'serialNumber' && o.field !== 'taskName';
});
}
return {
rowProps: {
gutter: 16,
},
schemas: searchConfig,
fieldMapToTime: [['searchDate', ['startTime', 'endTime'], 'YYYY-MM-DD', true]],
showResetButton: false,
} as FormProps;
}
return {
formConfig,
data,
processId,
taskId,
schemaId,
showBatchApproval,
selectionChange,
};
}