!360 Merge remote-tracking branch 'yudao/master'

Merge pull request !360 from Jason/master
pull/361/head
芋道源码 2026-06-13 17:50:03 +00:00 committed by Gitee
commit 97ca9cfa45
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
9 changed files with 289 additions and 18 deletions

View File

@ -83,6 +83,7 @@ export namespace BpmProcessInstanceApi {
reason: string;
signPicUrl: string;
status: number;
attachments?: string[];
}
/** 抄送流程实例 */

View File

@ -34,6 +34,7 @@ const props = withDefaults(defineProps<FileUploadProps>(), {
resultField: '',
returnText: false,
showDescription: false,
showDownloadIcon: true,
});
const emit = defineEmits([
'change',
@ -295,7 +296,7 @@ function getValue() {
:show-upload-list="{
showPreviewIcon: true,
showRemoveIcon: true,
showDownloadIcon: true,
showDownloadIcon,
}"
@remove="handleRemove"
@preview="handlePreview"
@ -361,3 +362,10 @@ function getValue() {
color: #999;
}
</style>
<style>
/* 文件上传列表显示手型光标样式失效。不知道为啥. 先这里加上 */
.ant-upload-list-text .ant-upload-list-item {
cursor: pointer;
}
</style>

View File

@ -29,5 +29,6 @@ export interface FileUploadProps {
resultField?: string; // support xxx.xxx.xx
returnText?: boolean; // 是否返回文件文本内容
showDescription?: boolean; // 是否显示下面的描述
showDownloadIcon?: boolean; // 是否显示下载按钮
value?: string | string[];
}

View File

@ -56,6 +56,7 @@ import {
transferTask,
} from '#/api/bpm/task';
import { setConfAndFields2 } from '#/components/form-create';
import { FileUpload } from '#/components/upload';
import { $t } from '#/locales';
import Signature from './signature.vue';
@ -120,6 +121,7 @@ const approveReasonForm: any = reactive({
reason: '',
signPicUrl: '',
nextAssignees: {},
attachments: [],
});
const approveReasonRule: Record<string, any> = computed(() => {
return {
@ -142,6 +144,7 @@ const approveReasonRule: Record<string, any> = computed(() => {
const rejectFormRef = ref<FormInstance>();
const rejectReasonForm = reactive({
reason: '',
attachments: [],
}); //
const rejectReasonRule: any = computed(() => {
return {
@ -290,6 +293,14 @@ function closePopover(type: string, formRef: any | FormInstance) {
if (formRef) {
formRef.resetFields();
}
if (type === 'approve') {
approveReasonForm.reason = '';
approveReasonForm.attachments = [];
approveReasonForm.signPicUrl = '';
} else if (type === 'reject') {
rejectReasonForm.reason = '';
rejectReasonForm.attachments = [];
}
if (popOverVisible.value[type]) popOverVisible.value[type] = false;
nextAssigneesActivityNode.value = [];
// Timeline
@ -401,6 +412,7 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
const data = {
id: runningTask.value.id,
reason: approveReasonForm.reason,
attachments: approveReasonForm.attachments,
variables, // ,
nextAssignees: approveReasonForm.nextAssignees, //
} as any;
@ -414,6 +426,9 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
await formCreateApi.validate();
}
await approveTask(data);
approveReasonForm.reason = '';
approveReasonForm.attachments = [];
approveReasonForm.signPicUrl = '';
popOverVisible.value.approve = false;
nextAssigneesActivityNode.value = [];
// Timeline
@ -426,8 +441,11 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
const data = {
id: runningTask.value.id,
reason: rejectReasonForm.reason,
attachments: rejectReasonForm.attachments,
};
await rejectTask(data);
rejectReasonForm.reason = '';
rejectReasonForm.attachments = [];
popOverVisible.value.reject = false;
message.success('审批不通过成功');
}
@ -748,6 +766,34 @@ function handleSignFinish(url: string) {
approveFormRef.value?.validateFields(['signPicUrl']);
}
/** 附件图片预览 */
const imagePreviewVisible = ref(false);
const imagePreviewUrl = ref('');
/** 判断文件是否为图片类型 */
function isImageUrl(url: string) {
return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(url);
}
/** 处理文件预览 */
function handleFilePreview(file: any) {
if (!file?.url && !file?.response) {
message.warning('文件地址不存在,无法预览');
return;
}
const url = file.url || file?.response?.url || file?.response;
if (!url) {
message.warning('文件地址不存在,无法预览');
return;
}
if (isImageUrl(url)) {
imagePreviewUrl.value = url;
imagePreviewVisible.value = true;
} else {
window.open(url, '_blank');
}
}
/** 处理弹窗可见性 */
function handlePopoverVisible(visible: boolean) {
if (!visible) {
@ -843,6 +889,16 @@ defineExpose({ loadTodoTask });
:rows="4"
/>
</FormItem>
<FormItem label="上传附件/图片" name="attachments">
<FileUpload
v-model:value="approveReasonForm.attachments"
:max-number="10"
:multiple="true"
:show-download-icon="false"
help-text="支持多文件/图片上传"
@preview="handleFilePreview"
/>
</FormItem>
<FormItem>
<Space>
<Button
@ -900,6 +956,15 @@ defineExpose({ loadTodoTask });
:rows="4"
/>
</FormItem>
<FormItem label="上传附件/图片" name="attachments">
<FileUpload
v-model:value="rejectReasonForm.attachments"
:max-number="10"
:multiple="true"
help-text="支持多文件/图片上传"
@preview="handleFilePreview"
/>
</FormItem>
<FormItem>
<Button
:disabled="formLoading"
@ -1444,4 +1509,17 @@ defineExpose({ loadTodoTask });
<!-- 签名弹窗 -->
<SignatureModal @success="handleSignFinish" />
<!-- 图片预览隐藏的 Image 组件仅用于附件预览弹窗 -->
<div style="display: none">
<Image
:preview="{
visible: imagePreviewVisible,
onVisibleChange: (visible: boolean) => {
imagePreviewVisible = visible;
},
}"
:src="imagePreviewUrl"
/>
</div>
</template>

View File

@ -194,16 +194,25 @@ function shouldShowCustomUserSelect(
);
}
/** 判断是否需要显示审批意见 */
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
/** 判断是否需要显示审批意见和附件 */
function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) {
return (
task.reason &&
(task.reason || task.attachments?.length > 0) &&
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
nodeType,
)
);
}
function getAttachmentName(url: string) {
return decodeURIComponent(url.slice(url.lastIndexOf('/') + 1));
}
function isImageAttachment(url: string) {
const ext = url.split('.').pop()?.toLowerCase();
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
}
/** 用户选择弹窗关闭 */
function handleUserSelectClosed() {
selectedUsers.value = [];
@ -406,13 +415,60 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
</div>
</div>
<!-- 审批意见和签名 -->
<!-- 审批意见,附件和签名 -->
<teleport defer :to="`#activity-task-${activity.id}-${index}`">
<div
v-if="shouldShowApprovalReason(task, activity.nodeType)"
v-if="shouldShowReasonAndAttachment(task, activity.nodeType)"
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
>
审批意见{{ task.reason }}
<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="

View File

@ -83,6 +83,7 @@ export namespace BpmProcessInstanceApi {
reason: string;
signPicUrl: string;
status: number;
attachments?: string[];
}
/** 抄送流程实例 */

View File

@ -139,9 +139,6 @@ function handleRemove(file: UploadFile) {
/** 处理文件预览 */
function handlePreview(file: UploadFile) {
emit('preview', file);
if (file.url) {
window.open(file.url);
}
}
/** 处理文件数量超限 */
@ -307,7 +304,7 @@ function getValue() {
</script>
<template>
<div>
<div class="w-full">
<ElUpload
v-bind="$attrs"
v-model:file-list="fileList"

View File

@ -31,6 +31,7 @@ import {
ElForm,
ElFormItem,
ElImage,
ElImageViewer,
ElInput,
ElMessage,
ElOption,
@ -55,6 +56,7 @@ import {
transferTask,
} from '#/api/bpm/task';
import { setConfAndFields2 } from '#/components/form-create';
import { FileUpload } from '#/components/upload';
import { $t } from '#/locales';
import Signature from './signature.vue';
@ -119,6 +121,7 @@ const approveReasonForm: any = reactive({
reason: '',
signPicUrl: '',
nextAssignees: {},
attachments: [],
});
const approveReasonRule: Record<string, any> = computed(() => {
return {
@ -141,6 +144,7 @@ const approveReasonRule: Record<string, any> = computed(() => {
const rejectFormRef = ref<FormInstance>();
const rejectReasonForm = reactive({
reason: '',
attachments: [],
}); //
const rejectReasonRule: any = computed(() => {
return {
@ -299,6 +303,14 @@ function closePopover(type: string, formRef: any | FormInstance) {
if (formRef) {
formRef.resetFields();
}
if (type === 'approve') {
approveReasonForm.reason = '';
approveReasonForm.attachments = [];
approveReasonForm.signPicUrl = '';
} else if (type === 'reject') {
rejectReasonForm.reason = '';
rejectReasonForm.attachments = [];
}
if (popOverVisible.value[type]) popOverVisible.value[type] = false;
nextAssigneesActivityNode.value = [];
// Timeline
@ -410,6 +422,7 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
const data = {
id: runningTask.value.id,
reason: approveReasonForm.reason,
attachments: approveReasonForm.attachments,
variables, // ,
nextAssignees: approveReasonForm.nextAssignees, //
} as any;
@ -423,6 +436,9 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
await formCreateApi.validate();
}
await approveTask(data);
approveReasonForm.reason = '';
approveReasonForm.attachments = [];
approveReasonForm.signPicUrl = '';
popOverVisible.value.approve = false;
nextAssigneesActivityNode.value = [];
// Timeline
@ -435,8 +451,11 @@ async function handleAudit(pass: boolean, formRef: FormInstance | undefined) {
const data = {
id: runningTask.value.id,
reason: rejectReasonForm.reason,
attachments: rejectReasonForm.attachments,
};
await rejectTask(data);
rejectReasonForm.reason = '';
rejectReasonForm.attachments = [];
popOverVisible.value.reject = false;
ElMessage.success('审批不通过成功');
}
@ -757,6 +776,34 @@ function handleSignFinish(url: string) {
approveFormRef.value?.validateField('signPicUrl');
}
/** 判断文件是否为图片类型 */
function isImageUrl(url: string) {
return /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(url);
}
/** 附件图片预览 */
const imagePreviewVisible = ref(false);
const imagePreviewUrl = ref('');
/** 处理文件预览 */
function handleFilePreview(file: any) {
if (!file?.url && !file?.response) {
ElMessage.warning('文件地址不存在,无法预览');
return;
}
const url = file.url || file?.response?.url || file?.response;
if (!url) {
ElMessage.warning('文件地址不存在,无法预览');
return;
}
if (isImageUrl(url)) {
imagePreviewUrl.value = url;
imagePreviewVisible.value = true;
} else {
window.open(url, '_blank');
}
}
// TODO @jasonhandlePopoverVisible
defineExpose({ loadTodoTask });
@ -849,6 +896,15 @@ defineExpose({ loadTodoTask });
:rows="4"
/>
</ElFormItem>
<ElFormItem label="上传附件/图片" prop="attachments">
<FileUpload
v-model:value="approveReasonForm.attachments"
:max-number="10"
:multiple="true"
help-text="支持多文件/图片上传"
@preview="handleFilePreview"
/>
</ElFormItem>
<ElFormItem>
<ElSpace>
<ElButton
@ -873,7 +929,7 @@ defineExpose({ loadTodoTask });
<ElPopover
:visible="popOverVisible.reject"
placement="top"
:popper-style="{ minWidth: '400px' }"
:popper-style="{ minWidth: '500px' }"
trigger="click"
v-if="
runningTask &&
@ -907,6 +963,15 @@ defineExpose({ loadTodoTask });
:rows="4"
/>
</ElFormItem>
<ElFormItem label="上传附件/图片" prop="attachments">
<FileUpload
v-model:value="rejectReasonForm.attachments"
:max-number="10"
:multiple="true"
help-text="支持多文件/图片上传"
@preview="handleFilePreview"
/>
</ElFormItem>
<ElFormItem>
<ElButton
:disabled="formLoading"
@ -1472,4 +1537,12 @@ defineExpose({ loadTodoTask });
<!-- 签名弹窗 -->
<SignatureModal @success="handleSignFinish" />
<!-- 图片预览弹窗 -->
<ElImageViewer
v-if="imagePreviewVisible"
:url-list="[imagePreviewUrl]"
@close="imagePreviewVisible = false"
:z-index="9999"
/>
</template>

View File

@ -201,16 +201,25 @@ function shouldShowCustomUserSelect(
);
}
/** 判断是否需要显示审批意见 */
function shouldShowApprovalReason(task: any, nodeType: BpmNodeTypeEnum) {
/** 判断是否需要显示审批意见和附件 */
function shouldShowReasonAndAttachment(task: any, nodeType: BpmNodeTypeEnum) {
return (
task.reason &&
(task.reason || task.attachments?.length > 0) &&
[BpmNodeTypeEnum.START_USER_NODE, BpmNodeTypeEnum.USER_TASK_NODE].includes(
nodeType,
)
);
}
function getAttachmentName(url: string) {
return decodeURIComponent(url.slice(url.lastIndexOf('/') + 1));
}
function isImageAttachment(url: string) {
const ext = url.split('.').pop()?.toLowerCase();
return ['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp'].includes(ext || '');
}
/** 用户选择弹窗关闭 */
function handleUserSelectClosed() {
selectedUsers.value = [];
@ -411,13 +420,60 @@ defineExpose({ setCustomApproveUsers, batchSetCustomApproveUsers });
</div>
</div>
<!-- 审批意见和签名 -->
<!-- 审批意见,附件和签名 -->
<teleport defer :to="`#activity-task-${activity.id}-${index}`">
<div
v-if="shouldShowApprovalReason(task, activity.nodeType)"
v-if="shouldShowReasonAndAttachment(task, activity.nodeType)"
class="mt-1 w-full rounded-md bg-gray-100 p-2 text-sm text-gray-500"
>
审批意见{{ task.reason }}
<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="