feat(bpm): 展示流程任务附件和签名
- 统一在流程 timeline、流转记录、任务管理中展示附件和签名 - 办理节点展示办理意见,审批节点展示审批意见 - 新增任务附件/签名展示组件,复用文件名解析和图片判断逻辑 - 接入流程评论列表并补齐评论 API 与字典类型 - 清理冗余 helper、临时常量和 form-create ref 类型写法
This commit is contained in:
@@ -14,6 +14,8 @@ export namespace BpmTaskApi {
|
||||
endTime: number; // 结束时间
|
||||
durationInMillis: number; // 持续时间
|
||||
reason: string; // 审批理由
|
||||
attachments?: string[]; // 审批附件
|
||||
signPicUrl?: string; // 签名图片
|
||||
ownerUser: any; // 负责人
|
||||
assigneeUser: any; // 处理人
|
||||
taskDefinitionKey: string; // 任务定义的标识
|
||||
|
||||
@@ -40,11 +40,11 @@ import {
|
||||
Textarea,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
cancelProcessInstanceByStartUser,
|
||||
getNextApprovalNodes,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
approveTask,
|
||||
copyTask,
|
||||
@@ -114,7 +114,6 @@ const APPROVAL_ATTACHMENT_FILE_TYPES = [
|
||||
'webp',
|
||||
];
|
||||
const APPROVAL_ATTACHMENT_FILE_SIZE = 5;
|
||||
const APPROVAL_ATTACHMENT_DIRECTORY = 'bpm/task-attachment';
|
||||
|
||||
/** 创建流程表达式 */
|
||||
function openSignatureModal() {
|
||||
@@ -951,7 +950,6 @@ defineExpose({ loadTodoTask });
|
||||
<FileUpload
|
||||
v-model:value="approveReasonForm.attachments"
|
||||
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
|
||||
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
|
||||
:max-number="10"
|
||||
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
|
||||
:multiple="true"
|
||||
@@ -1022,7 +1020,6 @@ defineExpose({ loadTodoTask });
|
||||
<FileUpload
|
||||
v-model:value="rejectReasonForm.attachments"
|
||||
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
|
||||
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
|
||||
:max-number="10"
|
||||
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
|
||||
:multiple="true"
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { getFileNameFromUrl, isImage } from '@vben/utils';
|
||||
|
||||
defineOptions({ name: 'BpmTaskEvidence' });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
attachments?: null | string | string[];
|
||||
compact?: boolean;
|
||||
emptyText?: string;
|
||||
reason?: string;
|
||||
reasonLabel?: string;
|
||||
signPicUrl?: null | string;
|
||||
}>(),
|
||||
{
|
||||
attachments: () => [],
|
||||
emptyText: '-',
|
||||
reason: '',
|
||||
reasonLabel: '审批意见',
|
||||
signPicUrl: '',
|
||||
},
|
||||
);
|
||||
|
||||
const reasonText = computed(() => props.reason?.trim() || '');
|
||||
const attachmentList = computed(() => normalizeAttachments(props.attachments)); // 附件列表
|
||||
const signPicUrlValue = computed(() => props.signPicUrl?.trim() || ''); // 签名图片地址
|
||||
const hasEvidence = computed(
|
||||
() =>
|
||||
!!reasonText.value ||
|
||||
attachmentList.value.length > 0 ||
|
||||
!!signPicUrlValue.value,
|
||||
); // 是否存在审批凭证
|
||||
|
||||
/** 标准化附件列表 */
|
||||
function normalizeAttachments(attachments?: null | string | string[]) {
|
||||
if (!attachments) {
|
||||
return [];
|
||||
}
|
||||
const list = Array.isArray(attachments)
|
||||
? attachments
|
||||
: String(attachments).split(',');
|
||||
return list.map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/** 判断是否图片附件 */
|
||||
function isImageAttachment(url: string) {
|
||||
return isImage(getFileNameFromUrl(url));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="hasEvidence"
|
||||
:class="
|
||||
compact
|
||||
? 'flex flex-col items-center gap-1 text-xs text-gray-500'
|
||||
: 'mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500'
|
||||
"
|
||||
>
|
||||
<div v-if="reasonText" class="w-full">
|
||||
{{ reasonLabel }}:{{ reasonText }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="attachmentList.length > 0"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
reasonText && !compact,
|
||||
}"
|
||||
>
|
||||
<div v-if="!compact" class="mb-1 text-xs font-semibold text-gray-400">
|
||||
附件列表:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5" :class="{ 'justify-center': compact }">
|
||||
<a
|
||||
v-for="attachment in attachmentList"
|
||||
:key="attachment"
|
||||
:href="attachment"
|
||||
target="_blank"
|
||||
class="inline-flex max-w-[220px] items-center gap-1 rounded border border-solid border-gray-200 bg-white px-2 py-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
:title="getFileNameFromUrl(attachment)"
|
||||
>
|
||||
<img
|
||||
v-if="isImageAttachment(attachment)"
|
||||
:src="attachment"
|
||||
class="size-5 rounded object-cover"
|
||||
/>
|
||||
<IconifyIcon v-else icon="lucide:file-text" class="text-gray-400" />
|
||||
<span class="truncate">{{ getFileNameFromUrl(attachment) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="signPicUrlValue"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
(reasonText || attachmentList.length) && !compact,
|
||||
}"
|
||||
>
|
||||
<div class="mb-1 text-xs font-semibold text-gray-400">签名:</div>
|
||||
<a :href="signPicUrlValue" target="_blank" title="查看签名">
|
||||
<img
|
||||
:src="signPicUrlValue"
|
||||
class="max-h-[60px] max-w-[180px] rounded border border-solid border-gray-200 bg-white object-contain"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else-if="compact" class="text-gray-400">{{ emptyText }}</span>
|
||||
</template>
|
||||
@@ -16,6 +16,8 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskListByProcessInstanceId } from '#/api/bpm/task';
|
||||
import { setConfAndFields2 } from '#/components/form-create';
|
||||
|
||||
import TaskEvidence from './task-evidence.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'BpmProcessInstanceTaskList',
|
||||
});
|
||||
@@ -79,6 +81,14 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'evidence',
|
||||
title: '附件/签名',
|
||||
slots: {
|
||||
default: 'slot-evidence',
|
||||
},
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
@@ -186,6 +196,13 @@ defineExpose({
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #slot-evidence="{ row }">
|
||||
<TaskEvidence
|
||||
compact
|
||||
:attachments="row.attachments"
|
||||
:sign-pic-url="row.signPicUrl"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
<Modal class="w-3/5">
|
||||
|
||||
@@ -14,10 +14,12 @@ import {
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime, isEmpty } from '@vben/utils';
|
||||
|
||||
import { Avatar, Button, Image, Timeline, Tooltip } from 'ant-design-vue';
|
||||
import { Avatar, Button, Timeline, Tooltip } from 'ant-design-vue';
|
||||
|
||||
import { UserSelectModal } from '#/views/system/user/components';
|
||||
|
||||
import TaskEvidence from './task-evidence.vue';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceTimeline' });
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -196,7 +198,7 @@ function shouldShowCustomUserSelect(
|
||||
);
|
||||
}
|
||||
|
||||
/** 判断是否需要显示审批意见和附件 */
|
||||
/** 判断是否需要显示审批凭证 */
|
||||
function shouldShowReasonAndAttachment(
|
||||
task: any,
|
||||
nodeType: BpmNodeTypeEnum,
|
||||
@@ -207,26 +209,25 @@ function shouldShowReasonAndAttachment(
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
Boolean(task.reason || task.attachments?.length > 0) &&
|
||||
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||
nodeType,
|
||||
)
|
||||
hasTaskEvidence(task) &&
|
||||
[
|
||||
BpmNodeTypeEnum.START_USER_NODE,
|
||||
BpmNodeTypeEnum.TRANSACTOR_NODE,
|
||||
BpmNodeTypeEnum.USER_TASK_NODE,
|
||||
].includes(nodeType)
|
||||
);
|
||||
}
|
||||
|
||||
function getAttachmentName(url: string) {
|
||||
const cleanUrl = url.split(/[?#]/)[0] || '';
|
||||
const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1);
|
||||
try {
|
||||
return decodeURIComponent(fileName);
|
||||
} catch {
|
||||
return fileName;
|
||||
}
|
||||
/** 判断是否存在审批凭证 */
|
||||
function hasTaskEvidence(task: any) {
|
||||
return Boolean(
|
||||
task?.reason || task?.attachments?.length > 0 || task?.signPicUrl,
|
||||
);
|
||||
}
|
||||
|
||||
function isImageAttachment(url: string) {
|
||||
const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase();
|
||||
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
|
||||
/** 获取意见文案 */
|
||||
function getReasonLabel(nodeType: BpmNodeTypeEnum) {
|
||||
return nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理意见' : '审批意见';
|
||||
}
|
||||
|
||||
/** 用户选择弹窗关闭 */
|
||||
@@ -428,76 +429,16 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批意见,附件和签名 -->
|
||||
<div
|
||||
<!-- 审批凭证 -->
|
||||
<TaskEvidence
|
||||
v-if="
|
||||
shouldShowReasonAndAttachment(task, activity.nodeType, index)
|
||||
"
|
||||
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||
>
|
||||
<div v-if="task.reason">审批意见:{{ task.reason }}</div>
|
||||
<div
|
||||
v-if="(task.attachments?.length || 0) > 0"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
task.reason,
|
||||
}"
|
||||
>
|
||||
<div class="mb-1 text-xs font-semibold text-gray-400">
|
||||
附件列表:
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<template
|
||||
v-for="(attachment, attachmentIndex) in task.attachments"
|
||||
:key="attachmentIndex"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
isImageAttachment(attachment)
|
||||
? 'lucide:image'
|
||||
: 'lucide:file-text'
|
||||
"
|
||||
class="text-gray-400"
|
||||
/>
|
||||
<Image
|
||||
v-if="isImageAttachment(attachment)"
|
||||
:width="32"
|
||||
:height="32"
|
||||
class="rounded border border-solid border-gray-200 object-cover"
|
||||
:src="attachment"
|
||||
:preview="{ src: attachment }"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="attachment"
|
||||
target="_blank"
|
||||
class="max-w-[240px] truncate text-blue-500 hover:text-blue-600 hover:underline"
|
||||
:title="getAttachmentName(attachment)"
|
||||
>
|
||||
{{ getAttachmentName(attachment) }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
task.signPicUrl &&
|
||||
activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE
|
||||
"
|
||||
class="mt-1 flex w-full items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||
>
|
||||
签名:
|
||||
<Image
|
||||
class="ml-2"
|
||||
:width="180"
|
||||
:height="60"
|
||||
:src="task.signPicUrl"
|
||||
:preview="{ src: task.signPicUrl }"
|
||||
/>
|
||||
</div>
|
||||
:attachments="task.attachments"
|
||||
:reason="task.reason"
|
||||
:reason-label="getReasonLabel(activity.nodeType)"
|
||||
:sign-pic-url="task.signPicUrl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
|
||||
|
||||
@@ -78,6 +78,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
title: '审批建议',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'evidence',
|
||||
title: '附件/签名',
|
||||
minWidth: 220,
|
||||
slots: { default: 'evidence' },
|
||||
},
|
||||
{
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DocAlert, Page } from '@vben/common-ui';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskManagerPage } from '#/api/bpm/task';
|
||||
import { router } from '#/router';
|
||||
import TaskEvidence from '#/views/bpm/processInstance/detail/modules/task-evidence.vue';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
@@ -60,6 +61,13 @@ const [Grid] = useVbenVxeGrid({
|
||||
</template>
|
||||
|
||||
<Grid table-title="流程任务">
|
||||
<template #evidence="{ row }">
|
||||
<TaskEvidence
|
||||
compact
|
||||
:attachments="row.attachments"
|
||||
:sign-pic-url="row.signPicUrl"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
||||
@@ -14,6 +14,8 @@ export namespace BpmTaskApi {
|
||||
endTime: number; // 结束时间
|
||||
durationInMillis: number; // 持续时间
|
||||
reason: string; // 审批理由
|
||||
attachments?: string[]; // 审批附件
|
||||
signPicUrl?: string; // 签名图片
|
||||
ownerUser: any; // 负责人
|
||||
assigneeUser: any; // 处理人
|
||||
taskDefinitionKey: string; // 任务定义的标识
|
||||
|
||||
@@ -38,11 +38,11 @@ import {
|
||||
TextArea,
|
||||
} from 'antdv-next';
|
||||
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
cancelProcessInstanceByStartUser,
|
||||
getNextApprovalNodes,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
approveTask,
|
||||
copyTask,
|
||||
@@ -113,7 +113,6 @@ const APPROVAL_ATTACHMENT_FILE_TYPES = [
|
||||
'webp',
|
||||
];
|
||||
const APPROVAL_ATTACHMENT_FILE_SIZE = 5;
|
||||
const APPROVAL_ATTACHMENT_DIRECTORY = 'bpm/task-attachment';
|
||||
|
||||
/** 创建流程表达式 */
|
||||
function openSignatureModal() {
|
||||
@@ -954,7 +953,6 @@ defineExpose({ loadTodoTask });
|
||||
<FileUpload
|
||||
v-model:value="approveReasonForm.attachments"
|
||||
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
|
||||
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
|
||||
:max-number="10"
|
||||
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
|
||||
:multiple="true"
|
||||
@@ -1025,7 +1023,6 @@ defineExpose({ loadTodoTask });
|
||||
<FileUpload
|
||||
v-model:value="rejectReasonForm.attachments"
|
||||
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
|
||||
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
|
||||
:max-number="10"
|
||||
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
|
||||
:multiple="true"
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { getFileNameFromUrl, isImage } from '@vben/utils';
|
||||
|
||||
defineOptions({ name: 'BpmTaskEvidence' });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
attachments?: null | string | string[];
|
||||
compact?: boolean;
|
||||
emptyText?: string;
|
||||
reason?: string;
|
||||
reasonLabel?: string;
|
||||
signPicUrl?: null | string;
|
||||
}>(),
|
||||
{
|
||||
attachments: () => [],
|
||||
emptyText: '-',
|
||||
reason: '',
|
||||
reasonLabel: '审批意见',
|
||||
signPicUrl: '',
|
||||
},
|
||||
);
|
||||
|
||||
const reasonText = computed(() => props.reason?.trim() || '');
|
||||
const attachmentList = computed(() => normalizeAttachments(props.attachments)); // 附件列表
|
||||
const signPicUrlValue = computed(() => props.signPicUrl?.trim() || ''); // 签名图片地址
|
||||
const hasEvidence = computed(
|
||||
() =>
|
||||
!!reasonText.value ||
|
||||
attachmentList.value.length > 0 ||
|
||||
!!signPicUrlValue.value,
|
||||
); // 是否存在审批凭证
|
||||
|
||||
/** 标准化附件列表 */
|
||||
function normalizeAttachments(attachments?: null | string | string[]) {
|
||||
if (!attachments) {
|
||||
return [];
|
||||
}
|
||||
const list = Array.isArray(attachments)
|
||||
? attachments
|
||||
: String(attachments).split(',');
|
||||
return list.map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/** 判断是否图片附件 */
|
||||
function isImageAttachment(url: string) {
|
||||
return isImage(getFileNameFromUrl(url));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="hasEvidence"
|
||||
:class="
|
||||
compact
|
||||
? 'flex flex-col items-center gap-1 text-xs text-gray-500'
|
||||
: 'mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500'
|
||||
"
|
||||
>
|
||||
<div v-if="reasonText" class="w-full">
|
||||
{{ reasonLabel }}:{{ reasonText }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="attachmentList.length > 0"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
reasonText && !compact,
|
||||
}"
|
||||
>
|
||||
<div v-if="!compact" class="mb-1 text-xs font-semibold text-gray-400">
|
||||
附件列表:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5" :class="{ 'justify-center': compact }">
|
||||
<a
|
||||
v-for="attachment in attachmentList"
|
||||
:key="attachment"
|
||||
:href="attachment"
|
||||
target="_blank"
|
||||
class="inline-flex max-w-[220px] items-center gap-1 rounded border border-solid border-gray-200 bg-white px-2 py-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
:title="getFileNameFromUrl(attachment)"
|
||||
>
|
||||
<img
|
||||
v-if="isImageAttachment(attachment)"
|
||||
:src="attachment"
|
||||
class="size-5 rounded object-cover"
|
||||
/>
|
||||
<IconifyIcon v-else icon="lucide:file-text" class="text-gray-400" />
|
||||
<span class="truncate">{{ getFileNameFromUrl(attachment) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="signPicUrlValue"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
(reasonText || attachmentList.length) && !compact,
|
||||
}"
|
||||
>
|
||||
<div class="mb-1 text-xs font-semibold text-gray-400">签名:</div>
|
||||
<a :href="signPicUrlValue" target="_blank" title="查看签名">
|
||||
<img
|
||||
:src="signPicUrlValue"
|
||||
class="max-h-[60px] max-w-[180px] rounded border border-solid border-gray-200 bg-white object-contain"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else-if="compact" class="text-gray-400">{{ emptyText }}</span>
|
||||
</template>
|
||||
@@ -16,6 +16,8 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskListByProcessInstanceId } from '#/api/bpm/task';
|
||||
import { setConfAndFields2 } from '#/components/form-create';
|
||||
|
||||
import TaskEvidence from './task-evidence.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'BpmProcessInstanceTaskList',
|
||||
});
|
||||
@@ -79,6 +81,14 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'evidence',
|
||||
title: '附件/签名',
|
||||
slots: {
|
||||
default: 'slot-evidence',
|
||||
},
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
@@ -186,6 +196,13 @@ defineExpose({
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
<template #slot-evidence="{ row }">
|
||||
<TaskEvidence
|
||||
compact
|
||||
:attachments="row.attachments"
|
||||
:sign-pic-url="row.signPicUrl"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
<Modal class="w-3/5">
|
||||
|
||||
@@ -17,7 +17,6 @@ import { formatDateTime, isEmpty } from '@vben/utils';
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
Image,
|
||||
Timeline,
|
||||
TimelineItem,
|
||||
Tooltip,
|
||||
@@ -25,6 +24,8 @@ import {
|
||||
|
||||
import { UserSelectModal } from '#/views/system/user/components';
|
||||
|
||||
import TaskEvidence from './task-evidence.vue';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceTimeline' });
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -203,7 +204,7 @@ function shouldShowCustomUserSelect(
|
||||
);
|
||||
}
|
||||
|
||||
/** 判断是否需要显示审批意见和附件 */
|
||||
/** 判断是否需要显示审批凭证 */
|
||||
function shouldShowReasonAndAttachment(
|
||||
task: any,
|
||||
nodeType: BpmNodeTypeEnum,
|
||||
@@ -214,26 +215,25 @@ function shouldShowReasonAndAttachment(
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
Boolean(task.reason || task.attachments?.length > 0) &&
|
||||
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||
nodeType,
|
||||
)
|
||||
hasTaskEvidence(task) &&
|
||||
[
|
||||
BpmNodeTypeEnum.START_USER_NODE,
|
||||
BpmNodeTypeEnum.TRANSACTOR_NODE,
|
||||
BpmNodeTypeEnum.USER_TASK_NODE,
|
||||
].includes(nodeType)
|
||||
);
|
||||
}
|
||||
|
||||
function getAttachmentName(url: string) {
|
||||
const cleanUrl = url.split(/[?#]/)[0] || '';
|
||||
const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1);
|
||||
try {
|
||||
return decodeURIComponent(fileName);
|
||||
} catch {
|
||||
return fileName;
|
||||
}
|
||||
/** 判断是否存在审批凭证 */
|
||||
function hasTaskEvidence(task: any) {
|
||||
return Boolean(
|
||||
task?.reason || task?.attachments?.length > 0 || task?.signPicUrl,
|
||||
);
|
||||
}
|
||||
|
||||
function isImageAttachment(url: string) {
|
||||
const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase();
|
||||
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
|
||||
/** 获取意见文案 */
|
||||
function getReasonLabel(nodeType: BpmNodeTypeEnum) {
|
||||
return nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理意见' : '审批意见';
|
||||
}
|
||||
|
||||
/** 用户选择弹窗关闭 */
|
||||
@@ -435,76 +435,16 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批意见、附件和签名 -->
|
||||
<div
|
||||
<!-- 审批凭证 -->
|
||||
<TaskEvidence
|
||||
v-if="
|
||||
shouldShowReasonAndAttachment(task, activity.nodeType, index)
|
||||
"
|
||||
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||
>
|
||||
<div v-if="task.reason">审批意见:{{ task.reason }}</div>
|
||||
<div
|
||||
v-if="(task.attachments?.length || 0) > 0"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
task.reason,
|
||||
}"
|
||||
>
|
||||
<div class="mb-1 text-xs font-semibold text-gray-400">
|
||||
附件列表:
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<template
|
||||
v-for="(attachment, attachmentIndex) in task.attachments"
|
||||
:key="attachmentIndex"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
isImageAttachment(attachment)
|
||||
? 'lucide:image'
|
||||
: 'lucide:file-text'
|
||||
"
|
||||
class="text-gray-400"
|
||||
/>
|
||||
<Image
|
||||
v-if="isImageAttachment(attachment)"
|
||||
:width="32"
|
||||
:height="32"
|
||||
class="rounded border border-solid border-gray-200 object-cover"
|
||||
:src="attachment"
|
||||
:preview="{ src: attachment }"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="attachment"
|
||||
target="_blank"
|
||||
class="max-w-[240px] truncate text-blue-500 hover:text-blue-600 hover:underline"
|
||||
:title="getAttachmentName(attachment)"
|
||||
>
|
||||
{{ getAttachmentName(attachment) }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
task.signPicUrl &&
|
||||
activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE
|
||||
"
|
||||
class="mt-1 flex w-full items-center rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||
>
|
||||
签名:
|
||||
<Image
|
||||
class="ml-2"
|
||||
:width="180"
|
||||
:height="60"
|
||||
:src="task.signPicUrl"
|
||||
:preview="{ src: task.signPicUrl }"
|
||||
/>
|
||||
</div>
|
||||
:attachments="task.attachments"
|
||||
:reason="task.reason"
|
||||
:reason-label="getReasonLabel(activity.nodeType)"
|
||||
:sign-pic-url="task.signPicUrl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
|
||||
|
||||
@@ -78,6 +78,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
title: '审批建议',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'evidence',
|
||||
title: '附件/签名',
|
||||
minWidth: 220,
|
||||
slots: { default: 'evidence' },
|
||||
},
|
||||
{
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DocAlert, Page } from '@vben/common-ui';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskManagerPage } from '#/api/bpm/task';
|
||||
import { router } from '#/router';
|
||||
import TaskEvidence from '#/views/bpm/processInstance/detail/modules/task-evidence.vue';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
@@ -60,6 +61,13 @@ const [Grid] = useVbenVxeGrid({
|
||||
</template>
|
||||
|
||||
<Grid table-title="流程任务">
|
||||
<template #evidence="{ row }">
|
||||
<TaskEvidence
|
||||
compact
|
||||
:attachments="row.attachments"
|
||||
:sign-pic-url="row.signPicUrl"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
||||
@@ -14,6 +14,8 @@ export namespace BpmTaskApi {
|
||||
endTime: number; // 结束时间
|
||||
durationInMillis: number; // 持续时间
|
||||
reason: string; // 审批理由
|
||||
attachments?: string[]; // 审批附件
|
||||
signPicUrl?: string; // 签名图片
|
||||
ownerUser: any; // 负责人
|
||||
assigneeUser: any; // 处理人
|
||||
taskDefinitionKey: string; // 任务定义的标识
|
||||
|
||||
@@ -40,11 +40,11 @@ import {
|
||||
ElSpace,
|
||||
} from 'element-plus';
|
||||
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
cancelProcessInstanceByStartUser,
|
||||
getNextApprovalNodes,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
approveTask,
|
||||
copyTask,
|
||||
@@ -114,7 +114,6 @@ const APPROVAL_ATTACHMENT_FILE_TYPES = [
|
||||
'webp',
|
||||
];
|
||||
const APPROVAL_ATTACHMENT_FILE_SIZE = 5;
|
||||
const APPROVAL_ATTACHMENT_DIRECTORY = 'bpm/task-attachment';
|
||||
|
||||
/** 创建流程表达式 */
|
||||
function openSignatureModal() {
|
||||
@@ -958,7 +957,6 @@ defineExpose({ loadTodoTask });
|
||||
<FileUpload
|
||||
v-model:value="approveReasonForm.attachments"
|
||||
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
|
||||
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
|
||||
:max-number="10"
|
||||
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
|
||||
:multiple="true"
|
||||
@@ -1029,7 +1027,6 @@ defineExpose({ loadTodoTask });
|
||||
<FileUpload
|
||||
v-model:value="rejectReasonForm.attachments"
|
||||
:accept="APPROVAL_ATTACHMENT_FILE_TYPES"
|
||||
:directory="APPROVAL_ATTACHMENT_DIRECTORY"
|
||||
:max-number="10"
|
||||
:max-size="APPROVAL_ATTACHMENT_FILE_SIZE"
|
||||
:multiple="true"
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { getFileNameFromUrl, isImage } from '@vben/utils';
|
||||
|
||||
defineOptions({ name: 'BpmTaskEvidence' });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
attachments?: null | string | string[];
|
||||
compact?: boolean;
|
||||
emptyText?: string;
|
||||
reason?: string;
|
||||
reasonLabel?: string;
|
||||
signPicUrl?: null | string;
|
||||
}>(),
|
||||
{
|
||||
attachments: () => [],
|
||||
emptyText: '-',
|
||||
reason: '',
|
||||
reasonLabel: '审批意见',
|
||||
signPicUrl: '',
|
||||
},
|
||||
);
|
||||
|
||||
const reasonText = computed(() => props.reason?.trim() || '');
|
||||
const attachmentList = computed(() => normalizeAttachments(props.attachments)); // 附件列表
|
||||
const signPicUrlValue = computed(() => props.signPicUrl?.trim() || ''); // 签名图片地址
|
||||
const hasEvidence = computed(
|
||||
() =>
|
||||
!!reasonText.value ||
|
||||
attachmentList.value.length > 0 ||
|
||||
!!signPicUrlValue.value,
|
||||
); // 是否存在审批凭证
|
||||
|
||||
/** 标准化附件列表 */
|
||||
function normalizeAttachments(attachments?: null | string | string[]) {
|
||||
if (!attachments) {
|
||||
return [];
|
||||
}
|
||||
const list = Array.isArray(attachments)
|
||||
? attachments
|
||||
: String(attachments).split(',');
|
||||
return list.map((item) => item.trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
/** 判断是否图片附件 */
|
||||
function isImageAttachment(url: string) {
|
||||
return isImage(getFileNameFromUrl(url));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-if="hasEvidence"
|
||||
:class="
|
||||
compact
|
||||
? 'flex flex-col items-center gap-1 text-xs text-gray-500'
|
||||
: 'mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500'
|
||||
"
|
||||
>
|
||||
<div v-if="reasonText" class="w-full">
|
||||
{{ reasonLabel }}:{{ reasonText }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="attachmentList.length > 0"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
reasonText && !compact,
|
||||
}"
|
||||
>
|
||||
<div v-if="!compact" class="mb-1 text-xs font-semibold text-gray-400">
|
||||
附件列表:
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1.5" :class="{ 'justify-center': compact }">
|
||||
<a
|
||||
v-for="attachment in attachmentList"
|
||||
:key="attachment"
|
||||
:href="attachment"
|
||||
target="_blank"
|
||||
class="inline-flex max-w-[220px] items-center gap-1 rounded border border-solid border-gray-200 bg-white px-2 py-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
:title="getFileNameFromUrl(attachment)"
|
||||
>
|
||||
<img
|
||||
v-if="isImageAttachment(attachment)"
|
||||
:src="attachment"
|
||||
class="size-5 rounded object-cover"
|
||||
/>
|
||||
<IconifyIcon v-else icon="lucide:file-text" class="text-gray-400" />
|
||||
<span class="truncate">{{ getFileNameFromUrl(attachment) }}</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="signPicUrlValue"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
(reasonText || attachmentList.length) && !compact,
|
||||
}"
|
||||
>
|
||||
<div class="mb-1 text-xs font-semibold text-gray-400">签名:</div>
|
||||
<a :href="signPicUrlValue" target="_blank" title="查看签名">
|
||||
<img
|
||||
:src="signPicUrlValue"
|
||||
class="max-h-[60px] max-w-[180px] rounded border border-solid border-gray-200 bg-white object-contain"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<span v-else-if="compact" class="text-gray-400">{{ emptyText }}</span>
|
||||
</template>
|
||||
@@ -15,6 +15,8 @@ import { useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskListByProcessInstanceId } from '#/api/bpm/task';
|
||||
import { setConfAndFields2 } from '#/components/form-create';
|
||||
|
||||
import TaskEvidence from './task-evidence.vue';
|
||||
|
||||
defineOptions({
|
||||
name: 'BpmProcessInstanceTaskList',
|
||||
});
|
||||
@@ -78,6 +80,14 @@ function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
},
|
||||
minWidth: 200,
|
||||
},
|
||||
{
|
||||
field: 'evidence',
|
||||
title: '附件/签名',
|
||||
slots: {
|
||||
default: 'slot-evidence',
|
||||
},
|
||||
minWidth: 220,
|
||||
},
|
||||
{
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
@@ -187,6 +197,13 @@ defineExpose({
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
<template #slot-evidence="{ row }">
|
||||
<TaskEvidence
|
||||
compact
|
||||
:attachments="row.attachments"
|
||||
:sign-pic-url="row.signPicUrl"
|
||||
/>
|
||||
</template>
|
||||
</Grid>
|
||||
</div>
|
||||
<Modal class="w-3/5">
|
||||
|
||||
@@ -17,7 +17,6 @@ import { formatDateTime, isEmpty } from '@vben/utils';
|
||||
import {
|
||||
ElAvatar,
|
||||
ElButton,
|
||||
ElImage,
|
||||
ElTimeline,
|
||||
ElTimelineItem,
|
||||
ElTooltip,
|
||||
@@ -25,6 +24,8 @@ import {
|
||||
|
||||
import { UserSelectModal } from '#/views/system/user/components';
|
||||
|
||||
import TaskEvidence from './task-evidence.vue';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceTimeline' });
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -203,7 +204,7 @@ function shouldShowCustomUserSelect(
|
||||
);
|
||||
}
|
||||
|
||||
/** 判断是否需要显示审批意见和附件 */
|
||||
/** 判断是否需要显示审批凭证 */
|
||||
function shouldShowReasonAndAttachment(
|
||||
task: any,
|
||||
nodeType: BpmNodeTypeEnum,
|
||||
@@ -214,26 +215,25 @@ function shouldShowReasonAndAttachment(
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
Boolean(task.reason || task.attachments?.length > 0) &&
|
||||
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
|
||||
nodeType,
|
||||
)
|
||||
hasTaskEvidence(task) &&
|
||||
[
|
||||
BpmNodeTypeEnum.START_USER_NODE,
|
||||
BpmNodeTypeEnum.TRANSACTOR_NODE,
|
||||
BpmNodeTypeEnum.USER_TASK_NODE,
|
||||
].includes(nodeType)
|
||||
);
|
||||
}
|
||||
|
||||
function getAttachmentName(url: string) {
|
||||
const cleanUrl = url.split(/[?#]/)[0] || '';
|
||||
const fileName = cleanUrl.slice(cleanUrl.lastIndexOf('/') + 1);
|
||||
try {
|
||||
return decodeURIComponent(fileName);
|
||||
} catch {
|
||||
return fileName;
|
||||
}
|
||||
/** 判断是否存在审批凭证 */
|
||||
function hasTaskEvidence(task: any) {
|
||||
return Boolean(
|
||||
task?.reason || task?.attachments?.length > 0 || task?.signPicUrl,
|
||||
);
|
||||
}
|
||||
|
||||
function isImageAttachment(url: string) {
|
||||
const ext = url.split(/[?#]/)[0]?.split('.').pop()?.toLowerCase();
|
||||
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
|
||||
/** 获取意见文案 */
|
||||
function getReasonLabel(nodeType: BpmNodeTypeEnum) {
|
||||
return nodeType === BpmNodeTypeEnum.TRANSACTOR_NODE ? '办理意见' : '审批意见';
|
||||
}
|
||||
|
||||
/** 用户选择弹窗关闭 */
|
||||
@@ -433,74 +433,16 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 审批意见,附件和签名 -->
|
||||
<div
|
||||
<!-- 审批凭证 -->
|
||||
<TaskEvidence
|
||||
v-if="
|
||||
shouldShowReasonAndAttachment(task, activity.nodeType, index)
|
||||
"
|
||||
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||
>
|
||||
<div v-if="task.reason">审批意见:{{ task.reason }}</div>
|
||||
<div
|
||||
v-if="(task.attachments?.length || 0) > 0"
|
||||
:class="{
|
||||
'mt-2 border-t border-dashed border-gray-300 pt-2':
|
||||
task.reason,
|
||||
}"
|
||||
>
|
||||
<div class="mb-1 text-xs font-semibold text-gray-400">
|
||||
附件列表:
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<template
|
||||
v-for="(attachment, attachmentIndex) in task.attachments"
|
||||
:key="attachmentIndex"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
isImageAttachment(attachment)
|
||||
? 'lucide:image'
|
||||
: 'lucide:file-text'
|
||||
"
|
||||
class="text-gray-400"
|
||||
/>
|
||||
<ElImage
|
||||
v-if="isImageAttachment(attachment)"
|
||||
style="width: 32px; height: 32px"
|
||||
class="rounded border border-solid border-gray-200 object-cover"
|
||||
:src="attachment"
|
||||
:preview-src-list="[attachment]"
|
||||
fit="cover"
|
||||
/>
|
||||
<a
|
||||
v-else
|
||||
:href="attachment"
|
||||
target="_blank"
|
||||
class="max-w-[240px] truncate text-blue-500 hover:text-blue-600 hover:underline"
|
||||
:title="getAttachmentName(attachment)"
|
||||
>
|
||||
{{ getAttachmentName(attachment) }}
|
||||
</a>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="
|
||||
task.signPicUrl &&
|
||||
activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE
|
||||
"
|
||||
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
|
||||
>
|
||||
签名:
|
||||
<ElImage
|
||||
class="ml-1 h-10 w-24"
|
||||
:src="task.signPicUrl"
|
||||
:preview-src-list="[task.signPicUrl]"
|
||||
/>
|
||||
</div>
|
||||
:attachments="task.attachments"
|
||||
:reason="task.reason"
|
||||
:reason-label="getReasonLabel(activity.nodeType)"
|
||||
:sign-pic-url="task.signPicUrl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
|
||||
|
||||
@@ -78,6 +78,12 @@ export function useGridColumns(): VxeTableGridOptions['columns'] {
|
||||
title: '审批建议',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'evidence',
|
||||
title: '附件/签名',
|
||||
minWidth: 220,
|
||||
slots: { default: 'evidence' },
|
||||
},
|
||||
{
|
||||
field: 'durationInMillis',
|
||||
title: '耗时',
|
||||
|
||||
@@ -7,6 +7,7 @@ import { DocAlert, Page } from '@vben/common-ui';
|
||||
import { ACTION_ICON, TableAction, useVbenVxeGrid } from '#/adapter/vxe-table';
|
||||
import { getTaskManagerPage } from '#/api/bpm/task';
|
||||
import { router } from '#/router';
|
||||
import TaskEvidence from '#/views/bpm/processInstance/detail/modules/task-evidence.vue';
|
||||
|
||||
import { useGridColumns, useGridFormSchema } from './data';
|
||||
|
||||
@@ -60,6 +61,13 @@ const [Grid] = useVbenVxeGrid({
|
||||
</template>
|
||||
|
||||
<Grid table-title="流程任务">
|
||||
<template #evidence="{ row }">
|
||||
<TaskEvidence
|
||||
compact
|
||||
:attachments="row.attachments"
|
||||
:sign-pic-url="row.signPicUrl"
|
||||
/>
|
||||
</template>
|
||||
<template #actions="{ row }">
|
||||
<TableAction
|
||||
:actions="[
|
||||
|
||||
Reference in New Issue
Block a user