提交 62b97b82 authored 作者: liuluyu's avatar liuluyu

Merge branch 'master' of http://47.97.51.208/root/zrch-risk-39

......@@ -147,6 +147,17 @@ export function getMyTaskFlow(data) {
})
}
// 得到退回的节点
export function getRejectNode(query) {
alert(JSON.stringify(query))
return defHttp.get({
url: '/flowable/form/getRejectTargetNode',
params: query
})
}
<template>
<div class="approval-panel">
<a-card class="approval-card" :bordered="false">
<template #title>
<div class="approval-header">
<span class="approval-title">{{ title }}</span>
<a-tag :color="tagColor" v-if="showTag">{{ tagText }}</a-tag>
<span class="approval-title">审批操作</span>
</div>
</template>
<template #extra>
<a-button @click="handleSaveApp" type="primary" size="small">保存审批</a-button>
</template>
<div class="approval-content">
<a-form :model="approvalForm" layout="vertical">
<a-form-item label="审核结果" required>
<a-radio-group v-model:value="approvalForm.result" class="result-group">
<a-radio
v-for="option in resultOptions"
:key="option.value"
:value="option.value"
:class="option.className"
>
<div class="radio-content">
<component :is="option.icon" class="radio-icon" />
<span class="radio-label">{{ option.label }}</span>
</div>
</a-radio>
<BasicForm @register="registerForm" style="width: 100%;">
<template #slot_passOrReturn="{ model }">
<a-radio-group v-model:value="model.reviewStatus" button-style="solid" class="approval-radio-group">
<a-radio-button :value="true" class="radio-button">
<CheckCircleOutlined />
通过
</a-radio-button>
<a-radio-button :value="false" class="radio-button">
<CloseCircleOutlined />
退回
</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item
:label="commentLabel"
:required="isCommentRequired"
:extra="commentExtra"
>
<a-textarea
v-model:value="approvalForm.comment"
:rows="commentRows"
:placeholder="commentPlaceholder"
:maxlength="commentMaxLength"
show-count
/>
</a-form-item>
<!-- 退回节点选择(仅当支持退回且结果选择退回时显示) -->
<a-form-item
v-if="showRejectNodeSelect && approvalForm.result === 'rejected'"
label="退回节点"
required
>
<a-select
v-model:value="approvalForm.rejectNode"
placeholder="请选择退回节点"
:options="rejectNodeOptions"
:disabled="!canSelectRejectNode"
>
<template v-if="rejectNodeOptions.length === 0">
<a-select-option value="" disabled>无可退回的节点</a-select-option>
</template>
</a-select>
<div class="form-tip" v-if="rejectNodeOptions.length === 0">
{{ emptyRejectNodesTip }}
</div>
</a-form-item>
<!-- 自定义扩展区域 -->
<slot name="extra-fields"></slot>
<!-- 附加说明 -->
<div v-if="showAdditionalInfo && approvalForm.result === 'rejected'" class="additional-info">
<div class="info-tip">
<info-circle-outlined />
<span>{{ additionalInfoText }}</span>
</div>
</div>
<a-divider v-if="showSummary" />
<div class="approval-summary" v-if="showSummary">
<div
v-for="item in summaryItems"
:key="item.label"
class="summary-item"
>
<span class="summary-label">{{ item.label }}</span>
<span class="summary-value">{{ item.value || '--' }}</span>
</div>
</div>
</a-form>
<a-space direction="" style="width:100%">
<a-button type="primary" ghost block @click="handleSubmit">保存</a-button>
<a-button type="primary" ghost block @click="handleSubmit">保存并发送</a-button>
</a-space>
</BasicForm>
</div>
</a-card>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onUnmounted } from 'vue'
import {
CheckCircleOutlined,
CloseCircleOutlined,
InfoCircleOutlined,
QuestionCircleOutlined
} from '@ant-design/icons-vue'
import dayjs from 'dayjs'
// 审核结果选项接口
interface ResultOption {
value: 'approved' | 'rejected' | string
label: string
icon: any
className: string
color?: string
}
// 摘要项接口
interface SummaryItem {
label: string
value: string
key?: string
}
// 工作流节点接口
interface WorkflowNode {
id: string
name: string
processor?: string
[key: string]: any
}
const props = defineProps({
// 基础配置
title: {
type: String,
default: '审核意见'
},
showTag: {
type: Boolean,
default: true
},
tagColor: {
type: String,
default: 'orange'
},
tagText: {
type: String,
default: '审核节点'
import { ref,onMounted,nextTick } from 'vue'
import { useForm, BasicForm, FormSchema } from '/@/components/Form'
import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons-vue'
import { complete, getMyTaskFlow,getRejectNode } from '/@/components/Process/api/todo'
import { message } from 'ant-design-vue'
const emit = defineEmits(['update:visible', 'success', 'error', 'close'])
const props = defineProps({
beforeFlowNode: {
type: Object,
default: () => ({})
},
assignee: { type: String, default: null },
userType: { type: String, default: '' },
deployId: { type: String, default: '' },
dataId: { type: String, default: '' },
})
// 审核结果配置
resultOptions: {
type: Array as () => ResultOption[],
default: () => [
const formSchemas: FormSchema[] = [
{
value: 'approved',
label: '通过',
icon: CheckCircleOutlined,
className: 'approved',
color: '#52c41a'
label: '审批结果',
field: 'reviewStatus',
component: 'Input',
slot: 'slot_passOrReturn',
required: true,
},
{
value: 'rejected',
label: '退回',
icon: CloseCircleOutlined,
className: 'rejected',
color: '#ff4d4f'
}
]
},
defaultResult: {
type: String,
default: 'approved'
},
// 评论配置
commentLabel: {
type: String,
default: '审核意见'
},
commentPlaceholder: {
type: String,
default: '请输入审核意见...'
},
commentRows: {
type: Number,
default: 4
},
commentMaxLength: {
type: Number,
default: 500
},
isCommentRequired: {
type: Boolean,
default: false
},
// 退回时是否强制要求评论
requireCommentOnReject: {
type: Boolean,
default: true
},
// 退回节点配置
showRejectNodeSelect: {
type: Boolean,
default: true
},
rejectNodes: {
type: Array as () => WorkflowNode[],
default: () => []
},
canSelectRejectNode: {
type: Boolean,
default: true
},
emptyRejectNodesTip: {
type: String,
default: '当前流程无可退回的历史节点'
},
// 附加信息配置
showAdditionalInfo: {
type: Boolean,
default: true
},
additionalInfoText: {
type: String,
default: '退回后,申请人需重新提交申请'
},
// 摘要配置
showSummary: {
type: Boolean,
default: true
},
summaryItems: {
type: Array as () => SummaryItem[],
default: () => []
label: '用户类型',
field: 'userType',
component: 'Input',
defaultValue: props.userType,
ifShow: false,
},
// 当前节点信息(用于默认摘要)
currentNode: {
type: Object as () => WorkflowNode | null,
default: null
},
currentUser: {
type: String,
default: ''
},
showCurrentTime: {
type: Boolean,
default: true
{
label: '审核意见',
field: 'comment',
component: 'InputTextArea',
required: true,
componentProps: {
allowClear: true,
showCount: true,
maxlength: 512,
autoSize: { minRows: 7, maxRows: 10 },
style: { width: '100%' }
}
},
// 初始数据
initialData: {
type: Object as () => {
result?: string
comment?: string
rejectNode?: string
[key: string]: any
{
label: '接收人',
field: 'approvalUser',
component: 'JSelectUser',
required: true,
componentProps: {
allowClear: true,
rowKey: 'username',
labelKey: 'realname',
showButton: false,
modalTitle: '用户',
style: { width: '100%' }
},
default: () => ({})
},
// 其他配置
disabled: {
type: Boolean,
default: false
{
label: '接收角色',
field: 'approvalRole',
component: 'RoleSelect',
required: true,
componentProps: {
allowClear: true,
maxSelectCount: 3,
isRadioSelection: false,
style: { width: '100%' }
},
readonly: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['update:approval-data', 'change'])
// 表单数据
const approvalForm = ref({
result: props.defaultResult as string,
comment: '',
rejectNode: ''
})
// 当前时间
const currentTime = ref(new Date())
// 退回节点选项
const rejectNodeOptions = computed(() => {
if (!props.rejectNodes || props.rejectNodes.length === 0) {
return []
}
return props.rejectNodes.map(node => ({
label: node.name,
value: node.id
}))
})
// 评论是否必填
const isCommentRequiredComputed = computed(() => {
if (props.isCommentRequired) return true
if (props.requireCommentOnReject && approvalForm.value.result === 'rejected') return true
return false
})
// 评论额外提示
const commentExtra = computed(() => {
if (isCommentRequiredComputed.value) {
return '此项为必填项'
}
return ''
})
// 默认摘要项
const defaultSummaryItems = computed<SummaryItem[]>(() => {
const items: SummaryItem[] = []
]
if (props.currentNode) {
items.push({
label: '当前节点',
value: props.currentNode.name
const [registerForm, { validate,updateSchema,setFieldsValue }] = useForm({
schemas: formSchemas,
showActionButtonGroup: false,
layout: 'vertical',
labelWidth: '120px',
size: 'small',
baseColProps: { span: 24 },
})
}
if (props.currentUser) {
items.push({
label: '处理人',
value: props.currentUser
const handleClose = () => {
emit('update:visible', false)
emit('close')
}
const handleSaveApp = async () => {
const formDataTmp = await validate()
const myTaskFlow = await getMyTaskFlow({ deploymentId: props.deployId, dataId: props.dataId })
if (myTaskFlow?.taskId) {
alert(JSON.stringify(myTaskFlow))
const reviewStatus = formDataTmp.reviewStatus;
if (reviewStatus === true) {
await complete({
instanceId: myTaskFlow.procInsId || '',
deployId: myTaskFlow.deployId || '',
taskId: myTaskFlow.taskId,
comment: formDataTmp.comment,
values: {
approval: formDataTmp.approvalUser || formDataTmp.approvalRole,
approvalType: formDataTmp.userType
},
})
}
if (props.showCurrentTime) {
items.push({
label: '待处理时间',
value: dayjs(currentTime.value).format('YYYY-MM-DD HH:mm:ss')
} else {
const rejectNode = await getRejectNode({
deployId: props.deployId,
taskId: myTaskFlow.taskId,
isApproved: false,
})
alert(JSON.stringify(rejectNode))
if (rejectNode?.targetNode) {
await complete({
instanceId: myTaskFlow.procInsId || '',
deployId: myTaskFlow.deployId || '',
taskId: myTaskFlow.taskId,
comment: formDataTmp.comment,
values: {
rejectNode: rejectNode.targetNode,
},
})
}
return items
})
// 最终的摘要项(合并默认和自定义)
const finalSummaryItems = computed(() => {
if (props.summaryItems.length > 0) {
return props.summaryItems
}
return defaultSummaryItems.value
})
// 获取审核数据
function getApprovalData() {
const data: any = {
result: approvalForm.value.result,
comment: approvalForm.value.comment
}
// 如果是退回且需要退回节点信息
if (approvalForm.value.result === 'rejected' && props.showRejectNodeSelect) {
const selectedNode = props.rejectNodes.find(
node => node.id === approvalForm.value.rejectNode
)
data.rejectNodeId = approvalForm.value.rejectNode
data.rejectNodeName = selectedNode?.name || ''
}
// 添加时间戳
data.approvalTime = new Date().toISOString()
return data
}
// 验证表单
async function validate(): Promise<{ valid: boolean; errors: string[] }> {
const errors: string[] = []
if (!approvalForm.value.result) {
errors.push('请选择审核结果')
}
if (isCommentRequiredComputed.value && !approvalForm.value.comment?.trim()) {
errors.push('请填写审核意见')
}
if (approvalForm.value.result === 'rejected' && props.showRejectNodeSelect) {
if (props.canSelectRejectNode && !approvalForm.value.rejectNode) {
errors.push('请选择退回节点')
}
}
return {
valid: errors.length === 0,
errors
}
}
// 重置表单
function resetForm() {
approvalForm.value = {
result: props.defaultResult,
comment: '',
rejectNode: props.rejectNodes[0]?.id || ''
}
// 如果有初始数据,则填充
if (props.initialData.result) {
approvalForm.value.result = props.initialData.result
approvalForm.value.comment = props.initialData.comment || ''
approvalForm.value.rejectNode = props.initialData.rejectNode || ''
}
}
// 设置表单数据
function setFormData(data: any) {
if (data.result !== undefined) {
approvalForm.value.result = data.result
}
if (data.comment !== undefined) {
approvalForm.value.comment = data.comment
}
if (data.rejectNode !== undefined) {
approvalForm.value.rejectNode = data.rejectNode
}
}
// 监听表单变化
watch(
approvalForm,
(newVal) => {
const approvalData = {
result: newVal.result,
comment: newVal.comment,
rejectNode: newVal.rejectNode
}
emit('update:approval-data', approvalData)
emit('change', approvalData)
},
{ deep: true }
)
// 监听初始数据变化
watch(
() => props.initialData,
(newData) => {
if (newData && Object.keys(newData).length > 0) {
setFormData(newData)
emit('success', props.dataId)
nextTick(() => {
message.success('任务发送成功')
handleClose()
})
}
},
{ deep: true, immediate: true }
)
// 监听可退回节点变化,设置默认退回节点
watch(
() => props.rejectNodes,
(nodes) => {
if (nodes && nodes.length > 0 && !approvalForm.value.rejectNode) {
approvalForm.value.rejectNode = nodes[0]?.id || ''
}
},
{ immediate: true }
)
// 启动定时器更新时间
let timer: ReturnType<typeof setInterval> | null = null
if (props.showCurrentTime) {
timer = setInterval(() => {
currentTime.value = new Date()
}, 1000)
}
// 组件卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer)
onMounted(async() => {
if (props.userType === 'user') {
updateSchema([{
field: 'approvalUser',
required: true,
ifShow: true,
},{
field: 'approvalRole',
required: false,
ifShow:false,
}])
} else {
updateSchema([{
field: 'approvalRole',
required: true,
ifShow: true,
},{
field: 'approvalUser',
required: false,
ifShow:false,
}])
}
})
// 暴露方法给父组件
defineExpose({
getApprovalData,
validate,
resetForm,
setFormData,
getFormRef: () => approvalForm
})
})
</script>
<style scoped lang="scss">
.approval-panel {
background-color: #fff;
border-left: 1px solid #e8eef2;
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: #f5f7fa;
.approval-card {
height: 100%;
display: flex;
flex-direction: column;
background-color: #fff;
.approval-header {
padding: 16px 20px;
:deep(.ant-card-head) {
padding: 0 24px;
background-color: #e9ecef;
border-bottom: 1px solid #e8eef2;
background-color: #fafbfc;
}
:deep(.ant-card-body) {
flex: 1;
padding: 24px;
overflow-y: auto;
}
}
.approval-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
padding: 16px 0;
.approval-title {
font-size: 16px;
......@@ -505,149 +231,53 @@ defineExpose({
.approval-content {
flex: 1;
overflow-y: auto;
padding: 20px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 2px;
&:hover {
background: #a8a8a8;
}
}
}
width: 100%;
.result-group {
display: flex;
gap: 16px;
.approval-radio-group {
width: 100%;
display: flex;
gap: 8px;
.ant-radio-wrapper {
.radio-button {
flex: 1;
:deep(.ant-radio) {
display: none;
}
:deep(.ant-radio + *) {
padding: 0;
}
.radio-content {
text-align: center;
height: 36px;
line-height: 36px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
border: 1px solid #d9d9d9;
border-radius: 8px;
transition: all 0.3s;
cursor: pointer;
.radio-icon {
font-size: 28px;
margin-bottom: 8px;
}
.radio-label {
font-size: 14px;
gap: 6px;
font-weight: 500;
}
}
&.approved .radio-content {
&:hover {
&:first-child:not(.ant-radio-button-wrapper-checked) {
border-color: #52c41a;
background-color: #f6ffed;
}
}
color: #52c41a;
&.rejected .radio-content {
&:hover {
border-color: #ff4d4f;
background-color: #fff2f0;
}
}
}
:deep(.ant-radio-wrapper-checked) {
.approved .radio-content {
border-color: #52c41a;
background-color: #f6ffed;
.radio-icon {
color: #52c41a;
border-color: #73d13d;
color: #73d13d;
}
}
.rejected .radio-content {
&:last-child:not(.ant-radio-button-wrapper-checked) {
border-color: #ff4d4f;
background-color: #fff2f0;
.radio-icon {
color: #ff4d4f;
}
}
}
}
.form-tip {
font-size: 12px;
color: #ff4d4f;
margin-top: 4px;
}
.additional-info {
margin-top: 16px;
.info-tip {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background-color: #fff7e6;
border-radius: 4px;
font-size: 12px;
color: #d46b00;
.anticon {
font-size: 14px;
}
&:hover {
border-color: #ff7875;
color: #ff7875;
}
}
.approval-summary {
background-color: #fafbfc;
border-radius: 8px;
padding: 12px;
.summary-item {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 13px;
&:last-child {
margin-bottom: 0;
&.ant-radio-button-wrapper-checked:first-child {
background: #52c41a;
border-color: #52c41a;
}
.summary-label {
color: #8c8c8c;
&.ant-radio-button-wrapper-checked:last-child {
background: #ff4d4f;
border-color: #ff4d4f;
}
.summary-value {
color: #262626;
font-weight: 500;
}
}
}
......
<template>
<div class="form-panel">
<a-card class="form-card" :bordered="false">
<template #title>
<div class="form-header">
<span class="form-title">当前待办</span>
<a-tag color="green" v-if="editableNode">可编辑</a-tag>
<span class="form-title">当前待办[{{ editableNode?.name || '无' }}]</span>
<a-button color="blue" v-if="!isEditStatus" @click="handleSetEditStatus">编辑</a-button>
<a-button color="blue" v-if="isEditStatus" @click="handleSetEditStatus">只读</a-button>
</div>
<div class="form-content" v-if="editableNode">
<a-card :title="editableNode.name" :bordered="false" class="current-form-card">
<template #extra>
<a-tag color="processing">待处理</a-tag>
</template>
<div class="form-content" v-if="editableNode">
<component
:is="getComponent(editableNode.formUrl)"
:ref="(el) => setFormRef(el, editableNode.id)"
......@@ -18,11 +18,11 @@
:current-flow-node="editableNode"
@update:form-data="handleFormDataUpdate"
/>
</a-card>
</div>
<div v-else class="empty-form-state">
<a-empty description="未找到可编辑的表单节点" />
</div>
</a-card>
</div>
</template>
......@@ -79,6 +79,8 @@ const modules = import.meta.glob('@/views/**/*.vue')
const formComponentRef = ref<FormComponentInstance | null>(null)
const formData = ref<any>({})
const isEditStatus = ref(false);
// 设置表单组件 ref
function setFormRef(el: any, nodeId: string) {
if (el) {
......@@ -250,6 +252,14 @@ function createErrorComponent(msg: string) {
}
}
// 处理编辑状态切换
function handleSetEditStatus() {
isEditStatus.value = !isEditStatus.value
if (formComponentRef.value&&typeof formComponentRef.value.formDisabled === 'function') {
//formComponentRef.value.formDisabled(isEditStatus.value)
}
}
// 暴露方法给父组件
defineExpose({
getFormData,
......@@ -266,16 +276,40 @@ defineExpose({
display: flex;
flex-direction: column;
overflow: hidden;
background-color: #f5f7fa;
.form-card {
height: 100%;
display: flex;
flex-direction: column;
background-color: #fff;
.form-header {
padding: 16px 24px;
:deep(.ant-card-head) {
padding: 0 24px;
background-color: #e9ecef;
border-bottom: 1px solid #e8eef2;
background-color: #fff;
min-height: auto;
.ant-card-head-wrapper {
height: auto;
}
}
:deep(.ant-card-body) {
flex: 1;
padding: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
}
.form-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
width: 100%;
padding: 16px 0;
.form-title {
font-size: 16px;
......@@ -308,28 +342,6 @@ defineExpose({
}
}
.current-form-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border: 1px solid #e8eef2;
:deep(.ant-card-head) {
background-color: #fafbfc;
border-bottom: 1px solid #e8eef2;
padding: 12px 20px;
.ant-card-head-title {
font-size: 15px;
font-weight: 500;
color: #096dd9;
}
}
:deep(.ant-card-body) {
padding: 24px;
}
}
.empty-form-state {
flex: 1;
display: flex;
......
<template>
<div class="flowchart-container">
<div v-show="loading" class="loading-state">
<a-spin tip="加载流程图中..." size="large" />
</div>
<div v-show="!loading" class="bpmn-wrapper">
<div class="bpmn-content">
<bpmn-viewer :flow-data="flowData" :procInsId="procInsId" />
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import { flowXmlAndNode } from '/@/components/Process/api/definition';
import BpmnViewer from '/@/components/Process/viewer/index.vue';
// Emits
const emit = defineEmits(['loaded']);
// 响应式数据
const loading = ref(false);
const flowData = ref<any>({});
const procInsId = ref('');
const loadData = async (data) => {
procInsId.value = data.procInsId || '';
const deployId = data.deployId || '';
try {
loading.value = true;
const res = await flowXmlAndNode({ procInsId: procInsId.value,deployId: deployId });
loading.value = false;
flowData.value = res;
} catch (error) {
console.error('加载流程图失败:', error);
message.error('加载流程图失败');
throw error;
} finally {
loading.value = false;
}
};
// 重置数据
const resetData = () => {
flowData.value = {};
};
// 暴露方法给父组件
defineExpose({
loadData,
resetData,
});
</script>
<style lang="scss" scoped>
.flowchart-container {
height: 100%;
padding: 24px;
background: #f5f7fa;
overflow: hidden;
}
.bpmn-wrapper {
height: 100%;
.bpmn-content {
height: 100%;
min-height: 500px;
border: 1px solid #e8e8e8;
border-radius: 6px;
overflow: hidden;
background: white;
}
}
.loading-state {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 300px;
margin-top: 20px;
}
@media (max-width: 768px) {
.flowchart-container {
padding: 16px;
}
}
</style>
\ No newline at end of file
<template>
<BasicDrawer v-bind="$attrs" title="流程追踪" width="100%"
@register="registerBasicDrawer"
:header-style="{ backgroundColor: '#018ffb', borderBottom: '1px solid #e8eef2' }">
<div class="drawer-content">
<FlowHistoryChart ref="refFlowHistoryChart" />
<FlowHistoryRecord :procInsId="procInsId" />
</div>
</BasicDrawer>
</template>
<script lang="ts" setup>
import { defineComponent,ref } from 'vue';
import { BasicDrawer, useDrawerInner } from '/@/components/Drawer';
import FlowHistoryChart from './FlowHistoryChart.vue';
import FlowHistoryRecord from './FlowHistoryRecord.vue';
const procInsId = ref('');
const dataId = ref('');
const deployId = ref('');
const refFlowHistoryChart = ref();
const callback = (data) => {
refFlowHistoryChart.value.loadData(data);
}
const [registerBasicDrawer, { closeDrawer, setDrawerProps }] = useDrawerInner(callback);
const handleCloseDrawer = () => {
closeDrawer();
}
</script>
\ No newline at end of file
<template>
<div class="record-container">
<div v-if="loading" class="loading-state">
<a-spin tip="加载流转记录中..." />
</div>
<div v-else-if="flowRecordList.length === 0" class="empty-state">
<a-empty description="暂无流转记录" />
</div>
<a-timeline v-else class="flow-timeline">
<a-timeline-item
v-for="(item, index) in flowRecordList"
:key="index"
:color="getTimelineColor(item.finishTime)"
>
<div class="timeline-item">
<div class="timeline-header">
<div class="task-name">
<a-tag :color="getTaskStatusColor(item.finishTime)" class="status-tag">
{{ item.finishTime ? '已完成' : '进行中' }}
</a-tag>
<span class="task-title">{{ item.taskName }}</span>
</div>
<div class="task-time">
<ClockCircleOutlined />
<span>{{ formatDuration(item.duration) }}</span>
</div>
</div>
<div class="timeline-content">
<a-descriptions size="small" :column="1" bordered>
<a-descriptions-item label="办理人" v-if="item.assigneeName">
<UserOutlined class="desc-icon" />
{{ item.assigneeName }}
<a-tag v-if="item.deptName" color="default" size="small">
{{ item.deptName }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="候选办理" v-if="item.candidate">
<TeamOutlined class="desc-icon" />
{{ item.candidate }}
</a-descriptions-item>
<a-descriptions-item label="接收时间">
<CalendarOutlined class="desc-icon" />
{{ formatTime(item.createTime) }}
</a-descriptions-item>
<a-descriptions-item label="处理时间" v-if="item.finishTime">
<CheckCircleOutlined class="desc-icon" />
{{ formatTime(item.finishTime) }}
</a-descriptions-item>
<a-descriptions-item label="处理意见" v-if="item.comment?.comment">
<FormOutlined class="desc-icon" />
<div class="comment-box">
{{ item.comment.comment }}
</div>
</a-descriptions-item>
</a-descriptions>
</div>
</div>
</a-timeline-item>
</a-timeline>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import {
UserOutlined,
CalendarOutlined,
ClockCircleOutlined,
FormOutlined,
TeamOutlined,
CheckCircleOutlined,
} from '@ant-design/icons-vue';
import { flowRecord } from '/@/components/Process/api/finished';
import dayjs from 'dayjs';
// Props
const props = defineProps<{
procInsId: string;
}>();
// 响应式数据
const loading = ref(false);
const flowRecordList = ref<any[]>([]);
// 方法
const formatTime = (time: string) => {
if (!time) return '';
return dayjs(time).format('YYYY-MM-DD HH:mm:ss');
};
const formatDuration = (duration: string) => {
if (!duration) return '';
return duration.replace('PT', '').toLowerCase();
};
const getTimelineColor = (finishTime: string | null) => {
return finishTime ? 'green' : 'blue';
};
const getTaskStatusColor = (finishTime: string | null) => {
return finishTime ? 'success' : 'processing';
};
const loadData = async (data) => {
try {
loading.value = true;
const res = await flowRecord({ procInsId: props.procInsId });
flowRecordList.value = res.flowList || [];
} catch (error) {
console.error('加载流转记录失败:', error);
message.error('加载流转记录失败');
throw error;
} finally {
loading.value = false;
}
};
// 重置数据
const resetData = () => {
flowRecordList.value = [];
};
// 暴露方法给父组件
defineExpose({
loadData,
resetData,
});
</script>
<style lang="scss" scoped>
.record-container {
height: 100%;
padding: 24px;
background: #f5f7fa;
overflow-y: auto;
}
.flow-timeline {
:deep(.ant-timeline-item) {
padding-bottom: 24px;
&:last-child {
padding-bottom: 0;
}
}
}
.timeline-item {
background: white;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
flex-wrap: wrap;
gap: 8px;
.task-name {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
.status-tag {
border-radius: 12px;
font-size: 12px;
padding: 2px 10px;
}
.task-title {
font-weight: 500;
color: #333;
}
}
.task-time {
color: #666;
font-size: 13px;
.anticon {
margin-right: 4px;
}
}
}
.timeline-content {
:deep(.ant-descriptions) {
border-radius: 6px;
overflow: hidden;
.ant-descriptions-item-label {
background: #fafafa;
font-weight: 500;
min-width: 80px;
}
.desc-icon {
margin-right: 8px;
color: #666;
}
}
}
.comment-box {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.6;
color: #333;
}
}
.loading-state,
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 300px;
}
@media (max-width: 768px) {
.record-container {
padding: 16px;
}
.timeline-item {
padding: 12px;
}
}
</style>
\ No newline at end of file
<template>
<div class="history-panel" :class="{ 'empty-history': readonlyNodes.length === 0 }">
<div class="history-panel-wrapper">
<a-card class="history-card-container" :bordered="false" :body-style="{ padding: 0, height: '100%', display: 'flex', flexDirection: 'column' }">
<template #title>
<div class="history-header">
<span class="history-title">历史节点</span>
<a-tag color="blue" v-if="readonlyNodes.length > 0">{{ readonlyNodes.length }}个节点</a-tag>
</div>
<div class="history-content" :class="{ 'has-scroll': readonlyNodes.length > 3 }">
</template>
<div class="history-content" :class="{ 'empty-history': readonlyNodes.length === 0, 'has-scroll': readonlyNodes.length > 3 }">
<div v-if="readonlyNodes.length === 0" class="empty-history-state">
<a-empty description="暂无历史节点" :image="simpleImage" />
</div>
......@@ -24,7 +27,7 @@
:title="node.name"
:bordered="false"
size="small"
class="history-card"
class="history-node-card"
:class="{ 'last-card': index === readonlyNodes.length - 1 }"
>
<template #extra>
......@@ -70,6 +73,7 @@
</a-timeline-item>
</a-timeline>
</div>
</a-card>
</div>
</template>
......@@ -314,7 +318,7 @@ watch(expandedPreviewId, async (newId, oldId) => {
</script>
<style scoped lang="scss">
.history-panel {
.history-panel-wrapper {
width: 50%;
background-color: #f5f7fa;
border-right: 1px solid #e8eef2;
......@@ -322,22 +326,35 @@ watch(expandedPreviewId, async (newId, oldId) => {
flex-direction: column;
overflow: hidden;
&.empty-history {
.history-content {
.history-card-container {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background-color: #f5f7fa;
:deep(.ant-card-head) {
padding: 0 20px;
background-color: #e9ecef; // 灰色背景,比整体背景稍深
border-bottom: 1px solid #e8eef2;
min-height: auto;
.ant-card-head-wrapper {
height: auto;
}
}
:deep(.ant-card-body) {
flex: 1;
overflow: hidden;
}
}
.history-header {
padding: 16px 20px;
border-bottom: 1px solid #e8eef2;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
width: 100%;
padding: 16px 0;
.history-title {
font-size: 16px;
......@@ -351,6 +368,12 @@ watch(expandedPreviewId, async (newId, oldId) => {
overflow-y: auto;
padding: 16px 12px;
&.empty-history {
display: flex;
align-items: center;
justify-content: center;
}
&.has-scroll {
padding-right: 8px;
}
......@@ -406,7 +429,7 @@ watch(expandedPreviewId, async (newId, oldId) => {
}
}
.history-card {
.history-node-card {
background-color: #fff;
border-radius: 8px;
margin-bottom: 8px;
......
<template>
<a-drawer
:title="title"
title="任务指派"
:visible="visible"
:width="width"
width="30%"
:closable="true"
:mask-closable="maskClosable"
:destroy-on-close="destroyOnClose"
:mask-closable="false"
:destroy-on-close="true"
:footer="null"
@close="handleClose"
class="task-assignee-drawer"
>
<div class="drawer-content">
<div class="assignee-section">
<a-card :title="assigneeTitle" :bordered="false" class="assignee-card">
<a-form layout="vertical" ref="formRef">
<a-card title="选择任务指派人" :bordered="false" class="assignee-card">
<a-form layout="vertical">
<a-form-item label="用户类型" required>
<a-radio-group v-model:value="localUserType" @change="handleUserTypeChange" disabled>
<a-radio-group v-model:value="localUserType" disabled>
<a-radio value="user">
<user-outlined /> 用户
<UserOutlined /> 用户
</a-radio>
<a-radio value="role">
<team-outlined /> 角色
<TeamOutlined /> 角色
</a-radio>
</a-radio-group>
</a-form-item>
<!-- 用户选择区域 - 未选择时显示选择器 -->
<!-- 用户/角色选择区域(未选择时) -->
<a-form-item
v-if="localUserType === 'user' && !userAssigneeId"
label="选择用户"
v-if="!hasAssignee"
:label="localUserType === 'user' ? '选择用户' : '选择角色'"
:required="required"
:validate-status="userValidateStatus"
:help="userValidateHelp"
:validate-status="validateStatus"
:help="validateHelp"
>
<div class="assignee-selector">
<a-input
:value="userName"
placeholder="点击'选择'按钮选择用户"
:value="assigneeDisplayName"
:placeholder="`点击'选择'按钮选择${localUserType === 'user' ? '用户' : '角色'}`"
readonly
class="assignee-input"
>
<template #suffix>
<a-button type="link" size="small" @click="handleSelectUser">选择</a-button>
<a-button type="link" size="small" @click="handleSelect">选择</a-button>
</template>
</a-input>
<a-button
v-if="userAssigneeId"
type="text"
size="small"
@click="clearAssignee"
class="clear-btn"
>
<close-circle-outlined />
</a-button>
</div>
</a-form-item>
<!-- 已指定用户信息展示 -->
<div v-if="localUserType === 'user' && userAssigneeId" class="assignee-info-wrapper">
<div class="assignee-info-label">已指定用户</div>
<!-- 已指定信息展示 -->
<div v-if="hasAssignee" class="assignee-info-wrapper">
<div class="assignee-info-label">已指定{{ localUserType === 'user' ? '用户' : '角色' }}</div>
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="用户ID">{{ userAssigneeId }}</a-descriptions-item>
<a-descriptions-item label="用户名称">{{ userName || '--' }}</a-descriptions-item>
<a-descriptions-item :label="localUserType === 'user' ? '用户ID' : '角色ID'">
{{ assigneeId }}
</a-descriptions-item>
<a-descriptions-item :label="localUserType === 'user' ? '用户名' : '角色名'">
{{ assigneeDisplayName || '--' }}
</a-descriptions-item>
</a-descriptions>
<!-- <a-button
type="link"
size="small"
@click="clearAssignee"
class="change-btn"
>
<a-button type="link" size="small" @click="clearAssignee" class="change-btn">
重新选择
</a-button> -->
</div>
<!-- 角色选择区域 - 未选择时显示选择器 -->
<a-form-item
v-if="localUserType === 'role' && !roleAssigneeId"
label="选择角色"
:required="required"
:validate-status="roleValidateStatus"
:help="roleValidateHelp"
>
<div class="assignee-selector">
<a-input
:value="roleName"
placeholder="点击'选择'按钮选择角色"
readonly
class="assignee-input"
>
<template #suffix>
<a-button type="link" size="small" @click="handleSelectRole">选择</a-button>
</template>
</a-input>
<a-button
v-if="roleAssigneeId"
type="text"
size="small"
@click="clearAssignee"
class="clear-btn"
>
<close-circle-outlined />
</a-button>
</div>
</a-form-item>
<!-- 已指定角色信息展示 -->
<div v-if="localUserType === 'role' && roleAssigneeId" class="assignee-info-wrapper">
<div class="assignee-info-label">已指定角色:</div>
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="角色ID">{{ roleAssigneeId }}</a-descriptions-item>
<a-descriptions-item label="角色名称">{{ roleName || '--' }}</a-descriptions-item>
</a-descriptions>
<!-- <a-button
type="link"
size="small"
@click="clearAssignee"
class="change-btn"
>
重新选择
</a-button> -->
</div>
</a-form>
<div class="assignee-actions">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button @click="handleClose">取消</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleConfirm">确认</a-button>
</a-space>
</div>
</a-card>
</div>
</div>
<RoleSelectModal
rowKey="id"
......@@ -152,306 +92,169 @@
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue'
import { ref, computed, watch,nextTick } from 'vue'
import { message } from 'ant-design-vue'
import { useModal } from '/@/components/Modal'
import {
UserOutlined,
TeamOutlined,
CloseCircleOutlined
} from '@ant-design/icons-vue'
import { UserOutlined, TeamOutlined } from '@ant-design/icons-vue'
import UserSelectModal from '/@/components/Form/src/jeecg/components/modal/UserSelectModal.vue'
import RoleSelectModal from '/@/components/Form/src/jeecg/components/modal/RoleSelectModal.vue'
import { complete, getMyTaskFlow } from '/@/components/Process/api/todo'
const props = defineProps({
// 抽屉基础配置
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '任务分配'
},
width: {
type: [Number, String],
default: 600
},
maskClosable: {
type: Boolean,
default: false
},
destroyOnClose: {
type: Boolean,
default: true
},
formData: {
type: Object,
default: () => ({})
},
// 初始分配数据
assignee: {
type: String,
default: null
},
// 分配组件配置
assigneeTitle: {
type: String,
default: '选择处理人'
},
deployId: {
type: String,
default: ''
},
userType: {
type: String,
default: 'user'
},
required: {
type: Boolean,
default: true
},
beforeFlowNode: {
type: Object,
default: () => ({})
},
currentFlowNode: {
type: Object,
default: () => ({})
},
nextFlowNode: {
type: Object,
default: () => ({})
}
visible: { type: Boolean, default: false },
formData: { type: Object, default: () => ({}) },
assignee: { type: String, default: null },
userType: { type: String, default: 'user' },
deployId: { type: String, default: '' },
required: { type: Boolean, default: true },
assigneeName: { type: String, default: null },
dataId: { type: String, default: '' }
})
const emit = defineEmits([
'update:visible',
'success',
'error',
'close',
'assignee-confirm'
])
const emit = defineEmits(['update:visible', 'success', 'error', 'close'])
const [registerSelUserModal, { openModal: userOpenModal }] = useModal()
const [registerSelRoleModal, { openModal: roleOpenModal }] = useModal()
// 状态
const localUserType = ref<'user' | 'role'>('user')
const userAssigneeId = ref('')
const userName = ref('')
const roleAssigneeId = ref('')
const roleName = ref('')
const assigneeId = ref('')
const assigneeDisplayName = ref('')
const confirmLoading = ref(false)
const drawerHistoryVisible = ref(false)
/**
* 初始化数据 - 修复核心问题
*/
const initData = () => {
console.log('initData called, assignee:', props.assignee, 'userType:', props.userType)
// 计算属性
const hasAssignee = computed(() => !!assigneeId.value)
const validateStatus = computed(() => {
if (!props.required || hasAssignee.value) return ''
return 'error'
})
// 清空现有数据
userAssigneeId.value = ''
userName.value = ''
roleAssigneeId.value = ''
roleName.value = ''
const validateHelp = computed(() =>
validateStatus.value === 'error' ? `请选择${localUserType.value === 'user' ? '用户' : '角色'}` : ''
)
// 初始化数据
const initData = () => {
assigneeId.value = ''
assigneeDisplayName.value = ''
if (props.assignee) {
// 根据 userType 决定显示类型
if (props.userType === 'role') {
localUserType.value = 'role'
roleAssigneeId.value = props.assignee
// 如果有 name 属性可以传入,但当前 props 没有,可以后续扩展
} else {
localUserType.value = 'user'
userAssigneeId.value = props.assignee
// 如果有 name 属性可以传入,但当前 props 没有,可以后续扩展
localUserType.value = props.userType === 'role' ? 'role' : 'user'
assigneeId.value = props.assignee
if (props.assigneeName) {
assigneeDisplayName.value = props.assigneeName
}
} else {
// 没有 assignee 时使用默认类型
localUserType.value = props.userType === 'role' ? 'role' : 'user'
}
console.log('initData result:', {
localUserType: localUserType.value,
userAssigneeId: userAssigneeId.value,
roleAssigneeId: roleAssigneeId.value
})
}
/**
* 设置用户信息(供外部调用,用于传入用户名)
*/
const setUserInfo = (userId: string, userNameValue: string) => {
userAssigneeId.value = userId
userName.value = userNameValue
localUserType.value = 'user'
const resetDrawer = () => {
localUserType.value = props.userType === 'role' ? 'role' : 'user'
assigneeId.value = ''
assigneeDisplayName.value = ''
}
/**
* 设置角色信息(供外部调用,用于传入角色名)
*/
const setRoleInfo = (roleId: string, roleNameValue: string) => {
roleAssigneeId.value = roleId
roleName.value = roleNameValue
localUserType.value = 'role'
const clearAssignee = () => {
assigneeId.value = ''
assigneeDisplayName.value = ''
}
// 表单验证状态
const userValidateStatus = computed(() => {
if (!props.required) return ''
if (localUserType.value !== 'user') return ''
return !userAssigneeId.value ? 'error' : ''
})
const userValidateHelp = computed(() =>
userValidateStatus.value === 'error' ? '请选择用户' : ''
)
const roleValidateStatus = computed(() => {
if (!props.required) return ''
if (localUserType.value !== 'role') return ''
return !roleAssigneeId.value ? 'error' : ''
})
const roleValidateHelp = computed(() =>
roleValidateStatus.value === 'error' ? '请选择角色' : ''
)
function handleUserTypeChange() {
clearAssignee()
}
function handleSelectUser() {
const handleSelect = () => {
if (localUserType.value === 'user') {
userOpenModal()
}
function handleSelectRole() {
} else {
roleOpenModal()
}
}
// 选择用户回调
function onSelectUserOk(options: any[], values: any[]) {
if (!values || values.length === 0) return
userAssigneeId.value = values[0]
userName.value = options[0]?.label || ''
const onSelectUserOk = (options: any[], values: any[]) => {
alert(JSON.stringify(options))
if (!values?.length) return
assigneeId.value = values[0]
assigneeDisplayName.value = options[0]?.label || ''
localUserType.value = 'user'
}
// 选择角色回调
function onSelectRoleOk(options: any[], values: any[]) {
if (!values || values.length === 0) return
roleAssigneeId.value = values[0]
roleName.value = options[0]?.label || ''
const onSelectRoleOk = (options: any[], values: any[]) => {
alert(JSON.stringify(options))
if (!values?.length) return
assigneeId.value = values[0]
assigneeDisplayName.value = options[0]?.label || ''
localUserType.value = 'role'
}
function clearAssignee() {
userAssigneeId.value = ''
userName.value = ''
roleAssigneeId.value = ''
roleName.value = ''
}
function handleCancel() {
handleClose()
const handleClose = () => {
emit('update:visible', false)
emit('close')
}
async function handleConfirm() {
// 验证
if (props.required) {
if (localUserType.value === 'user' && !userAssigneeId.value) {
message.error('请选择用户')
return
}
if (localUserType.value === 'role' && !roleAssigneeId.value) {
message.error('请选择角色')
const handleConfirm = async () => {
if (props.required && !assigneeId.value) {
message.error(`请选择${localUserType.value === 'user' ? '用户' : '角色'}`)
return
}
}
confirmLoading.value = true
try {
await handleSendTask()
setTimeout(() => {
const dataId = props.dataId?.id || ''
if (dataId && props.deployId) {
const myTaskFlow = await getMyTaskFlow({ deploymentId: props.deployId, dataId })
if (myTaskFlow?.taskId) {
await complete({
instanceId: myTaskFlow.procInsId || '',
deployId: myTaskFlow.deployId || '',
taskId: myTaskFlow.taskId,
comment: '',
values: {
approval: assigneeId.value,
approvalType: localUserType.value
},
})
}
}
emit('success', props.dataId)
nextTick(() => {
message.success('任务发送成功')
handleClose()
}, 500)
})
} catch (error: any) {
message.error(error?.message || '提交失败,请重试')
emit('error', error)
} finally {
confirmLoading.value = false
}
}
function handleClose() {
emit('update:visible', false)
emit('close')
}
function resetDrawer() {
localUserType.value = props.userType === 'role' ? 'role' : 'user'
userAssigneeId.value = ''
userName.value = ''
roleAssigneeId.value = ''
roleName.value = ''
}
}
function getAssigneeData() {
return {
// 对外暴露的方法
const getAssigneeData = () => ({
userType: localUserType.value,
assignee: localUserType.value === 'user' ? userAssigneeId.value : roleAssigneeId.value,
name: localUserType.value === 'user' ? userName.value : roleName.value,
assignee: assigneeId.value,
name: assigneeDisplayName.value,
remark: ''
}
})
const setUserInfo = (userId: string, userNameValue: string) => {
assigneeId.value = userId
assigneeDisplayName.value = userNameValue
localUserType.value = 'user'
}
const handleSendTask = async () => {
try {
const submitData = {
instanceId: "",
deployId: "",
taskId: "",
comment: '',
values: {},
approval: '',
approvalType: '',
}
const setRoleInfo = (roleId: string, roleNameValue: string) => {
assigneeId.value = roleId
assigneeDisplayName.value = roleNameValue
localUserType.value = 'role'
}
submitData.values['approval'] = localUserType.value === 'user' ? userAssigneeId.value : roleAssigneeId.value
submitData.values['approvalType'] = localUserType.value === 'user' ? 'user' : 'role'
const dataId = props.formData.id || ''
const openHistoryDrawer = () => {
drawerHistoryVisible.value = true;
};
if (dataId) {
try {
const myTaskFlow = await getMyTaskFlow({ deploymentId: props.deployId, dataId: dataId })
console.log('获取流程任务信息88888888888888888:', myTaskFlow)
if (myTaskFlow?.taskId) {
submitData.taskId = myTaskFlow.taskId
submitData.deployId = myTaskFlow.deployId
submitData.instanceId = myTaskFlow.procInsId
}
await complete(submitData)
emit('success', dataId)
message.success('任务发送成功')
} catch (e) {
console.warn('flowTaskInfo 获取 taskId 失败', e)
throw e // 重新抛出错误,让外层 catch 处理
}
} else {
// 没有 dataId 时的处理
emit('success', null)
message.success('操作成功')
}
} catch (error: any) {
console.error('handleSendTask error:', error)
throw error
}
}
// 监听 visible 变化
watch(() => props.visible, (newVal) => {
......@@ -462,14 +265,10 @@ watch(() => props.visible, (newVal) => {
}
}, { immediate: true })
// 监听 assignee 变化
watch(() => props.assignee, () => {
if (props.visible) {
initData()
}
if (props.visible) initData()
})
// 监听 userType 变化
watch(() => props.userType, () => {
if (props.visible && !props.assignee) {
localUserType.value = props.userType === 'role' ? 'role' : 'user'
......@@ -538,10 +337,7 @@ defineExpose({
}
}
.assignee-section {
height: 100%;
.assignee-card {
.assignee-card {
height: 100%;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
......@@ -559,13 +355,12 @@ defineExpose({
:deep(.ant-card-body) {
padding: 24px;
height: calc(100% - 57px);
overflow-y: auto;
display: flex;
flex-direction: column;
}
}
}
.assignee-selector {
.assignee-selector {
display: flex;
align-items: center;
gap: 8px;
......@@ -582,18 +377,9 @@ defineExpose({
}
}
}
}
.clear-btn {
color: #999;
font-size: 14px;
&:hover {
color: #ff4d4f;
}
}
}
.assignee-info-wrapper {
.assignee-info-wrapper {
margin-top: 12px;
padding: 12px;
background-color: #fafbfc;
......@@ -616,13 +402,12 @@ defineExpose({
width: 80px;
}
}
}
}
.assignee-actions {
.assignee-actions {
margin-top: auto;
padding-top: 24px;
text-align: center;
border-top: 1px solid #f0f0f0;
}
}
</style>
\ No newline at end of file
<template>
<div class="task-assignee-selector">
<a-card :title="title" :bordered="false" class="assignee-card">
<template #extra>
<a-tag :color="assigneeStatus.color">{{ assigneeStatus.text }}</a-tag>
</template>
<a-form layout="vertical">
<!-- 分配类型选择 -->
<a-form-item label="分配类型" required>
<a-radio-group v-model:value="formData.userType" @change="handleUserTypeChange">
<a-radio value="user">
<user-outlined /> 用户
</a-radio>
<a-radio value="role">
<team-outlined /> 角色
</a-radio>
</a-radio-group>
</a-form-item>
<!-- 用户选择区域 -->
<div v-if="formData.userType === 'user'" class="assignee-section">
<a-form-item
label="选择用户"
required
:validate-status="userValidateStatus"
:help="userValidateHelp"
>
<div class="assignee-selector">
<a-input
:value="formData.userName"
placeholder="点击'选择'按钮选择用户"
readonly
class="assignee-input"
>
<template #suffix>
<a-button type="link" size="small" @click="handleSelectUser">选择</a-button>
</template>
</a-input>
<a-button
v-if="formData.assignee"
type="text"
size="small"
@click="clearAssignee"
class="clear-btn"
>
<close-circle-outlined />
</a-button>
</div>
</a-form-item>
<div v-if="formData.assignee" class="assignee-info">
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="用户ID">{{ formData.assignee }}</a-descriptions-item>
<a-descriptions-item label="用户名称">{{ formData.userName }}</a-descriptions-item>
<a-descriptions-item label="用户类型"><a-tag color="blue">系统用户</a-tag></a-descriptions-item>
</a-descriptions>
</div>
</div>
<!-- 角色选择区域 -->
<div v-if="formData.userType === 'role'" class="assignee-section">
<a-form-item
label="选择角色"
required
:validate-status="roleValidateStatus"
:help="roleValidateHelp"
>
<div class="assignee-selector">
<a-input
:value="formData.roleName"
placeholder="点击'选择'按钮选择角色"
readonly
class="assignee-input"
>
<template #suffix>
<a-button type="link" size="small" @click="handleSelectRole">选择</a-button>
</template>
</a-input>
<a-button
v-if="formData.assignee"
type="text"
size="small"
@click="clearAssignee"
class="clear-btn"
>
<close-circle-outlined />
</a-button>
</div>
</a-form-item>
<div v-if="formData.assignee" class="assignee-info">
<a-descriptions :column="1" size="small" bordered>
<a-descriptions-item label="角色ID">{{ formData.assignee }}</a-descriptions-item>
<a-descriptions-item label="角色名称">{{ formData.roleName }}</a-descriptions-item>
<a-descriptions-item label="角色类型"><a-tag color="purple">系统角色</a-tag></a-descriptions-item>
</a-descriptions>
</div>
</div>
<!-- 备注信息 -->
<a-form-item label="备注">
<a-textarea
v-model:value="formData.remark"
placeholder="请输入备注信息(选填)"
:rows="3"
:maxlength="200"
show-count
/>
</a-form-item>
</a-form>
<div class="assignee-actions">
<a-space>
<a-button @click="handleCancel">取消</a-button>
<a-button type="primary" :loading="confirmLoading" @click="handleConfirm" :disabled="!isValid">确认</a-button>
</a-space>
</div>
</a-card>
<RoleSelectModal
rowKey="id"
@register="registerSelRoleModal"
@getSelectResult="onSelectOk"
isRadioSelection
:showButton="false"
labelKey="roleName"
/>
<UserSelectModal
rowKey="id"
@register="registerSelUserModal"
@getSelectResult="onSelectOk"
isRadioSelection
:showButton="false"
labelKey="realname"
/>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import { useModal } from '/@/components/Modal'
import {
UserOutlined,
TeamOutlined,
CloseCircleOutlined
} from '@ant-design/icons-vue'
import UserSelectModal from '/@/components/Form/src/jeecg/components/modal/UserSelectModal.vue'
import RoleSelectModal from '/@/components/Form/src/jeecg/components/modal/RoleSelectModal.vue'
interface AssigneeData {
userType: 'user' | 'role'
assignee: string
userName?: string
roleName?: string
remark?: string
}
const props = defineProps({
title: { type: String, default: '任务分配' },
procDefId: { type: String, required: true },
taskId: { type: String, required: true },
formData: { type: Object, default: () => ({}) },
initialAssignee: { type: Object as () => AssigneeData | null, default: null },
userType: { type: String, default: 'user' },
required: { type: Boolean, default: true }
})
const emit = defineEmits(['confirm', 'cancel', 'success', 'error'])
const [registerSelUserModal, { openModal: userOpenModal }] = useModal()
const [registerSelRoleModal, { openModal: roleOpenModal }] = useModal()
// 修复:使用函数初始化,确保能正确读取 props 值
const getInitialUserType = (): 'user' | 'role' => {
// 优先使用 initialAssignee 中的 userType
if (props.initialAssignee?.userType) {
return props.initialAssignee.userType
}
// 其次使用 props.userType
if (props.userType === 'role') {
return 'role'
}
return 'user'
}
// 表单数据
const formData = ref<AssigneeData>({
userType: getInitialUserType(),
assignee: props.initialAssignee?.assignee || '',
userName: props.initialAssignee?.userName || '',
roleName: props.initialAssignee?.roleName || '',
remark: props.initialAssignee?.remark || ''
})
const confirmLoading = ref(false)
const selectedItem = ref<any>(null)
// 验证状态
const userValidateStatus = computed(() => {
if (!props.required) return ''
return formData.value.userType === 'user' && !formData.value.assignee ? 'error' : ''
})
const userValidateHelp = computed(() => userValidateStatus.value === 'error' ? '请选择用户' : '')
const roleValidateStatus = computed(() => {
if (!props.required) return ''
return formData.value.userType === 'role' && !formData.value.assignee ? 'error' : ''
})
const roleValidateHelp = computed(() => roleValidateStatus.value === 'error' ? '请选择角色' : '')
// 分配状态
const assigneeStatus = computed(() => {
if (!formData.value.assignee) return { text: '未分配', color: 'default' }
if (formData.value.userType === 'user') {
return { text: `已分配用户: ${formData.value.userName}`, color: 'green' }
}
return { text: `已分配角色: ${formData.value.roleName}`, color: 'blue' }
})
const isValid = computed(() => !props.required || !!formData.value.assignee)
function handleUserTypeChange() {
clearAssignee()
}
function handleSelectUser() {
userOpenModal()
}
function handleSelectRole() {
roleOpenModal()
}
// 选择回调
function onSelectOk(options: any[], values: any[]) {
if (!values || values.length === 0) return
selectedItem.value = values[0]
const selectedOption = options[0]
if (formData.value.userType === 'user') {
formData.value.assignee = selectedItem.value.id
formData.value.userName = selectedOption.label
message.success(`已选择用户: ${formData.value.userName}`)
} else {
formData.value.assignee = selectedItem.value.id
formData.value.roleName = selectedOption.label
message.success(`已选择角色: ${formData.value.roleName}`)
}
}
function clearAssignee() {
formData.value.assignee = ''
formData.value.userName = ''
formData.value.roleName = ''
}
function handleCancel() {
emit('cancel')
}
async function handleConfirm() {
if (props.required && !formData.value.assignee) {
message.error(formData.value.userType === 'user' ? '请选择用户' : '请选择角色')
return
}
confirmLoading.value = true
try {
const assigneeData = {
userType: formData.value.userType,
assignee: formData.value.assignee,
name: formData.value.userType === 'user' ? formData.value.userName : formData.value.roleName,
remark: formData.value.remark
}
emit('confirm', assigneeData)
emit('success', assigneeData)
message.success('任务提交成功')
} catch (error: any) {
message.error(error?.message || '提交失败,请重试')
emit('error', error)
} finally {
confirmLoading.value = false
}
}
function getAssigneeData() {
return {
userType: formData.value.userType,
assignee: formData.value.assignee,
name: formData.value.userType === 'user' ? formData.value.userName : formData.value.roleName,
remark: formData.value.remark
}
}
function resetForm() {
formData.value = {
userType: 'user',
assignee: '',
userName: '',
roleName: '',
remark: ''
}
}
// 监听 initialAssignee 变化
watch(() => props.initialAssignee, (newVal) => {
if (newVal) {
formData.value = {
userType: newVal.userType || getInitialUserType(),
assignee: newVal.assignee || '',
userName: newVal.userName || '',
roleName: newVal.roleName || '',
remark: newVal.remark || ''
}
}
}, { immediate: true, deep: true })
// 修复:监听 userType prop 变化,确保 radio 能正确响应
watch(() => props.userType, (newVal) => {
if (newVal === 'role' || newVal === 'user') {
// 只有当当前没有分配人时才自动切换类型
if (!formData.value.assignee) {
formData.value.userType = newVal
}
}
}, { immediate: true }) // immediate: true 确保立即执行
defineExpose({
getAssigneeData,
resetForm,
validate: () => isValid.value
})
</script>
<style scoped lang="scss">
.task-assignee-selector {
height: 100%;
.assignee-card {
height: 100%;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
:deep(.ant-card-head) {
background-color: #fafbfc;
border-bottom: 1px solid #e8eef2;
.ant-card-head-title {
font-size: 16px;
font-weight: 500;
}
}
:deep(.ant-card-body) {
padding: 24px;
height: calc(100% - 57px);
overflow-y: auto;
display: flex;
flex-direction: column;
}
}
.assignee-section {
margin-bottom: 16px;
}
.assignee-selector {
display: flex;
align-items: center;
gap: 8px;
.assignee-input {
flex: 1;
:deep(.ant-input) {
background-color: #fafafa;
cursor: pointer;
&:hover {
background-color: #fff;
}
}
}
.clear-btn {
color: #999;
font-size: 14px;
&:hover {
color: #ff4d4f;
}
}
}
.assignee-info {
margin-top: 12px;
:deep(.ant-descriptions) {
.ant-descriptions-item-label {
background-color: #fafbfc;
width: 80px;
}
}
}
.assignee-actions {
margin-top: auto;
padding-top: 24px;
text-align: center;
border-top: 1px solid #f0f0f0;
}
}
</style>
\ No newline at end of file
......@@ -8,55 +8,57 @@
:header-style="{ backgroundColor: '#018ffb', borderBottom: '1px solid #e8eef2' }"
@close="handleClose"
class="workflow-form-drawer"
style="padding: 0px;"
:body-style="{ padding: '5px' }"
>
<div class="drawer-layout" :class="{ 'three-columns': showApprovalPanel }">
<HistoryPanel
<div class="drawer-container">
<a-row :gutter="10" class="drawer-row" :wrap="false">
<!-- 左侧历史面板 -->
<a-col :width="props.leftPanelWidth">
<HistoryPanel style="width: 100%;height: 100%;"
:readonly-nodes="readonlyNodes"
:data-id="dataId"
:show-history-form-data="props.showHistoryFormData"
/>
<div class="form-wrapper">
<CurrentFormPanel
</a-col>
<!-- 中间表单区域 -->
<a-col :width="props.centerPanelWidth">
<CurrentFormPanel style="width: 100%;height: 100%;"
ref="currentFormPanelRef"
:editable-node="editableNode"
:data-id="dataId"
:external-form-data="externalFormData"
:form-data="props.formData"
:form-bpm="formBpm"
:disabled="showApprovalPanel||false"
:disabled="showApprovalPanel || false"
@update:form-data="handleFormDataUpdate"
@form-mounted="handleFormMounted"
/>
</div>
<div v-if="showApprovalPanel" class="approval-wrapper">
<slot name="approval-panel" :approval-data="approvalData" :node="editableNode">
<ApprovalPanel
</a-col>
<!-- 右侧审核面板 -->
<a-col v-if="showApprovalPanel" :width="props.rightPanelWidth">
<ApprovalPanel style="width: 100%;height: 100%;"
ref="approvalPanelRef"
:title="approvalPanelTitle"
:current-node="editableNode"
:current-user="currentUser"
:reject-nodes="rejectNodes"
:can-select-reject-node="canSelectRejectNode"
:initial-data="initialApprovalData"
:show-summary="showApprovalSummary"
:result-options="approvalResultOptions"
:default-result="defaultApprovalResult"
:comment-placeholder="commentPlaceholder"
:require-comment-on-reject="requireCommentOnReject"
:show-reject-node-select="showRejectNodeSelect"
:show-approval-panel="true"
:deploy-id="props.deployId"
:data-id="props.dataId"
:assignee="props.assignee"
:user-type="props.userType"
@update:approval-data="handleApprovalDataUpdate"
@success="handleApprovalSuccess"
/>
</slot>
</div>
</div>
<template #footer>
<div class="drawer-footer">
</a-col>
</a-row>
</div>
<template #extra>
<a-button @click="openHistoryDrawer()" type="primary">流程追踪</a-button>
<a-button @click="handleClose()" type="primary">关闭抽屉</a-button>
</template>
<FlowHistoryDrawer @register="refFlowHistoryDrawer"/>
</a-drawer>
</template>
......@@ -66,12 +68,14 @@ import { message } from 'ant-design-vue'
import HistoryPanel from './HistoryPanel.vue'
import CurrentFormPanel from './CurrentFormPanel.vue'
import ApprovalPanel from './ApprovalPanel.vue'
import FlowHistoryDrawer from './FlowHistoryDrawer.vue';
import { useDrawer } from '/@/components/Drawer';
const formBpm = ref(true)
const formDisabled = ref(false)
const drawerHistoryVisible = ref(false)
const [refFlowHistoryDrawer, { openDrawer }] = useDrawer();
interface WorkflowNode {
id: string
......@@ -80,6 +84,7 @@ interface WorkflowNode {
formUrl?: string
formListUrl?: string
procDefId?: string
procInsId?: string
dataId?: string
processTime?: string
processor?: string
......@@ -126,10 +131,16 @@ const props = defineProps({
type: Object as () => Record<string, any>,
default: () => ({})
},
procDefId: {
deployId: {
type: String,
default: ''
},
procInsId: {
type: String,
default: ''
},
dataId: {
type: String,
default: ''
......@@ -144,14 +155,6 @@ const props = defineProps({
type: Boolean,
default: false
},
approvalPanelTitle: {
type: String,
default: '审核意见'
},
submitButtonText: {
type: String,
default: '提交'
},
// 审核结果配置
approvalResultOptions: {
......@@ -171,52 +174,27 @@ const props = defineProps({
}
]
},
defaultApprovalResult: {
type: String,
default: 'approved'
},
// 审核意见配置
commentPlaceholder: {
type: String,
default: '请输入审核意见...'
},
requireCommentOnReject: {
type: Boolean,
default: true
},
// 退回节点配置
showRejectNodeSelect: {
type: Boolean,
default: true
},
rejectNodes: {
flowNodes: {
type: Array as () => WorkflowNode[],
default: () => []
},
canSelectRejectNode: {
type: Boolean,
default: true
currentFlowNodeIdx: {
type: Number,
default: 0
},
assignee: { type: String, default: null },
userType: { type: String, default: 'user' },
// 面板宽度配置
leftPanelWidth: { type: String,default: '40%'},
centerPanelWidth: { type: String, default: '40%'},
rightPanelWidth: { type: String,default: '20%'}
// 其他配置
currentUser: {
type: String,
default: ''
},
initialApprovalData: {
type: Object as () => {
result?: string
comment?: string
rejectNode?: string
},
default: () => ({})
},
showApprovalSummary: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['update:visible', 'submit', 'close', 'form-data-update'])
......@@ -232,7 +210,6 @@ const approvalPanelRef = ref<InstanceType<typeof ApprovalPanel> | null>(null)
const drawerTitle = computed(() => props.title)
const drawerWidth = computed(() => props.width)
// 只读节点:索引小于 currentNodeIndex 的节点
const readonlyNodes = computed(() => {
if (!props.workflowNodes || props.workflowNodes.length === 0) {
......@@ -266,11 +243,18 @@ function handleApprovalDataUpdate(data: any) {
approvalData.value = data
}
// 处理表单组件挂载
function handleFormMounted({ nodeId, instance }: { nodeId: string; instance: any }) {
console.log('表单组件已挂载 - 节点:', nodeId)
}
// 处理审核成功
function handleApprovalSuccess(dataId: string) {
if (dataId === props.dataId) {
message.success('审核成功')
handleClose()
}
}
// 提交处理
async function handleSubmit() {
if (!editableNode.value) {
......@@ -283,7 +267,6 @@ async function handleSubmit() {
return
}
// 如果显示审核面板,验证审核数据
if (props.showApprovalPanel && approvalPanelRef.value) {
const result = await approvalPanelRef.value.validate()
if (!result.valid) {
......@@ -295,17 +278,12 @@ async function handleSubmit() {
submitLoading.value = true
try {
// 1. 表单验证
const isValid = await currentFormPanelRef.value.validateForm()
if (!isValid) {
message.error('请完善表单信息')
return
}
// 2. 获取表单数据
const submitData = await currentFormPanelRef.value.getFormData()
// 3. 构建提交数据
const finalSubmitData: any = {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
......@@ -346,13 +324,21 @@ function resetFormData() {
if (currentFormPanelRef.value) {
currentFormPanelRef.value.resetFormData()
}
if (approvalPanelRef.value) {
approvalPanelRef.value.resetForm()
if (approvalPanelRef.value&&props.showApprovalPanel) {
(approvalPanelRef.value as any).resetForm()
}
currentFormData.value = {}
approvalData.value = {}
}
const openHistoryDrawer = () => {
openDrawer(true,{
procInsId: props.procInsId,
dataId: props.dataId,
deployId: props.deployId,
} );
};
// 监听抽屉打开
watch(() => props.visible, async (newVal) => {
if (newVal) {
......@@ -413,8 +399,7 @@ defineExpose({
:deep(.ant-drawer-header) {
background-color: #f5f7fa;
border-bottom: 1px solid #e8eef2;
padding: 16px 24px;
padding: 10px 10px;
.ant-drawer-title {
font-size: 16px;
font-weight: 500;
......@@ -431,66 +416,53 @@ defineExpose({
}
:deep(.ant-drawer-body) {
padding: 0;
padding: 5px;
height: 100%;
overflow: hidden;
}
:deep(.ant-drawer-footer) {
padding: 12px 24px;
padding: 5px 10px;
border-top: 1px solid #e8eef2;
background-color: red;
background-color: #fff;
}
}
.drawer-layout {
display: flex;
.drawer-container {
height: 100%;
overflow: hidden;
background: #eef5f680;
padding: 1px;
margin: 0;
}
// 三栏布局
&.three-columns {
.history-panel {
width: 30%;
}
.form-wrapper {
width: 40%;
}
.approval-wrapper {
width: 30%;
}
}
.drawer-row {
height: 100%;
flex-wrap: nowrap;
background: #0c37a418;
}
// 两栏布局
&:not(.three-columns) {
.history-panel {
width: 40%;
}
.drawer-col {
height: 100%;
.form-wrapper {
flex: 1;
}
&:not(:last-child) {
margin-right: 16px;
}
}
.form-wrapper {
overflow: hidden;
display: flex;
flex-direction: column;
}
.panel-card {
height: 100%;
.approval-wrapper {
overflow: hidden;
:deep(.ant-card-body) {
height: 100%;
display: flex;
flex-direction: column;
}
}
.drawer-footer {
display: flex;
justify-content: flex-end;
height: 2px;
background: #018ffb;
gap: 12px;
}
</style>
\ No newline at end of file
......@@ -206,7 +206,7 @@ if (key === '3') {
deployId: taskForm.deployId,
}).then((res) => {
flowData.value = res
console.log("xml",JSON.stringify(res))
console.log("xml88888888888888888",JSON.stringify(res))
})
}
}
......
......@@ -92,6 +92,7 @@ const handleClick = (key: string) => {
flowXmlAndNode({ deployId: deployId.value }).then(res => {
flowData.value = res
})
}
}
......
......@@ -36,6 +36,7 @@
<StProblemCheckArchiveModal @register="registerModal" @success="handleSuccess"></StProblemCheckArchiveModal>
<!-- 审批记录 -->
<BpmPictureModal @register="registerBpmModal" />
</div>
</template>
......@@ -56,9 +57,13 @@
const [registerBpmModal, { openModal: bpmPicModal }] = useModal();
const queryParam = reactive<any>({});
//注册model
const refFlowHistory = ref();
const deployId = ref('');
const dataId = ref('');
// 审批记录抽屉
const drawerHistoryVisible = ref(false);
const [registerModal, {openModal}] = useModal();
//注册table数据
const { prefixCls,tableContext,onExportXls,onImportXls } = useListPage({
tableProps:{
title: '问题归档',
......@@ -102,31 +107,20 @@
})
const [registerTable, {reload},{ rowSelection, selectedRowKeys }] = tableContext
// 高级查询配置
const superQueryConfig = reactive(superQuerySchema);
/**
* 高级查询事件
*/
function handleSuperQuery(params) {
Object.keys(params).map((k) => {
queryParam[k] = params[k];
});
reload();
}
/**
* 新增事件
*/
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
});
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
......@@ -134,9 +128,6 @@
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
......@@ -144,39 +135,40 @@
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({id: record.id}, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ids: selectedRowKeys.value}, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
}
/**
* 操作栏
*/
function handleShowHistory(record) {
drawerHistoryVisible.value = true;
deployId.value = record.deployId;
dataId.value = record.id;
refFlowHistory.value.iniData(record);
}
function getTableAction(record){
return [
{
label: '编辑',
onClick: handleEdit.bind(null, record),
auth: 'problem:st_problem_check_archive:edit'
},
{
label: '查看流程',
onClick: handleShowHistory.bind(null, record),
}
]
}
/**
* 下拉操作栏
*/
function getDropDownAction(record){
let dropDownAction = [
{
......@@ -191,47 +183,11 @@
},
auth: 'problem:st_problem_check_archive:delete'
},
{
label: '审批进度',
onClick: handlePreviewPic.bind(null, record),
ifShow: !!record.bpmStatus && record.bpmStatus !== '1',
}
];
if(record.bpmStatus == '1'){
dropDownAction.push({
label: '发起流程',
popConfirm: {
title: '确认提交流程吗?',
confirm: handleProcess.bind(null, record),
placement: 'topLeft',
}
})
}
return dropDownAction;
}
/**
* 提交流程
*/
async function handleProcess(record) {
let params = {
flowCode: 'dev_st_problem_check_archive_001',
id: record.id,
formUrl: 'problem/components/StProblemCheckArchiveForm',
formUrlMobile: ''
}
await startProcess(params);
handleSuccess();
}
/**
* 审批进度
*/
async function handlePreviewPic(record) {
bpmPicModal(true, {
flowCode: 'dev_st_problem_check_archive_001',
dataId: record.id,
});
}
......
......@@ -25,7 +25,7 @@
import { list,problemArchive} from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
const emit = defineEmits(['callback'])
const emit = defineEmits(['callback','openMultiForm'])
const props = defineProps({
beforeFlowNode: {
......
......@@ -187,10 +187,9 @@ export const formSchema: FormSchema[] = [
component: 'JEditor',
required: true,
componentProps: {
rows: 4,
showCount: true,
maxlength: 30000,
height: 300,
maxlength: 512,
height: 220,
},
},
{
......
......@@ -52,7 +52,7 @@
}
})
const emit = defineEmits(['callback','startWorkFlow','sendWorkFlow'])
const emit = defineEmits(['callback','startWorkFlow','sendWorkFlow','openMultiForm'])
//注册model
const [registerModal, { openModal }] = useModal();
......@@ -182,8 +182,21 @@
})
}
async function handleStartUpdate(flowData) {
let record = {
procInsId: flowData.procInsId,
id:flowData.dataId
}
await saveOrUpdate(record,true).then(res => {
handleSuccess(null);
})
}
defineExpose({
handleUpdate,
handleStartUpdate
})
</script>
......
......@@ -25,7 +25,7 @@
import { list, saveOrUpdate } from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
const emit = defineEmits(['callback','sendWorkFlow'])
const emit = defineEmits(['callback','sendWorkFlow','openMultiForm'])
const props = defineProps({
beforeFlowNode: {
......@@ -64,7 +64,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handleExecuteApproval(record: Recordable) {
emit("callback",record)
emit("openMultiForm",record)
}
function handleSuccess() {
......
......@@ -25,7 +25,7 @@
import { list, saveOrUpdate } from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
const emit = defineEmits(['callback','sendWorkFlow'])
const emit = defineEmits(['callback','sendWorkFlow','openMultiForm'])
const props = defineProps({
beforeFlowNode: {
......@@ -64,7 +64,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handleExecute(record: Recordable) {
emit("callback",record)
emit("openMultiForm",record)
}
function handleSuccess() {
......
......@@ -11,16 +11,12 @@
:currentFlowNode="node"
:nextFlowNode="workflowNodes[index+1]"
@open-multi-form="handleOpenMultiForm"
@callback="handleCallback"
/>
</div>
<div v-else class="no-form">
该节点未配置表单
</div>
</a-tab-pane>
<template #tabBarExtraContent>
<a-button type="primary">查看流程图</a-button>
</template>
</a-tabs>
<WorkFlowFormDrawer
v-model:visible="drawerVisible"
......@@ -29,34 +25,29 @@
:workflow-nodes="workflowNodes"
:external-form-data="externalFormData"
:proc-def-id="currentProcDefId"
:proc-ins-id="procInsId"
:show-history-form-data="true"
:data-id="dataId"
:deploy-id="deployId"
:show-approval-panel="isShowApprovalPanel"
@submit="handleMultiFormSubmit"
@close="handleDrawerClose"
@form-data-update="handleMultiFormDataUpdate"
width="90%"
/>
<TaskAssigneeDrawer
v-model:visible="drawerTaskVisible"
:task-id="taskId"
:task-name="taskName"
:proc-def-id="procDefId"
:proc-def-name="procDefName"
:show-task-info="true"
:show-form-data="true"
:assignee-title="assigneeTitle"
:user-type="userType"
:assignee="assignee"
:deploy-id="deployId"
:user-type-options="userTypeOptions"
:custom-next-api="customNextApi"
:form-data="formData"
:data-id="dataId"
@success="handlSendSuccess"
@error="handleError"
/>
</div>
</template>
......@@ -66,7 +57,7 @@
import { definitionStart, definitionStartByDeployId,addMyTaskFlow } from "/@/components/Process/api/definition";
import WorkFlowFormDrawer from '/@/views/common/WorkFlowFormDrawer.vue';
import TaskAssigneeDrawer from '/@/views/common/TaskAssigneeDrawer.vue'
import { CONNREFUSED } from 'dns';
const formTableName = "st_problem_check";
const workflowNodes = ref<any[]>([]);
......@@ -75,33 +66,23 @@
const currentNode = ref<any>({});
const isShowApprovalPanel = ref(true);
const deployId = ref('');
const procInsId = ref('');
// 改为动态 ref 对象,存储每个节点的组件实例
const formComponentRefs = ref<Map<number, any>>(new Map());
// 任务指派抽屉相关状态
const drawerTaskVisible = ref(false);
const taskId = ref('')
const taskName = ref('')
const procDefId = ref('')
const procDefName = ref('')
const assignee = ref<any>("");
const assigneeTitle = ref<any>("");
const userType = ref<any>("");
const formData = ref<any>({});
const userTypeOptions = ref<any>([
{ value: 'user', label: '用户' },
{ value: 'role', label: '角色' }
]);
const customNextApi = ref<any>(null);
// 抽屉相关状态
const drawerVisible = ref(false);
const drawerTitle = ref('表单处理');
const currentMultiFormIndex = ref(0);
const currentProcDefId = ref('');
const externalFormData = ref<Record<string, any>>({});
const componentCache = new Map();
const modules = import.meta.glob('@/views/**/*.vue');
......@@ -127,10 +108,7 @@
}
function loadComponent(url: string) {
console.log('开始加载组件,URL:', url);
if (componentCache.has(url)) {
console.log('从缓存加载组件:', url);
return componentCache.get(url);
}
let componentPath = '';
......@@ -142,13 +120,9 @@
if (!componentPath.match(/\.(vue|js|ts|jsx|tsx)$/)) {
componentPath += '.vue';
}
console.log('生成的组件路径:', componentPath);
console.log('所有可用的组件路径示例:', Object.keys(modules).slice(0, 10));
let loader = modules[componentPath];
if (!loader) {
console.error('未找到组件:', componentPath);
console.log('包含 problemCheck 的路径:', Object.keys(modules).filter(key => key.includes('problemCheck')));
const ErrorComponent = {
render: () => h('div', { style: 'color: red; padding: 20px;' }, `组件未找到: ${componentPath}`)
};
......@@ -175,17 +149,18 @@
}
const handleDefinitionStart = async (data) => {
const formData = { dataId:data.id, dataName: 'id' };
const deployId = currentNode.value.deployId || '';
const startResRaw = await definitionStartByDeployId(deployId, formData);
alert(JSON.stringify(startResRaw))
let myTaskFlow = {}
if (startResRaw?.procInsId) {
procInsId.value = startResRaw.procInsId;
myTaskFlow["taskId"] = startResRaw.taskId;
myTaskFlow["deployId"] = startResRaw.deployId;
myTaskFlow["procInsId"] = startResRaw.instanceId;
myTaskFlow["procInsId"] = startResRaw.procInsId;
myTaskFlow["executionId"] = startResRaw.executionId;
myTaskFlow["procDefId"] = startResRaw.procInsId;
myTaskFlow["procDefId"] = startResRaw.instanceId ;
myTaskFlow["targetId"] = data.id;
myTaskFlow["taskDefinitionKey"] = currentNode.value.id;
myTaskFlow["formTableName"] = formTableName;
......@@ -200,77 +175,88 @@
}
}
await addMyTaskFlow(myTaskFlow);
const currentFormComponent = getCurrentFormComponent();
if (currentFormComponent && typeof currentFormComponent.handleUpdate === 'function') {
currentFormComponent.handleStartUpdate({
dataId:data.id,
procInsId: startResRaw.procInsId,
});
} else {
console.warn('当前组件实例不存在或没有 handleUpdate 方法');
}
}
}
const handleDefinitionSend = async (data) => {
drawerTaskVisible.value = true;
const nextNode = workflowNodes.value[activeTab.value];
if (nextNode?.assignee) {
assignee.value = nextNode.assignee;
} else {
assignee.value = '';
}
dataId.value = data.id;
deployId.value = currentNode.value.deployId || '';
deployId.value = nextNode?.deployId || '';
const attributes = nextNode.value?.attributes || {};
const userTypes = attributes.userType || [];
if (userTypes.length > 0) {
userType.value = userTypes[0].value || '';
}
formData.value = data;
await setNextNodeUser();
}
const handleOpenMultiForm = (params: {
nodeIndex?: number;
title?: string;
procDefId?: string;
formData?: Record<string, any>;
}) => {
const setNextNodeUser = async () => {
const nextNode = workflowNodes.value[activeTab.value];
const attributes = nextNode.attributes || {};
const userTypes = attributes.userType || [];
if (params.nodeIndex !== undefined) {
currentMultiFormIndex.value = params.nodeIndex;
} else {
currentMultiFormIndex.value = activeTab.value - 1;
if (userTypes.length > 0) {
userType.value = userTypes[0].value || '';
if(userType.value==="role"){
if(nextNode.candidateGroups){
assignee.value = nextNode.candidateGroups[0].id;
}
if (params.title) {
drawerTitle.value = params.title;
} else {
const currentNode = workflowNodes.value[currentMultiFormIndex.value];
drawerTitle.value = currentNode ? `${currentNode.name} - 历史表单查看` : '表单处理';
assignee.value = nextNode.assignee;
}
if (params.procDefId) {
currentProcDefId.value = params.procDefId;
} else if (workflowNodes.value[currentMultiFormIndex.value]) {
currentProcDefId.value = workflowNodes.value[currentMultiFormIndex.value].procDefId || '';
}
if (params.formData) {
externalFormData.value = params.formData;
} else {
externalFormData.value = {};
}
drawerVisible.value = true;
};
// const handleOpenMultiForm = (params: {
// nodeIndex?: number;
// title?: string;
// procDefId?: string;
// formData?: Record<string, any>;
// }) => {
// if (params.nodeIndex !== undefined) {
// currentMultiFormIndex.value = params.nodeIndex;
// } else {
// currentMultiFormIndex.value = activeTab.value - 1;
// }
// if (params.title) {
// drawerTitle.value = params.title;
// } else {
// const currentNode = workflowNodes.value[currentMultiFormIndex.value];
// drawerTitle.value = currentNode ? `${currentNode.name} - 历史表单查看` : '表单处理';
// }
// if (params.procDefId) {
// currentProcDefId.value = params.procDefId;
// } else if (workflowNodes.value[currentMultiFormIndex.value]) {
// currentProcDefId.value = workflowNodes.value[currentMultiFormIndex.value].procDefId || '';
// }
// if (params.formData) {
// externalFormData.value = params.formData;
// } else {
// externalFormData.value = {};
// }
// drawerVisible.value = true;
// };
const handleMultiFormSubmit = async (submitData: {
nodeId: string;
nodeName: string;
formData: any;
procDefId: string;
nodeId: string;nodeName: string;formData: any;procDefId: string;
}) => {
console.log('多表单提交数据:', submitData);
try {
await definitionStart(
currentProcDefId.value,
{
currentProcDefId.value,{
...submitData.formData
}
);
drawerVisible.value = false;
const currentTabKey = activeTab.value;
const currentComponent = loadComponent(workflowNodes.value[currentTabKey - 1]?.formListUrl);
const currentComponent = loadComponent(workflowNodes.value[ activeTab.value - 1]?.formListUrl);
} catch (error) {
console.error('提交失败:', error);
throw error;
......@@ -286,17 +272,6 @@
// 可以在这里实时保存数据到本地
};
const openHistoryForms = (nodeIndex: number, formData?: Record<string, any>) => {
handleOpenMultiForm({
nodeIndex,
title: `查看历史表单 - ${workflowNodes.value[nodeIndex]?.name || ''}`,
formData
});
};
function handleSuccess(response: any) {
console.log('任务处理成功:', response)
}
function handlSendSuccess(dataId: any) {
const currentFormComponent = getCurrentFormComponent();
......@@ -312,27 +287,32 @@
}
defineExpose({
openHistoryForms,
openMultiForm: handleOpenMultiForm
});
async function handleOpenMultiForm(data: any) {
deployId.value = currentNode.value.deployId || '';
procInsId.value = data.procInsId || '';
drawerVisible.value = true;
dataId.value = data.id || '';
currentNode.value = workflowNodes.value[currentMultiFormIndex.value];
currentProcDefId.value = currentNode.value.procDefId || '';
const isApprovalNode = JSON.parse(currentNode.value["isApprove"] || 'false');
if(isApprovalNode) {
isShowApprovalPanel.value = true;
} else {
isShowApprovalPanel.value = false;
}
await setNextNodeUser();
}
onMounted(async () => {
onMounted( async () => {
await nextTick();
try {
const nodes = await getNodesByTableName(formTableName);
workflowNodes.value = nodes;
console.log('获取到的节点:', workflowNodes.value);
workflowNodes.value.forEach((node, index) => {
console.log(`节点${index + 1}:`, node.name, 'formListUrl:', node.formListUrl);
});
if (workflowNodes.value && workflowNodes.value.length > 0) {
console.log('开始预加载组件...');
workflowNodes.value.forEach(node => {
if (node.formListUrl) {
console.log('预加载组件 URL:', node.formListUrl);
loadComponent(node.formListUrl);
}
});
......@@ -343,21 +323,7 @@
}
});
function handleCallback(data: any) {
drawerVisible.value = true;
dataId.value = data.id || '';
currentNode.value = workflowNodes.value[currentMultiFormIndex.value];
currentProcDefId.value = currentNode.value.procDefId || '';
const isApprovalNode = JSON.parse(currentNode.value["isApprove"] || 'false');
if(isApprovalNode) {
isShowApprovalPanel.value = true;
} else {
isShowApprovalPanel.value = false;
}
}
</script>
<style scoped>
......
......@@ -40,7 +40,7 @@
}
})
const emit = defineEmits(['callback','sendWorkFlow'])
const emit = defineEmits(['callback','sendWorkFlow','openMultiForm'])
//注册model
const [registerExecuteModal, { openModal: openExecuteModal }] = useModal();
......@@ -64,7 +64,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handlePlanApproval(record: Recordable) {
emit("callback",record)
emit("openMultiForm",record)
}
function handleSuccess() {
......
......@@ -25,7 +25,7 @@
import { list,saveOrUpdate} from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
const emit = defineEmits(['callback','sendWorkFlow'])
const emit = defineEmits(['callback','sendWorkFlow','openMultiForm'])
const props = defineProps({
beforeFlowNode: {
......@@ -62,7 +62,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handlePlan(record: Recordable) {
emit("callback",record)
emit("openMultiForm",record)
}
async function handleSendNext(record: Recordable) {
......@@ -87,7 +87,6 @@
}
async function handleUpdate(dataId) {
//alert(dataId)
let record = {
bmpNodeId: props.nextFlowNode.id,
deployId: props.nextFlowNode.deployId,
......
......@@ -14,6 +14,7 @@
import { propTypes } from '/@/utils/propTypes';
import { getExecuteFormSchema } from '../StProblemCheck.data';
import { saveOrUpdate, } from '../StProblemCheck.api';
import { useMessage } from '/@/hooks/web/useMessage';
export default defineComponent({
name: 'StProblemCheckForm',
......@@ -27,7 +28,8 @@
disabled: propTypes.bool.def(false),
},
setup(props) {
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
const { createMessage } = useMessage();
const [registerForm, { validate, setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getExecuteFormSchema(props.formData),
showActionButtonGroup: false,
......@@ -53,7 +55,16 @@
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
await saveOrUpdate(params, true);
const result = await saveOrUpdate(params, true);
if (result && result.id) {
await initFormData(result.id);
createMessage.success('保存成功!');
//emit('save-success', result);
} else {
await initFormData(props.dataId);
createMessage.success('保存成功!');
}
}
initFormData(props.dataId);
......
......@@ -28,7 +28,7 @@
},
setup(props) {
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
labelWidth: 100,
schemas: getBpmFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: { span: 24 },
......
<template>
<div style="min-height: 400px">
<div style="height: 100%; display: flex; flex-direction: column">
<a-card :style="{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'auto' }" :bordered="false" :body-style="{ flex: 1, display: 'flex', flexDirection: 'column' }">
<!-- 表单内容区域 -->
<div style="flex: 1; overflow: auto">
<BasicForm @register="registerForm" />
<div style="width: 100%; text-align: center" v-if="!formDisabled">
<a-button @click="submitForm" pre-icon="ant-design:check" type="primary">提 交</a-button>
</div>
<!-- 底部按钮区域 -->
<div style="text-align: center;" v-if="!formDisabled">
<a-space>
<a-button block @click="saveForm" pre-icon="ant-design:save" type="primary" ghost>保 存</a-button>
<a-button block @click="saveAndSendForm" pre-icon="ant-design:send" type="primary" ghost>保存并发送</a-button>
</a-space>
</div>
</a-card>
</div>
</template>
<script lang="ts">
import { BasicForm, useForm } from '/@/components/Form/index';
import { computed, defineComponent } from 'vue';
import { computed, defineComponent, ref } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import { getPlanFormSchema } from '../StProblemCheck.data';
import { saveOrUpdate, } from '../StProblemCheck.api';
import { saveOrUpdate } from '../StProblemCheck.api';
import { useMessage } from '/@/hooks/web/useMessage';
export default defineComponent({
name: 'StProblemCheckForm',
......@@ -26,9 +37,11 @@
dataId: propTypes.string.def(''),
disabled: propTypes.bool.def(false),
},
setup(props) {
emits: ['save-success', 'send-success'],
setup(props, { emit }) {
const { createMessage } = useMessage();
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
labelWidth: 100,
schemas: getPlanFormSchema(props.formData),
showActionButtonGroup: false,
baseColProps: { span: 24 },
......@@ -41,32 +54,94 @@
return true;
});
let formData = {};
let currentFormData = ref({});
const queryById = '/problem/stProblemCheck/queryById';
async function initFormData(did) {
let params = { id: props.dataId||did };
let params = { id: props.dataId || did };
const data = await defHttp.get({ url: queryById, params });
formData = { ...data };
await setFieldsValue(formData);
currentFormData.value = { ...data };
await setFieldsValue(currentFormData.value);
await setProps({ disabled: formDisabled.value });
}
async function submitForm() {
// 保存按钮
async function saveForm() {
try {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
await saveOrUpdate(params, true);
alert(1)
let params = Object.assign({}, currentFormData.value, data);
const result = await saveOrUpdate(params, true);
// 保存成功后更新表单
if (result && result.id) {
await initFormData(result.id);
createMessage.success('保存成功!');
emit('save-success', result);
} else {
await initFormData(props.dataId);
createMessage.success('保存成功!');
}
} catch (error) {
createMessage.error('保存失败,请重试!');
console.error('保存失败:', error);
}
}
// 保存并发送按钮
async function saveAndSendForm() {
try {
let data = getFieldsValue();
let params = Object.assign({}, currentFormData.value, data);
const result = await saveOrUpdate(params, true);
// 保存成功后更新表单
if (result && result.id) {
await initFormData(result.id);
createMessage.success('保存成功!');
// TODO: 调用发送接口
// await sendData(result.id);
createMessage.success('发送成功!');
emit('send-success', result);
} else {
await initFormData(props.dataId);
createMessage.success('保存成功!');
// TODO: 调用发送接口
createMessage.success('发送成功!');
}
} catch (error) {
createMessage.error('保存失败,请重试!');
console.error('保存失败:', error);
}
}
initFormData(props.dataId);
const setFormDisabledStatus = (isEdit: boolean) => {
if (props.disabled === false) {
return false;
}
return true;
};
return {
setFormDisabledStatus,
registerForm,
formDisabled,
submitForm,
saveForm,
saveAndSendForm,
initFormData
};
},
});
</script>
<style scoped>
/* 确保父容器高度100% */
:deep(.ant-card-body) {
flex: 1;
display: flex;
flex-direction: column;
padding: 24px;
}
</style>
\ No newline at end of file
......@@ -16,6 +16,7 @@ import org.jeecg.modules.flowable.apithird.business.entity.FlowForm;
import org.jeecg.modules.flowable.apithird.business.entity.SysDeployForm;
import org.jeecg.modules.flowable.apithird.business.service.IFlowFormService;
import org.jeecg.modules.flowable.apithird.business.service.ISysDeployFormService;
import org.jeecg.modules.flowable.service.IFlowRejectService;
import org.jeecg.modules.stm.my.entity.MyTaskFlow;
import org.jeecg.modules.stm.my.service.IMyTaskFlowService;
import org.jeecg.modules.stm.utils.UserUtil;
......@@ -23,25 +24,23 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;
import java.util.stream.Collectors;
import org.flowable.task.api.Task;
@Slf4j
@RestController
@RequestMapping("/flowable/form")
public class FlowFormController {
@Autowired
private IFlowFormService flowFormService;
@Autowired
private ISysDeployFormService sysDeployFormService;
@Autowired
private RepositoryService repositoryService;
@Autowired
private IMyTaskFlowService myTaskFlowService;
@Autowired
private IFlowRejectService flowRejectService;
@RequestMapping("/list")
public Result<IPage<FlowForm>> getFormList(
......@@ -237,6 +236,9 @@ public class FlowFormController {
nodeInfo.put("isApprove",isApprove);
String formKey = ((UserTask) element).getFormKey();
nodeInfo.put("assignee", userTask.getAssignee());
nodeInfo.put("candidateGroups", userTask.getCandidateGroups());
if (formKey != null) {
try {
Long formId = Long.parseLong(formKey);
......@@ -394,4 +396,106 @@ public class FlowFormController {
return extensionProperties;
}
/**
* 根据当前任务节点,判断审批不通过时走向哪个节点
* @param taskId 当前任务节点ID(如 "userTask1")
* @param deployId 部署ID
* @param isApproved 是否通过(true=通过,false=不通过)
* @return 目标节点信息
*/
@GetMapping("/appReject")
public void appReject(
@RequestParam String taskId,
@RequestParam String deployId,
@RequestParam String currentTaskId,
@RequestParam String rejectReason,
@RequestParam String targetTaskId,
@RequestParam boolean isApproved) {
Task task = flowRejectService.rejectToTarget(currentTaskId,targetTaskId,rejectReason);
}
@GetMapping("/getRejectTargetNode")
public Result<Map<String, Object>> getRejectTargetNode(
@RequestParam String taskId,
@RequestParam String deployId,
@RequestParam boolean isApproved) {
try {
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.deploymentId(deployId).singleResult();
if (processDefinition == null) {
return Result.error("未找到流程定义");
}
BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinition.getId());
if (bpmnModel == null) {
return Result.error("无法获取BPMN模型");
}
// 找到当前节点
FlowElement currentElement = findFlowElementById(bpmnModel, taskId);
if (!(currentElement instanceof UserTask)) {
return Result.error("当前节点不是用户任务");
}
UserTask userTask = (UserTask) currentElement;
// 遍历所有 outgoing 连线,找到条件匹配的连线
for (SequenceFlow sequenceFlow : userTask.getOutgoingFlows()) {
String conditionExpression = sequenceFlow.getConditionExpression();
if (conditionExpression != null && !conditionExpression.trim().isEmpty()) {
// 根据条件表达式判断是“通过”还是“不通过”
boolean matches = evaluateCondition(conditionExpression, isApproved);
if (matches) {
// 找到目标节点
FlowElement targetElement = findFlowElementById(bpmnModel, sequenceFlow.getTargetRef());
Map<String, Object> result = new HashMap<>();
result.put("targetNodeId", targetElement.getId());
result.put("targetNodeName", targetElement.getName());
result.put("targetNodeType", targetElement.getClass().getSimpleName());
result.put("conditionExpression", conditionExpression);
return Result.OK(result);
}
}
}
return Result.error("未找到匹配的连线条件");
} catch (Exception e) {
log.error("解析拒绝目标节点失败", e);
return Result.error("解析失败: " + e.getMessage());
}
}
private FlowElement findFlowElementById(BpmnModel bpmnModel, String elementId) {
for (org.flowable.bpmn.model.Process process : bpmnModel.getProcesses()) {
FlowElement element = process.getFlowElement(elementId);
if (element != null) {
return element;
}
}
return null;
}
private boolean evaluateCondition(String conditionExpression, boolean isApproved) {
// 条件表达式示例:${approved == true} 或 ${approved == false}
// 这里简化处理:根据字符串内容判断
if (conditionExpression == null) return false;
String expr = conditionExpression.trim();
if (isApproved) {
// 通过条件:通常包含 true, 1, 'yes', 'pass' 等
return expr.matches(".*\\$\\{.*(true|1|'yes'|'pass').*}.*");
} else {
// 不通过条件:通常包含 false, 0, 'no', 'reject' 等
return expr.matches(".*\\$\\{.*(false|0|'no'|'reject').*}.*");
}
}
}
package org.jeecg.modules.flowable.service;
import org.flowable.task.api.Task;
public interface IFlowRejectService {
/**
* 驳回任务到指定历史节点
*
* @param currentTaskId 当前任务ID
* @param targetTaskId 目标历史任务ID(要驳回到的节点)
* @param rejectReason 驳回原因
* @return 新创建的目标节点任务
* @throws RuntimeException 当目标任务不存在、当前任务不存在或驳回到当前节点时抛出
*/
Task rejectToTarget(String currentTaskId, String targetTaskId, String rejectReason);
}
\ No newline at end of file
package org.jeecg.modules.flowable.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.commons.io.IOUtils;
import org.flowable.bpmn.model.*;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
......@@ -19,22 +15,16 @@ import org.flowable.image.ProcessDiagramGenerator;
import org.flowable.image.impl.DefaultProcessDiagramGenerator;
import org.flowable.task.api.Task;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.modules.flowable.apithird.business.entity.FlowForm;
import org.jeecg.modules.flowable.apithird.business.entity.FlowMyBusiness;
import org.jeecg.modules.flowable.apithird.business.service.ISysDeployFormService;
import org.jeecg.modules.flowable.apithird.business.service.impl.FlowMyBusinessServiceImpl;
import org.jeecg.modules.flowable.apithird.entity.ActStatus;
import org.jeecg.modules.flowable.apithird.entity.SysUser;
import org.jeecg.modules.flowable.apithird.service.FlowCallBackServiceI;
import org.jeecg.modules.flowable.apithird.service.IFlowThirdService;
import org.jeecg.modules.flowable.common.constant.ProcessConstants;
import org.jeecg.modules.flowable.common.enums.FlowComment;
import org.jeecg.modules.flowable.domain.dto.FlowNextDto;
import org.jeecg.modules.flowable.domain.dto.FlowProcDefDto;
import org.jeecg.modules.flowable.factory.FlowServiceFactory;
import org.jeecg.modules.flowable.service.IFlowDefinitionService;
import org.jeecg.modules.stm.my.service.IMyTaskService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
......@@ -44,8 +34,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.stream.Collectors;
import org.jeecg.modules.stm.my.entity.MyTask;
/**
......
package org.jeecg.modules.flowable.service.impl;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.task.api.Task;
import org.flowable.task.api.history.HistoricTaskInstance;
import org.jeecg.modules.flowable.factory.FlowServiceFactory;
import org.jeecg.modules.flowable.service.IFlowRejectService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class FlowRejectServiceImpl extends FlowServiceFactory implements IFlowRejectService {
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private HistoryService historyService;
/**
* 驳回任务到指定历史节点
* @param currentTaskId 当前任务ID
* @param targetTaskId 目标历史任务ID(要驳回到的节点)
* @param rejectReason 驳回原因
*/
public Task rejectToTarget(String currentTaskId, String targetTaskId, String rejectReason) {
HistoricTaskInstance targetTask = historyService
.createHistoricTaskInstanceQuery()
.taskId(targetTaskId)
.singleResult();
if (targetTask == null) {
throw new RuntimeException("驳回目的任务不存在");
}
// 2. 获取当前任务
Task currentTask = taskService.createTaskQuery()
.taskId(currentTaskId)
.singleResult();
if (currentTask == null) {
throw new RuntimeException("当前任务不存在");
}
// 3. 不能驳回到当前节点
if (currentTask.getTaskDefinitionKey().equals(targetTask.getTaskDefinitionKey())) {
throw new RuntimeException("不能驳回到当前节点");
}
String processInstanceId = currentTask.getProcessInstanceId();
// 4. 添加驳回意见(可选)
taskService.addComment(currentTaskId, processInstanceId, "reject", rejectReason);
// 5. 核心API:回退节点
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(processInstanceId)
.moveActivityIdTo(currentTask.getTaskDefinitionKey(), targetTask.getTaskDefinitionKey())
.changeState();
// 6. 清理脏数据(中间节点的历史任务和变量)
clearDirtyData(processInstanceId, currentTask, targetTask);
// 7. 返回新创建的目标节点任务
return taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.taskDefinitionKey(targetTask.getTaskDefinitionKey())
.singleResult();
}
/**
* 清理驳回节点之间的脏数据
*/
private void clearDirtyData(String processInstanceId, Task sourceTask, HistoricTaskInstance targetTask) {
// 获取历史任务列表(按开始时间降序)
List<HistoricTaskInstance> historyTasks = historyService
.createHistoricTaskInstanceQuery()
.processInstanceId(processInstanceId)
.orderByHistoricTaskInstanceStartTime().desc()
.list();
// 获取需要删除的任务节点(从目标节点到当前节点之间的所有节点)
List<HistoricTaskInstance> betweenNodes = getBetweenNodes(historyTasks,
sourceTask.getId(), targetTask.getId());
// 收集需要清理的流程变量Key
List<String> variableKeys = new ArrayList<>();
for (HistoricTaskInstance task : betweenNodes) {
// 根据实际业务,清理相关变量
variableKeys.add("approveResult");
variableKeys.add("approveComment");
// 删除历史任务记录
historyService.deleteHistoricTaskInstance(task.getId());
}
// 删除流程变量
if (!variableKeys.isEmpty()) {
runtimeService.removeVariables(processInstanceId, variableKeys);
}
}
/**
* 获取两个节点之间的所有历史任务
*/
private List<HistoricTaskInstance> getBetweenNodes(List<HistoricTaskInstance> historyTasks,
String sourceTaskId, String targetTaskId) {
List<HistoricTaskInstance> betweenNodes = new ArrayList<>();
boolean startRecord = false;
for (HistoricTaskInstance task : historyTasks) {
if (task.getId().equals(sourceTaskId)) {
startRecord = true;
}
if (startRecord) {
betweenNodes.add(task);
}
if (task.getId().equals(targetTaskId)) {
break;
}
}
return betweenNodes;
}
}
\ No newline at end of file
......@@ -134,12 +134,33 @@ public class StProblemCheck implements Serializable {
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern="yyyy-MM-dd")
private java.util.Date planEndDate;
/**整改开始时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern="yyyy-MM-dd")
private java.util.Date execStartDate;
/**整改结束时间*/
@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern="yyyy-MM-dd")
private java.util.Date execEndDate;
/**整蛊执行人ID*/
private java.lang.String execUserId;
/**执行部门*/
private java.lang.String execDepCode;
/**整改落实情况*/
private java.lang.String execRemark;
/**发现人*/
private java.lang.String findUser;
/**流程状态*/
private java.lang.String bpmStatus;
/**部署ID*/
private java.lang.String deployId;
/**流程实例ID*/
private java.lang.String procInsId;
/**风险等级*/
private java.lang.Integer riskLevel;
/**流程节点ID*/
......
......@@ -21,16 +21,16 @@ management:
flowable:
database-schema-update: false
cmmn:
enabled: false
app:
enabled: false
content:
enabled: false
dmn:
enabled: false
form:
enabled: false
# cmmn:
# enabled: false
# app:
# enabled: false
# content:
# enabled: false
# dmn:
# enabled: false
# form:
# enabled: false
spring:
# main:
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论