feat(bpm): 为 vben 多端接入流程评论
- web-antd、web-antdv-next、web-ele 新增流程评论 API - 流程详情新增评论列表模块 - 操作区新增评论弹窗并提交评论 - 新增 BPM 评论类型字典常量
This commit is contained in:
40
apps/web-antd/src/api/bpm/comment/index.ts
Normal file
40
apps/web-antd/src/api/bpm/comment/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace BpmCommentApi {
|
||||
/** 流程评论 */
|
||||
export interface Comment {
|
||||
id: string;
|
||||
taskId?: string;
|
||||
task?: {
|
||||
id: string;
|
||||
name: string;
|
||||
taskDefinitionKey: string;
|
||||
};
|
||||
processInstanceId: string;
|
||||
type: string;
|
||||
message: string;
|
||||
createTime: string;
|
||||
user?: {
|
||||
id: number;
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
deptName?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 获得指定流程实例的评论列表 */
|
||||
export const getCommentListByProcessInstanceId = async (
|
||||
processInstanceId: string,
|
||||
) => {
|
||||
return await requestClient.get<BpmCommentApi.Comment[]>(
|
||||
'/bpm/comment/list-by-process-instance-id',
|
||||
{ params: { processInstanceId } },
|
||||
);
|
||||
};
|
||||
|
||||
/** 创建流程评论 */
|
||||
export const createComment = async (taskId: string, message: string) => {
|
||||
return await requestClient.post('/bpm/comment/create', { taskId, message });
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ import { setConfAndFields2 } from '#/components/form-create';
|
||||
import { registerComponent } from '#/utils';
|
||||
|
||||
import ProcessInstanceBpmnViewer from './modules/bpm-viewer.vue';
|
||||
import BpmProcessInstanceCommentList from './modules/comment-list.vue';
|
||||
import ProcessInstanceOperationButton from './modules/operation-button.vue';
|
||||
import ProcessPrint from './modules/process-print.vue';
|
||||
import ProcessInstanceSimpleViewer from './modules/simple-bpm-viewer.vue';
|
||||
@@ -53,6 +54,7 @@ const processDefinition = ref<any>({}); // 流程定义
|
||||
const processModelView = ref<any>({}); // 流程模型视图
|
||||
const operationButtonRef = ref(); // 操作按钮组件 ref
|
||||
const activeTab = ref('form');
|
||||
const commentListRef = ref(); // 评论列表组件 ref
|
||||
const taskListRef = ref();
|
||||
const auditIconsMap: {
|
||||
[key: string]:
|
||||
@@ -189,6 +191,8 @@ function setFieldPermission(field: string, permission: string) {
|
||||
const refresh = () => {
|
||||
// 重新获取详情
|
||||
getDetail();
|
||||
// 重新获取评论
|
||||
commentListRef.value?.getList();
|
||||
};
|
||||
|
||||
const [PrintModal, printModalApi] = useVbenModal({
|
||||
@@ -209,6 +213,10 @@ watch(
|
||||
// 如果切换到流转记录标签,刷新任务列表
|
||||
await nextTick();
|
||||
taskListRef.value?.refresh();
|
||||
} else if (newVal === 'comment') {
|
||||
// 如果切换到流程评论标签,刷新评论列表
|
||||
await nextTick();
|
||||
commentListRef.value?.getList();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -356,9 +364,12 @@ onMounted(async () => {
|
||||
:id="id"
|
||||
/>
|
||||
</TabPane>
|
||||
<!-- TODO 待开发 -->
|
||||
<TabPane tab="流转评论" key="comment" v-if="false" class="pr-3">
|
||||
<div class="h-full">待开发</div>
|
||||
<TabPane tab="流程评论" key="comment" class="pr-3">
|
||||
<BpmProcessInstanceCommentList
|
||||
ref="commentListRef"
|
||||
:loading="processInstanceLoading"
|
||||
:id="id"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BpmCommentApi } from '#/api/bpm/comment';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictObj } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Avatar, Empty } from 'ant-design-vue';
|
||||
|
||||
import { getCommentListByProcessInstanceId } from '#/api/bpm/comment';
|
||||
import DictTag from '#/components/dict-tag/dict-tag.vue';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceCommentList' });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
);
|
||||
|
||||
const commentLoading = ref(false); // 评论列表的加载中
|
||||
const comments = ref<BpmCommentApi.Comment[]>([]); // 评论列表
|
||||
|
||||
const commentColorMap: Record<string, string> = {
|
||||
danger: '#ff4d4f',
|
||||
default: '#909399',
|
||||
error: '#ff4d4f',
|
||||
info: '#909399',
|
||||
primary: '#1677ff',
|
||||
success: '#52c41a',
|
||||
warning: '#faad14',
|
||||
}; // 评论类型颜色映射
|
||||
|
||||
/** 获得评论类型简称 */
|
||||
function getCommentText(type: string) {
|
||||
return (getDictObj(DICT_TYPE.BPM_COMMENT_TYPE, type)?.label || '评论').slice(
|
||||
0,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得评论类型颜色 */
|
||||
function getCommentColor(type: string) {
|
||||
const dict = getDictObj(DICT_TYPE.BPM_COMMENT_TYPE, type);
|
||||
return (
|
||||
dict?.cssClass ||
|
||||
commentColorMap[dict?.colorType || 'primary'] ||
|
||||
commentColorMap.primary
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询评论列表 */
|
||||
async function getList() {
|
||||
if (!props.id) {
|
||||
comments.value = [];
|
||||
return;
|
||||
}
|
||||
commentLoading.value = true;
|
||||
try {
|
||||
comments.value = await getCommentListByProcessInstanceId(props.id);
|
||||
} finally {
|
||||
commentLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
() => getList(),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
defineExpose({ getList });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-loading="props.loading || commentLoading"
|
||||
class="min-h-full px-7 py-6"
|
||||
>
|
||||
<div class="flex items-center gap-3 border-b pb-4">
|
||||
<div class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
流程评论
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">共 {{ comments.length }} 条</div>
|
||||
</div>
|
||||
<Empty v-if="comments.length === 0" description="暂无评论" />
|
||||
<div v-else class="mt-6 pl-2">
|
||||
<div
|
||||
v-for="comment in comments"
|
||||
:key="comment.id"
|
||||
class="group relative flex gap-4 pb-7 last:pb-0"
|
||||
>
|
||||
<div
|
||||
class="absolute bottom-0 left-4 top-8 w-px bg-gray-200 group-last:hidden"
|
||||
></div>
|
||||
<div
|
||||
class="z-10 flex size-8 shrink-0 items-center justify-center rounded-full border-2 border-white text-sm font-bold text-white shadow"
|
||||
:style="{ backgroundColor: getCommentColor(comment.type) }"
|
||||
>
|
||||
{{ getCommentText(comment.type) }}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-h-8 items-center gap-2">
|
||||
<div class="flex shrink-0 items-center gap-2 font-bold">
|
||||
<Avatar v-if="comment.user?.avatar" :size="28" :src="comment.user.avatar" />
|
||||
<Avatar v-else :size="28">
|
||||
{{ comment.user?.nickname?.slice(0, 1) || '?' }}
|
||||
</Avatar>
|
||||
<span>{{ comment.user?.nickname || '系统' }}</span>
|
||||
</div>
|
||||
<DictTag :type="DICT_TYPE.BPM_COMMENT_TYPE" :value="comment.type" />
|
||||
<div
|
||||
v-if="comment.task?.name"
|
||||
class="inline-flex h-6 min-w-0 max-w-lg items-center gap-1.5 rounded-md border border-blue-200 bg-blue-50 px-2 text-sm text-gray-700 dark:border-blue-900 dark:bg-blue-950 dark:text-gray-200"
|
||||
>
|
||||
<span class="inline-flex shrink-0 items-center gap-1 font-medium text-blue-500">
|
||||
<IconifyIcon icon="lucide:git-branch" />
|
||||
任务
|
||||
</span>
|
||||
<span class="truncate font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ comment.task.name }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="ml-auto shrink-0 text-sm text-gray-500">
|
||||
{{ formatDateTime(comment.createTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 whitespace-pre-wrap break-words rounded-md bg-gray-50 px-3.5 py-3 leading-6 text-gray-700 dark:bg-gray-900 dark:text-gray-200"
|
||||
>
|
||||
{{ comment.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
cancelProcessInstanceByStartUser,
|
||||
getNextApprovalNodes,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
approveTask,
|
||||
copyTask,
|
||||
@@ -91,6 +92,7 @@ const popOverVisible: any = ref({
|
||||
addSign: false,
|
||||
return: false,
|
||||
copy: false,
|
||||
comment: false,
|
||||
cancel: false,
|
||||
deleteSign: false,
|
||||
}); // 气泡卡是否展示
|
||||
@@ -187,6 +189,14 @@ const copyFormRule: Record<string, Rule[]> = reactive({
|
||||
],
|
||||
});
|
||||
|
||||
const commentFormRef = ref<FormInstance>(); // 评论表单
|
||||
const commentForm = reactive({
|
||||
message: '',
|
||||
});
|
||||
const commentFormRule: Record<string, Rule[]> = reactive({
|
||||
message: [{ required: true, message: '评论内容不能为空', trigger: 'blur' }],
|
||||
});
|
||||
|
||||
const transferFormRef = ref<FormInstance>(); // 转办表单
|
||||
const transferForm = reactive({
|
||||
assigneeUserId: undefined,
|
||||
@@ -503,6 +513,30 @@ async function handleCopy() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理评论 */
|
||||
async function handleComment() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1. 校验表单
|
||||
if (!commentFormRef.value) return;
|
||||
await commentFormRef.value.validate();
|
||||
const content = commentForm.message.trim();
|
||||
if (!content) {
|
||||
message.warning('评论内容不能为空');
|
||||
return;
|
||||
}
|
||||
// 2. 提交评论
|
||||
await createComment(runningTask.value.id, content);
|
||||
commentFormRef.value.resetFields();
|
||||
popOverVisible.value.comment = false;
|
||||
message.success('评论成功');
|
||||
// 3. 加载最新数据
|
||||
reload();
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理转交 */
|
||||
async function handleTransfer() {
|
||||
formLoading.value = true;
|
||||
@@ -1020,6 +1054,56 @@ defineExpose({ loadTodoTask });
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<!-- 【评论】按钮 -->
|
||||
<Popover
|
||||
v-model:open="popOverVisible.comment"
|
||||
placement="top"
|
||||
:overlay-style="{ width: '400px' }"
|
||||
trigger="click"
|
||||
v-if="runningTask && isHandleTaskStatus()"
|
||||
>
|
||||
<Button ghost type="primary" @click="openPopover('comment')">
|
||||
<IconifyIcon icon="lucide:message-circle" />
|
||||
评论
|
||||
</Button>
|
||||
<template #content>
|
||||
<div class="flex flex-1 flex-col px-5 pt-5" v-loading="formLoading">
|
||||
<Form
|
||||
layout="vertical"
|
||||
class="mb-auto"
|
||||
ref="commentFormRef"
|
||||
:model="commentForm"
|
||||
:rules="commentFormRule"
|
||||
label-width="100px"
|
||||
>
|
||||
<FormItem label="评论内容" name="message">
|
||||
<Textarea
|
||||
v-model:value="commentForm.message"
|
||||
placeholder="请输入评论内容"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Space>
|
||||
<Button
|
||||
:disabled="formLoading"
|
||||
type="primary"
|
||||
@click="handleComment"
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
<Button @click="closePopover('comment', commentFormRef)">
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<!-- 【抄送】按钮 -->
|
||||
<Popover
|
||||
v-model:open="popOverVisible.copy"
|
||||
|
||||
40
apps/web-antdv-next/src/api/bpm/comment/index.ts
Normal file
40
apps/web-antdv-next/src/api/bpm/comment/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace BpmCommentApi {
|
||||
/** 流程评论 */
|
||||
export interface Comment {
|
||||
id: string;
|
||||
taskId?: string;
|
||||
task?: {
|
||||
id: string;
|
||||
name: string;
|
||||
taskDefinitionKey: string;
|
||||
};
|
||||
processInstanceId: string;
|
||||
type: string;
|
||||
message: string;
|
||||
createTime: string;
|
||||
user?: {
|
||||
id: number;
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
deptName?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 获得指定流程实例的评论列表 */
|
||||
export const getCommentListByProcessInstanceId = async (
|
||||
processInstanceId: string,
|
||||
) => {
|
||||
return await requestClient.get<BpmCommentApi.Comment[]>(
|
||||
'/bpm/comment/list-by-process-instance-id',
|
||||
{ params: { processInstanceId } },
|
||||
);
|
||||
};
|
||||
|
||||
/** 创建流程评论 */
|
||||
export const createComment = async (taskId: string, message: string) => {
|
||||
return await requestClient.post('/bpm/comment/create', { taskId, message });
|
||||
};
|
||||
|
||||
@@ -33,6 +33,7 @@ import { setConfAndFields2 } from '#/components/form-create';
|
||||
import { registerComponent } from '#/utils';
|
||||
|
||||
import ProcessInstanceBpmnViewer from './modules/bpm-viewer.vue';
|
||||
import BpmProcessInstanceCommentList from './modules/comment-list.vue';
|
||||
import ProcessInstanceOperationButton from './modules/operation-button.vue';
|
||||
import ProcessPrint from './modules/process-print.vue';
|
||||
import ProcessInstanceSimpleViewer from './modules/simple-bpm-viewer.vue';
|
||||
@@ -53,6 +54,7 @@ const processDefinition = ref<any>({}); // 流程定义
|
||||
const processModelView = ref<any>({}); // 流程模型视图
|
||||
const operationButtonRef = ref(); // 操作按钮组件 ref
|
||||
const activeTab = ref('form');
|
||||
const commentListRef = ref(); // 评论列表组件 ref
|
||||
const taskListRef = ref();
|
||||
const auditIconsMap: {
|
||||
[key: string]:
|
||||
@@ -189,6 +191,8 @@ function setFieldPermission(field: string, permission: string) {
|
||||
const refresh = () => {
|
||||
// 重新获取详情
|
||||
getDetail();
|
||||
// 重新获取评论
|
||||
commentListRef.value?.getList();
|
||||
};
|
||||
|
||||
const [PrintModal, printModalApi] = useVbenModal({
|
||||
@@ -209,6 +213,10 @@ watch(
|
||||
// 如果切换到流转记录标签,刷新任务列表
|
||||
await nextTick();
|
||||
taskListRef.value?.refresh();
|
||||
} else if (newVal === 'comment') {
|
||||
// 如果切换到流程评论标签,刷新评论列表
|
||||
await nextTick();
|
||||
commentListRef.value?.getList();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -354,9 +362,12 @@ onMounted(async () => {
|
||||
:id="id"
|
||||
/>
|
||||
</TabPane>
|
||||
<!-- TODO 待开发 -->
|
||||
<TabPane tab="流转评论" key="comment" v-if="false" class="pr-3">
|
||||
<div class="h-full">待开发</div>
|
||||
<TabPane tab="流程评论" key="comment" class="pr-3">
|
||||
<BpmProcessInstanceCommentList
|
||||
ref="commentListRef"
|
||||
:loading="processInstanceLoading"
|
||||
:id="id"
|
||||
/>
|
||||
</TabPane>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BpmCommentApi } from '#/api/bpm/comment';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictObj } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { Avatar, Empty } from 'antdv-next';
|
||||
|
||||
import { getCommentListByProcessInstanceId } from '#/api/bpm/comment';
|
||||
import DictTag from '#/components/dict-tag/dict-tag.vue';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceCommentList' });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
);
|
||||
|
||||
const commentLoading = ref(false); // 评论列表的加载中
|
||||
const comments = ref<BpmCommentApi.Comment[]>([]); // 评论列表
|
||||
|
||||
const commentColorMap: Record<string, string> = {
|
||||
danger: '#ff4d4f',
|
||||
default: '#909399',
|
||||
error: '#ff4d4f',
|
||||
info: '#909399',
|
||||
primary: '#1677ff',
|
||||
success: '#52c41a',
|
||||
warning: '#faad14',
|
||||
}; // 评论类型颜色映射
|
||||
|
||||
/** 获得评论类型简称 */
|
||||
function getCommentText(type: string) {
|
||||
return (getDictObj(DICT_TYPE.BPM_COMMENT_TYPE, type)?.label || '评论').slice(
|
||||
0,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得评论类型颜色 */
|
||||
function getCommentColor(type: string) {
|
||||
const dict = getDictObj(DICT_TYPE.BPM_COMMENT_TYPE, type);
|
||||
return (
|
||||
dict?.cssClass ||
|
||||
commentColorMap[dict?.colorType || 'primary'] ||
|
||||
commentColorMap.primary
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询评论列表 */
|
||||
async function getList() {
|
||||
if (!props.id) {
|
||||
comments.value = [];
|
||||
return;
|
||||
}
|
||||
commentLoading.value = true;
|
||||
try {
|
||||
comments.value = await getCommentListByProcessInstanceId(props.id);
|
||||
} finally {
|
||||
commentLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
() => getList(),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
defineExpose({ getList });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-loading="props.loading || commentLoading"
|
||||
class="min-h-full px-7 py-6"
|
||||
>
|
||||
<div class="flex items-center gap-3 border-b pb-4">
|
||||
<div class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
流程评论
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">共 {{ comments.length }} 条</div>
|
||||
</div>
|
||||
<Empty v-if="comments.length === 0" description="暂无评论" />
|
||||
<div v-else class="mt-6 pl-2">
|
||||
<div
|
||||
v-for="comment in comments"
|
||||
:key="comment.id"
|
||||
class="group relative flex gap-4 pb-7 last:pb-0"
|
||||
>
|
||||
<div
|
||||
class="absolute bottom-0 left-4 top-8 w-px bg-gray-200 group-last:hidden"
|
||||
></div>
|
||||
<div
|
||||
class="z-10 flex size-8 shrink-0 items-center justify-center rounded-full border-2 border-white text-sm font-bold text-white shadow"
|
||||
:style="{ backgroundColor: getCommentColor(comment.type) }"
|
||||
>
|
||||
{{ getCommentText(comment.type) }}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-h-8 items-center gap-2">
|
||||
<div class="flex shrink-0 items-center gap-2 font-bold">
|
||||
<Avatar v-if="comment.user?.avatar" :size="28" :src="comment.user.avatar" />
|
||||
<Avatar v-else :size="28">
|
||||
{{ comment.user?.nickname?.slice(0, 1) || '?' }}
|
||||
</Avatar>
|
||||
<span>{{ comment.user?.nickname || '系统' }}</span>
|
||||
</div>
|
||||
<DictTag :type="DICT_TYPE.BPM_COMMENT_TYPE" :value="comment.type" />
|
||||
<div
|
||||
v-if="comment.task?.name"
|
||||
class="inline-flex h-6 min-w-0 max-w-lg items-center gap-1.5 rounded-md border border-blue-200 bg-blue-50 px-2 text-sm text-gray-700 dark:border-blue-900 dark:bg-blue-950 dark:text-gray-200"
|
||||
>
|
||||
<span class="inline-flex shrink-0 items-center gap-1 font-medium text-blue-500">
|
||||
<IconifyIcon icon="lucide:git-branch" />
|
||||
任务
|
||||
</span>
|
||||
<span class="truncate font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ comment.task.name }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="ml-auto shrink-0 text-sm text-gray-500">
|
||||
{{ formatDateTime(comment.createTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 whitespace-pre-wrap break-words rounded-md bg-gray-50 px-3.5 py-3 leading-6 text-gray-700 dark:bg-gray-900 dark:text-gray-200"
|
||||
>
|
||||
{{ comment.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
cancelProcessInstanceByStartUser,
|
||||
getNextApprovalNodes,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
approveTask,
|
||||
copyTask,
|
||||
@@ -90,6 +91,7 @@ const popOverVisible: any = ref({
|
||||
addSign: false,
|
||||
return: false,
|
||||
copy: false,
|
||||
comment: false,
|
||||
cancel: false,
|
||||
deleteSign: false,
|
||||
}); // 气泡卡是否展示
|
||||
@@ -186,6 +188,14 @@ const copyFormRule: Record<string, Rule[]> = reactive({
|
||||
],
|
||||
});
|
||||
|
||||
const commentFormRef = ref<FormInstance>(); // 评论表单
|
||||
const commentForm = reactive({
|
||||
message: '',
|
||||
});
|
||||
const commentFormRule: Record<string, Rule[]> = reactive({
|
||||
message: [{ required: true, message: '评论内容不能为空', trigger: 'blur' }],
|
||||
});
|
||||
|
||||
const transferFormRef = ref<FormInstance>(); // 转办表单
|
||||
const transferForm = reactive({
|
||||
assigneeUserId: undefined,
|
||||
@@ -502,6 +512,30 @@ async function handleCopy() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理评论 */
|
||||
async function handleComment() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1. 校验表单
|
||||
if (!commentFormRef.value) return;
|
||||
await commentFormRef.value.validate();
|
||||
const content = commentForm.message.trim();
|
||||
if (!content) {
|
||||
message.warning('评论内容不能为空');
|
||||
return;
|
||||
}
|
||||
// 2. 提交评论
|
||||
await createComment(runningTask.value.id, content);
|
||||
commentFormRef.value.resetFields();
|
||||
popOverVisible.value.comment = false;
|
||||
message.success('评论成功');
|
||||
// 3. 加载最新数据
|
||||
reload();
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理转交 */
|
||||
async function handleTransfer() {
|
||||
formLoading.value = true;
|
||||
@@ -1023,6 +1057,56 @@ defineExpose({ loadTodoTask });
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<!-- 【评论】按钮 -->
|
||||
<Popover
|
||||
v-model:open="popOverVisible.comment"
|
||||
placement="top"
|
||||
:styles="{ root: { width: '400px' } }"
|
||||
:trigger="['click']"
|
||||
v-if="runningTask && isHandleTaskStatus()"
|
||||
>
|
||||
<Button ghost type="primary" @click="openPopover('comment')">
|
||||
<IconifyIcon icon="lucide:message-circle" />
|
||||
评论
|
||||
</Button>
|
||||
<template #content>
|
||||
<div class="flex flex-1 flex-col px-5 pt-5" v-loading="formLoading">
|
||||
<Form
|
||||
layout="vertical"
|
||||
class="mb-auto"
|
||||
ref="commentFormRef"
|
||||
:model="commentForm"
|
||||
:rules="commentFormRule"
|
||||
label-width="100px"
|
||||
>
|
||||
<FormItem label="评论内容" name="message">
|
||||
<TextArea
|
||||
v-model:value="commentForm.message"
|
||||
placeholder="请输入评论内容"
|
||||
:rows="4"
|
||||
:maxlength="500"
|
||||
show-count
|
||||
/>
|
||||
</FormItem>
|
||||
<FormItem>
|
||||
<Space>
|
||||
<Button
|
||||
:disabled="formLoading"
|
||||
type="primary"
|
||||
@click="handleComment"
|
||||
>
|
||||
提交
|
||||
</Button>
|
||||
<Button @click="closePopover('comment', commentFormRef)">
|
||||
取消
|
||||
</Button>
|
||||
</Space>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</div>
|
||||
</template>
|
||||
</Popover>
|
||||
|
||||
<!-- 【抄送】按钮 -->
|
||||
<Popover
|
||||
v-model:open="popOverVisible.copy"
|
||||
|
||||
40
apps/web-ele/src/api/bpm/comment/index.ts
Normal file
40
apps/web-ele/src/api/bpm/comment/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace BpmCommentApi {
|
||||
/** 流程评论 */
|
||||
export interface Comment {
|
||||
id: string;
|
||||
taskId?: string;
|
||||
task?: {
|
||||
id: string;
|
||||
name: string;
|
||||
taskDefinitionKey: string;
|
||||
};
|
||||
processInstanceId: string;
|
||||
type: string;
|
||||
message: string;
|
||||
createTime: string;
|
||||
user?: {
|
||||
id: number;
|
||||
nickname: string;
|
||||
avatar?: string;
|
||||
deptName?: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/** 获得指定流程实例的评论列表 */
|
||||
export const getCommentListByProcessInstanceId = async (
|
||||
processInstanceId: string,
|
||||
) => {
|
||||
return await requestClient.get<BpmCommentApi.Comment[]>(
|
||||
'/bpm/comment/list-by-process-instance-id',
|
||||
{ params: { processInstanceId } },
|
||||
);
|
||||
};
|
||||
|
||||
/** 创建流程评论 */
|
||||
export const createComment = async (taskId: string, message: string) => {
|
||||
return await requestClient.post('/bpm/comment/create', { taskId, message });
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ import { setConfAndFields2 } from '#/components/form-create';
|
||||
import { registerComponent } from '#/utils';
|
||||
|
||||
import ProcessInstanceBpmnViewer from './modules/bpm-viewer.vue';
|
||||
import BpmProcessInstanceCommentList from './modules/comment-list.vue';
|
||||
import ProcessInstanceOperationButton from './modules/operation-button.vue';
|
||||
import ProcessPrint from './modules/process-print.vue';
|
||||
import ProcessInstanceSimpleViewer from './modules/simple-bpm-viewer.vue';
|
||||
@@ -61,6 +62,7 @@ const processDefinition = ref<any>({}); // 流程定义
|
||||
const processModelView = ref<any>({}); // 流程模型视图
|
||||
const operationButtonRef = ref(); // 操作按钮组件 ref
|
||||
const activeTab = ref('form');
|
||||
const commentListRef = ref(); // 评论列表组件 ref
|
||||
const taskListRef = ref();
|
||||
const auditIconsMap: {
|
||||
[key: string]:
|
||||
@@ -197,6 +199,8 @@ function setFieldPermission(field: string, permission: string) {
|
||||
const refresh = () => {
|
||||
// 重新获取详情
|
||||
getDetail();
|
||||
// 重新获取评论
|
||||
commentListRef.value?.getList();
|
||||
};
|
||||
|
||||
const [PrintModal, printModalApi] = useVbenModal({
|
||||
@@ -217,6 +221,10 @@ watch(
|
||||
// 如果切换到流转记录标签,刷新任务列表
|
||||
await nextTick();
|
||||
taskListRef.value?.refresh();
|
||||
} else if (newVal === 'comment') {
|
||||
// 如果切换到流程评论标签,刷新评论列表
|
||||
await nextTick();
|
||||
commentListRef.value?.getList();
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -373,14 +381,12 @@ onMounted(async () => {
|
||||
:id="id"
|
||||
/>
|
||||
</ElTabPane>
|
||||
<!-- TODO 待开发 -->
|
||||
<ElTabPane
|
||||
label="流转评论"
|
||||
name="comment"
|
||||
v-if="false"
|
||||
class="pr-3"
|
||||
>
|
||||
<div>待开发</div>
|
||||
<ElTabPane label="流程评论" name="comment" class="pr-3">
|
||||
<BpmProcessInstanceCommentList
|
||||
ref="commentListRef"
|
||||
:loading="processInstanceLoading"
|
||||
:id="id"
|
||||
/>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<script lang="ts" setup>
|
||||
import type { BpmCommentApi } from '#/api/bpm/comment';
|
||||
|
||||
import { ref, watch } from 'vue';
|
||||
|
||||
import { DICT_TYPE } from '@vben/constants';
|
||||
import { getDictObj } from '@vben/hooks';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { formatDateTime } from '@vben/utils';
|
||||
|
||||
import { ElAvatar, ElEmpty } from 'element-plus';
|
||||
|
||||
import { getCommentListByProcessInstanceId } from '#/api/bpm/comment';
|
||||
import DictTag from '#/components/dict-tag/dict-tag.vue';
|
||||
|
||||
defineOptions({ name: 'BpmProcessInstanceCommentList' });
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
id?: string;
|
||||
loading?: boolean;
|
||||
}>(),
|
||||
{
|
||||
loading: false,
|
||||
},
|
||||
);
|
||||
|
||||
const commentLoading = ref(false); // 评论列表的加载中
|
||||
const comments = ref<BpmCommentApi.Comment[]>([]); // 评论列表
|
||||
|
||||
const commentColorMap: Record<string, string> = {
|
||||
danger: 'var(--el-color-danger)',
|
||||
default: 'var(--el-color-info)',
|
||||
error: 'var(--el-color-danger)',
|
||||
info: 'var(--el-color-info)',
|
||||
primary: 'var(--el-color-primary)',
|
||||
success: 'var(--el-color-success)',
|
||||
warning: 'var(--el-color-warning)',
|
||||
}; // 评论类型颜色映射
|
||||
|
||||
/** 获得评论类型简称 */
|
||||
function getCommentText(type: string) {
|
||||
return (getDictObj(DICT_TYPE.BPM_COMMENT_TYPE, type)?.label || '评论').slice(
|
||||
0,
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得评论类型颜色 */
|
||||
function getCommentColor(type: string) {
|
||||
const dict = getDictObj(DICT_TYPE.BPM_COMMENT_TYPE, type);
|
||||
return (
|
||||
dict?.cssClass ||
|
||||
commentColorMap[dict?.colorType || 'primary'] ||
|
||||
commentColorMap.primary
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询评论列表 */
|
||||
async function getList() {
|
||||
if (!props.id) {
|
||||
comments.value = [];
|
||||
return;
|
||||
}
|
||||
commentLoading.value = true;
|
||||
try {
|
||||
comments.value = await getCommentListByProcessInstanceId(props.id);
|
||||
} finally {
|
||||
commentLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.id,
|
||||
() => getList(),
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
defineExpose({ getList });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-loading="props.loading || commentLoading"
|
||||
class="min-h-full px-7 py-6"
|
||||
>
|
||||
<div class="flex items-center gap-3 border-b pb-4">
|
||||
<div class="text-lg font-bold text-gray-900 dark:text-gray-100">
|
||||
流程评论
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">共 {{ comments.length }} 条</div>
|
||||
</div>
|
||||
<ElEmpty v-if="comments.length === 0" description="暂无评论" />
|
||||
<div v-else class="mt-6 pl-2">
|
||||
<div
|
||||
v-for="comment in comments"
|
||||
:key="comment.id"
|
||||
class="group relative flex gap-4 pb-7 last:pb-0"
|
||||
>
|
||||
<div
|
||||
class="absolute bottom-0 left-4 top-8 w-px bg-gray-200 group-last:hidden"
|
||||
></div>
|
||||
<div
|
||||
class="z-10 flex size-8 shrink-0 items-center justify-center rounded-full border-2 border-white text-sm font-bold text-white shadow"
|
||||
:style="{ backgroundColor: getCommentColor(comment.type) }"
|
||||
>
|
||||
{{ getCommentText(comment.type) }}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex min-h-8 items-center gap-2">
|
||||
<div class="flex shrink-0 items-center gap-2 font-bold">
|
||||
<ElAvatar v-if="comment.user?.avatar" :size="28" :src="comment.user.avatar" />
|
||||
<ElAvatar v-else :size="28">
|
||||
{{ comment.user?.nickname?.slice(0, 1) || '?' }}
|
||||
</ElAvatar>
|
||||
<span>{{ comment.user?.nickname || '系统' }}</span>
|
||||
</div>
|
||||
<DictTag :type="DICT_TYPE.BPM_COMMENT_TYPE" :value="comment.type" />
|
||||
<div
|
||||
v-if="comment.task?.name"
|
||||
class="inline-flex h-6 min-w-0 max-w-lg items-center gap-1.5 rounded-md border border-blue-200 bg-blue-50 px-2 text-sm text-gray-700 dark:border-blue-900 dark:bg-blue-950 dark:text-gray-200"
|
||||
>
|
||||
<span class="inline-flex shrink-0 items-center gap-1 font-medium text-blue-500">
|
||||
<IconifyIcon icon="lucide:git-branch" />
|
||||
任务
|
||||
</span>
|
||||
<span class="truncate font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ comment.task.name }}
|
||||
</span>
|
||||
</div>
|
||||
<span class="ml-auto shrink-0 text-sm text-gray-500">
|
||||
{{ formatDateTime(comment.createTime) }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-2 whitespace-pre-wrap break-words rounded-md bg-gray-50 px-3.5 py-3 leading-6 text-gray-700 dark:bg-gray-900 dark:text-gray-200"
|
||||
>
|
||||
{{ comment.message }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
cancelProcessInstanceByStartUser,
|
||||
getNextApprovalNodes,
|
||||
} from '#/api/bpm/processInstance';
|
||||
import { createComment } from '#/api/bpm/comment';
|
||||
import {
|
||||
approveTask,
|
||||
copyTask,
|
||||
@@ -91,6 +92,7 @@ const popOverVisible: any = ref({
|
||||
addSign: false,
|
||||
return: false,
|
||||
copy: false,
|
||||
comment: false,
|
||||
cancel: false,
|
||||
deleteSign: false,
|
||||
}); // 气泡卡是否展示
|
||||
@@ -192,6 +194,14 @@ const copyFormRule: FormRules = reactive({
|
||||
],
|
||||
});
|
||||
|
||||
const commentFormRef = ref<FormInstance>(); // 评论表单
|
||||
const commentForm = reactive({
|
||||
message: '',
|
||||
});
|
||||
const commentFormRule: FormRules = reactive({
|
||||
message: [{ required: true, message: '评论内容不能为空', trigger: 'blur' }],
|
||||
});
|
||||
|
||||
const transferFormRef = ref<FormInstance>(); // 转办表单
|
||||
const transferForm = reactive({
|
||||
assigneeUserId: undefined,
|
||||
@@ -513,6 +523,30 @@ async function handleCopy() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理评论 */
|
||||
async function handleComment() {
|
||||
formLoading.value = true;
|
||||
try {
|
||||
// 1. 校验表单
|
||||
if (!commentFormRef.value) return;
|
||||
await commentFormRef.value.validate();
|
||||
const content = commentForm.message.trim();
|
||||
if (!content) {
|
||||
ElMessage.warning('评论内容不能为空');
|
||||
return;
|
||||
}
|
||||
// 2. 提交评论
|
||||
await createComment(runningTask.value.id, content);
|
||||
commentFormRef.value.resetFields();
|
||||
popOverVisible.value.comment = false;
|
||||
ElMessage.success('评论成功');
|
||||
// 3. 加载最新数据
|
||||
reload();
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理转交 */
|
||||
async function handleTransfer() {
|
||||
formLoading.value = true;
|
||||
@@ -1026,6 +1060,57 @@ defineExpose({ loadTodoTask });
|
||||
</div>
|
||||
</ElPopover>
|
||||
|
||||
<!-- 【评论】按钮 -->
|
||||
<ElPopover
|
||||
:visible="popOverVisible.comment"
|
||||
placement="top"
|
||||
:popper-style="{ width: '400px' }"
|
||||
trigger="click"
|
||||
v-if="runningTask && isHandleTaskStatus()"
|
||||
>
|
||||
<template #reference>
|
||||
<ElButton plain type="primary" @click="openPopover('comment')">
|
||||
<IconifyIcon icon="lucide:message-circle" />
|
||||
<span class="ml-1">评论</span>
|
||||
</ElButton>
|
||||
</template>
|
||||
<div class="flex flex-1 flex-col px-5 pt-5" v-loading="formLoading">
|
||||
<ElForm
|
||||
label-position="top"
|
||||
class="mb-auto"
|
||||
ref="commentFormRef"
|
||||
:model="commentForm"
|
||||
:rules="commentFormRule"
|
||||
label-width="100px"
|
||||
>
|
||||
<ElFormItem label="评论内容" prop="message">
|
||||
<ElInput
|
||||
type="textarea"
|
||||
v-model="commentForm.message"
|
||||
placeholder="请输入评论内容"
|
||||
:rows="4"
|
||||
maxlength="500"
|
||||
show-word-limit
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem>
|
||||
<ElSpace>
|
||||
<ElButton
|
||||
:disabled="formLoading"
|
||||
type="primary"
|
||||
@click="handleComment"
|
||||
>
|
||||
提交
|
||||
</ElButton>
|
||||
<ElButton @click="closePopover('comment', commentFormRef)">
|
||||
取消
|
||||
</ElButton>
|
||||
</ElSpace>
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
</div>
|
||||
</ElPopover>
|
||||
|
||||
<!-- 【抄送】按钮 -->
|
||||
<ElPopover
|
||||
:visible="popOverVisible.copy"
|
||||
|
||||
@@ -47,6 +47,7 @@ const BPM_DICT = {
|
||||
BPM_PROCESS_INSTANCE_STATUS: 'bpm_process_instance_status', // BPM 流程实例状态
|
||||
BPM_PROCESS_LISTENER_TYPE: 'bpm_process_listener_type', // BPM 流程监听器类型
|
||||
BPM_PROCESS_LISTENER_VALUE_TYPE: 'bpm_process_listener_value_type', // BPM 流程监听器值类型
|
||||
BPM_COMMENT_TYPE: 'bpm_comment_type', // BPM 评论类型
|
||||
BPM_TASK_CANDIDATE_STRATEGY: 'bpm_task_candidate_strategy', // BPM 任务候选人策略
|
||||
BPM_TASK_STATUS: 'bpm_task_status', // BPM 任务状态
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user