feat(bpm): 为 vben 多端接入流程评论

- web-antd、web-antdv-next、web-ele 新增流程评论 API
- 流程详情新增评论列表模块
- 操作区新增评论弹窗并提交评论
- 新增 BPM 评论类型字典常量
This commit is contained in:
YunaiV
2026-07-02 11:28:05 +08:00
parent a58e85e5e3
commit c5f845125c
13 changed files with 848 additions and 14 deletions

View 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 });
};

View File

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

View File

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

View File

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

View 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 });
};

View File

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

View File

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

View File

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

View 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 });
};

View File

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

View File

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

View File

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

View File

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