提交 78599ce5 authored 作者: kxjia's avatar kxjia

问题模块

上级 ca1f544f
......@@ -14,7 +14,6 @@ export const listDefinition = async (params) => {
// 部署流程实例
export function definitionStart(procDefId, data) {
alert(JSON.stringify(procDefId))
return defHttp.post({
url: '/flowable/definition/startByProcDefId',
data: {
......
<!-- CurrentFormPanel.vue -->
<template>
<div class="form-panel">
<div class="form-header">
<span class="form-title">当前待办</span>
<a-tag color="green" v-if="editableNode">可编辑</a-tag>
</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>
<component
:is="getComponent(editableNode.formUrl || editableNode.formListUrl)"
:ref="(el) => setFormRef(el, editableNode.id)"
:disabled="false"
:readonly="false"
:form-data="formData"
:current-flow-node="editableNode"
@update:form-data="handleFormDataUpdate"
/>
</a-card>
</div>
<div v-else class="empty-form-state">
<a-empty description="未找到可编辑的表单节点" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, defineAsyncComponent, h, watch, ComponentPublicInstance, nextTick } from 'vue'
interface WorkflowNode {
id: string
name: string
formUrl?: string
formListUrl?: string
[key: string]: any
}
// 表单组件实例类型
interface FormComponentInstance extends ComponentPublicInstance {
validate?: () => Promise<any>
getFormData?: () => any
initFormData?: (dataId: string) => Promise<void> | void
formData?: any
[key: string]: any
}
const props = defineProps({
editableNode: {
type: Object as () => WorkflowNode | null,
default: null
},
dataId: {
type: String,
default: ''
},
externalFormData: {
type: Object as () => Record<string, any>,
default: () => ({})
}
})
const emit = defineEmits(['update:form-data', 'form-mounted'])
// 组件缓存
const componentCache = new Map()
const modules = import.meta.glob('@/views/**/*.vue')
// 表单组件实例
const formComponentRef = ref<FormComponentInstance | null>(null)
const formData = ref<any>({})
// 设置表单组件 ref
function setFormRef(el: any, nodeId: string) {
if (el) {
formComponentRef.value = el
emit('form-mounted', { nodeId, instance: el })
callInitFormData(el)
}
}
// 调用 initFormData
async function callInitFormData(formComponent: FormComponentInstance) {
if (!props.dataId) return
if (!formComponent) return
if (typeof formComponent.initFormData === 'function') {
try {
await formComponent.initFormData(props.dataId)
} catch (error) {
console.error('initFormData 调用失败:', error)
}
}
}
// 处理表单数据更新
function handleFormDataUpdate(data: any) {
formData.value = { ...formData.value, ...data }
emit('update:form-data', formData.value)
}
// 获取当前表单实例
function getFormInstance(): FormComponentInstance | null {
return formComponentRef.value
}
// 获取表单数据
async function getFormData(): Promise<any> {
const formComponent = formComponentRef.value
if (!formComponent) {
return formData.value
}
if (typeof formComponent.getFormData === 'function') {
try {
const data = await formComponent.getFormData()
return data
} catch (error) {
console.error('调用 getFormData 失败:', error)
}
}
if (formComponent.formData !== undefined) {
return formComponent.formData
}
return formData.value
}
// 验证表单
async function validateForm(): Promise<boolean> {
const formComponent = formComponentRef.value
if (!formComponent) {
return true
}
if (typeof formComponent.validate === 'function') {
try {
await formComponent.validate()
return true
} catch (error) {
return false
}
}
return true
}
// 重置表单数据
function resetFormData() {
if (props.editableNode && props.externalFormData[props.editableNode.id]) {
formData.value = { ...props.externalFormData[props.editableNode.id] }
} else {
formData.value = {}
}
}
// 重新加载表单数据
async function reloadFormData() {
if (formComponentRef.value && typeof formComponentRef.value.initFormData === 'function') {
await formComponentRef.value.initFormData(props.dataId)
}
}
// 监听 dataId 变化
watch(() => props.dataId, async () => {
if (formComponentRef.value) {
await nextTick()
await reloadFormData()
}
})
// 监听外部数据变化
watch(() => props.externalFormData, (newData) => {
if (newData && props.editableNode && newData[props.editableNode.id]) {
formData.value = newData[props.editableNode.id]
}
}, { deep: true })
// 监听可编辑节点变化
watch(() => props.editableNode, () => {
resetFormData()
})
function getComponent(url: string) {
if (!url) {
return createEmptyComponent()
}
if (componentCache.has(url)) {
return componentCache.get(url)
}
let componentPath = ''
if (url.includes('/views')) {
componentPath = `/src${url}`
} else {
componentPath = `/src/views${url}`
}
if (!componentPath.match(/\.(vue|js|ts|jsx|tsx)$/)) {
componentPath += '.vue'
}
const loader = modules[componentPath]
if (!loader) {
console.error('未找到组件:', componentPath)
const ErrorComponent = createErrorComponent(`组件未找到: ${componentPath}`)
componentCache.set(url, ErrorComponent)
return ErrorComponent
}
const AsyncComponent = defineAsyncComponent({
loader: () => loader() as Promise<{ default: any }>,
loadingComponent: {
render: () => h('div', { style: 'text-align: center; padding: 20px;' }, '加载中...')
},
errorComponent: {
render: () => h('div', { style: 'color: red; padding: 20px;' }, '组件加载失败')
},
delay: 200,
timeout: 3000
})
componentCache.set(url, AsyncComponent)
return AsyncComponent
}
function createEmptyComponent() {
return {
render: () => h('div', { style: 'color: #999; padding: 20px; text-align: center;' }, '该节点未配置表单')
}
}
function createErrorComponent(msg: string) {
return {
render: () => h('div', { style: 'color: red; padding: 20px;' }, msg)
}
}
// 暴露方法给父组件
defineExpose({
getFormData,
validateForm,
getFormInstance,
resetFormData,
reloadFormData
})
</script>
<style scoped lang="scss">
.form-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
background-color: #fff;
.form-header {
padding: 16px 24px;
border-bottom: 1px solid #e8eef2;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
.form-title {
font-size: 16px;
font-weight: 500;
color: #1f2f3d;
}
}
.form-content {
flex: 1;
overflow-y: auto;
padding: 24px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
.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;
align-items: center;
justify-content: center;
}
}
</style>
\ No newline at end of file
<!-- HistoryPanel.vue -->
<template>
<div class="history-panel" :class="{ 'empty-history': readonlyNodes.length === 0 }">
<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 }">
<div v-if="readonlyNodes.length === 0" class="empty-history-state">
<a-empty description="暂无历史节点" :image="simpleImage" />
</div>
<a-timeline v-else>
<a-timeline-item
v-for="(node, index) in readonlyNodes"
:key="node.id"
:color="index === readonlyNodes.length - 1 ? 'blue' : 'gray'"
>
<template #dot>
<div class="timeline-dot">
<check-circle-outlined v-if="index < readonlyNodes.length - 1" />
<clock-circle-outlined v-else />
</div>
</template>
<a-card
:title="node.name"
:bordered="false"
size="small"
class="history-card"
:class="{ 'last-card': index === readonlyNodes.length - 1 }"
>
<template #extra>
<a-tag :color="index === readonlyNodes.length - 1 ? 'orange' : 'green'" size="small">
{{ index === readonlyNodes.length - 1 ? '已通过' : '已完成' }}
</a-tag>
</template>
<div class="history-card-content">
<div class="node-info">
<span class="node-label">表单名称:</span>
<span class="node-value">{{ node.formName || node.name }}</span>
</div>
<div class="node-info">
<span class="node-label">处理时间:</span>
<span class="node-value">{{ node.processTime || '--' }}</span>
</div>
<div class="node-info">
<span class="node-label">处理人:</span>
<span class="node-value">{{ node.processor || '--' }}</span>
</div>
<div class="node-info">
<span class="node-label">审批意见:</span>
<span class="node-value">{{ node.comment || '无' }}</span>
</div>
</div>
<!-- 如果需要显示历史表单数据,可以展开 -->
<template v-if="showHistoryFormData">
<a-divider style="margin: 12px 0" />
<div class="history-form-preview">
<div class="preview-header">
<span>表单数据预览</span>
<a-button type="link" size="small" @click="togglePreview(node.id)">
{{ expandedPreviewId === node.id ? '收起' : '展开' }}
</a-button>
</div>
<a-collapse v-model:activeKey="expandedPreviewId" :bordered="false" ghost>
<a-collapse-panel :key="node.id" :showArrow="false">
<component
:is="getComponent(node.formUrl)"
:disabled="true"
:readonly="true"
:form-data="{ dataId: dataId }"
:formId="Number(dataId) || 2035539989"
:current-flow-node="node"
class="history-form-component"
/>
</a-collapse-panel>
</a-collapse>
</div>
</template>
</a-card>
</a-timeline-item>
</a-timeline>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, defineAsyncComponent, h } from 'vue'
import { CheckCircleOutlined, ClockCircleOutlined } from '@ant-design/icons-vue'
import { Empty } from 'ant-design-vue'
interface WorkflowNode {
id: string
name: string
formName?: string
formUrl?: string
formListUrl?: string
procDefId?: string
dataId?: string
processTime?: string
processor?: string
comment?: string
[key: string]: any
}
const props = defineProps({
readonlyNodes: {
type: Array as () => WorkflowNode[],
required: true,
default: () => []
},
dataId: {
type: String,
default: ''
},
showHistoryFormData: {
type: Boolean,
default: false
}
})
// 简单空状态图片
const simpleImage = Empty.PRESENTED_IMAGE_SIMPLE
// 组件缓存
const componentCache = new Map()
const modules = import.meta.glob('@/views/**/*.vue')
// 历史表单预览展开状态
const expandedPreviewId = ref<string>('')
// 切换预览展开/收起
function togglePreview(nodeId: string) {
expandedPreviewId.value = expandedPreviewId.value === nodeId ? '' : nodeId
}
function getComponent(url: string) {
if (!url) {
return createEmptyComponent()
}
if (componentCache.has(url)) {
return componentCache.get(url)
}
let componentPath = ''
if (url.includes('/views')) {
componentPath = `/src${url}`
} else {
componentPath = `/src/views${url}`
}
if (!componentPath.match(/\.(vue|js|ts|jsx|tsx)$/)) {
componentPath += '.vue'
}
const loader = modules[componentPath]
if (!loader) {
console.error('未找到组件:', componentPath)
const ErrorComponent = createErrorComponent(`组件未找到: ${componentPath}`)
componentCache.set(url, ErrorComponent)
return ErrorComponent
}
const AsyncComponent = defineAsyncComponent({
loader: () => loader() as Promise<{ default: any }>,
loadingComponent: {
render: () => h('div', { style: 'text-align: center; padding: 20px;' }, '加载中...')
},
errorComponent: {
render: () => h('div', { style: 'color: red; padding: 20px;' }, '组件加载失败')
},
delay: 200,
timeout: 3000
})
componentCache.set(url, AsyncComponent)
return AsyncComponent
}
function createEmptyComponent() {
return {
render: () => h('div', { style: 'color: #999; padding: 20px; text-align: center;' }, '该节点未配置表单')
}
}
function createErrorComponent(msg: string) {
return {
render: () => h('div', { style: 'color: red; padding: 20px;' }, msg)
}
}
</script>
<style scoped lang="scss">
.history-panel {
width: 50%;
background-color: #f5f7fa;
border-right: 1px solid #e8eef2;
display: flex;
flex-direction: column;
overflow: hidden;
&.empty-history {
.history-content {
display: flex;
align-items: center;
justify-content: center;
}
}
.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;
.history-title {
font-size: 16px;
font-weight: 500;
color: #1f2f3d;
}
}
.history-content {
flex: 1;
overflow-y: auto;
padding: 16px 12px;
&.has-scroll {
padding-right: 8px;
}
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: #e8eef2;
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 2px;
&:hover {
background: #a8a8a8;
}
}
}
.empty-history-state {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
:deep(.ant-timeline) {
.ant-timeline-item {
padding-bottom: 20px;
}
.ant-timeline-item-tail {
border-left-color: #d9d9d9;
}
}
.timeline-dot {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
border-radius: 50%;
font-size: 14px;
.anticon {
font-size: 14px;
}
}
.history-card {
background-color: #fff;
border-radius: 8px;
margin-bottom: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
&.last-card {
border-left: 3px solid #1890ff;
}
:deep(.ant-card-head) {
padding: 12px 16px;
min-height: auto;
border-bottom: 1px solid #f0f0f0;
.ant-card-head-title {
font-size: 14px;
font-weight: 500;
padding: 0;
}
.ant-card-extra {
padding: 0;
}
}
:deep(.ant-card-body) {
padding: 12px 16px;
}
}
.history-card-content {
.node-info {
display: flex;
margin-bottom: 8px;
font-size: 13px;
&:last-child {
margin-bottom: 0;
}
.node-label {
width: 70px;
color: #8c8c8c;
flex-shrink: 0;
}
.node-value {
color: #262626;
flex: 1;
word-break: break-all;
}
}
}
.history-form-preview {
margin-top: 8px;
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 12px;
color: #8c8c8c;
}
:deep(.ant-collapse) {
background: transparent;
.ant-collapse-item {
border: none;
}
.ant-collapse-content {
border: none;
background: #fafafa;
border-radius: 4px;
margin-top: 8px;
}
.ant-collapse-content-box {
padding: 12px;
font-size: 12px;
}
}
}
.history-form-component {
:deep(*) {
font-size: 12px;
}
}
}
</style>
\ No newline at end of file
<!-- TaskAssigneeDrawer.vue -->
<template>
<a-drawer
:title="title"
:visible="visible"
:width="width"
:closable="true"
:mask-closable="maskClosable"
:destroy-on-close="destroyOnClose"
:footer="null"
@close="handleClose"
class="task-assignee-drawer"
>
<div class="drawer-content">
<!-- 任务分配组件 -->
<div class="assignee-section">
<TaskAssigneeSelector
ref="assigneeSelectorRef"
:title="assigneeTitle"
:proc-def-id="procDefId"
:task-id="taskId"
:form-data="formData"
:initial-assignee="initialAssignee"
:user-type-options="userTypeOptions"
:required="required"
:next-api="customNextApi"
@confirm="handleAssigneeConfirm"
@success="handleTaskSuccess"
@error="handleTaskError"
@cancel="handleCancel"
/>
</div>
</div>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import TaskAssigneeSelector from './TaskAssigneeSelector.vue'
interface AssigneeData {
userType: 'user' | 'role'
assignee: string
name?: string
remark?: string
}
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
},
// 任务信息
taskId: {
type: String,
required: true
},
procDefId: {
type: String,
required: true
},
// 表单数据
formData: {
type: Object,
default: () => ({})
},
// 初始分配数据
initialAssignee: {
type: Object as () => AssigneeData | null,
default: null
},
// 分配组件配置
assigneeTitle: {
type: String,
default: '选择处理人'
},
userTypeOptions: {
type: Array as () => Array<{ value: string; label: string }>,
default: () => [
{ value: 'user', label: '用户' },
{ value: 'role', label: '角色' }
]
},
required: {
type: Boolean,
default: true
},
// 自定义API
customNextApi: {
type: Function,
default: null
}
})
const emit = defineEmits([
'update:visible',
'success',
'error',
'close',
'assignee-confirm'
])
// 状态
const assigneeSelectorRef = ref<InstanceType<typeof TaskAssigneeSelector> | null>(null)
// 处理分配确认
function handleAssigneeConfirm(assigneeData: AssigneeData) {
emit('assignee-confirm', assigneeData)
}
// 处理任务成功
function handleTaskSuccess(response: any) {
emit('success', response)
// 成功后自动关闭抽屉
setTimeout(() => {
handleClose()
}, 500)
}
// 处理任务失败
function handleTaskError(error: any) {
emit('error', error)
}
// 处理取消
function handleCancel() {
handleClose()
}
// 关闭抽屉
function handleClose() {
emit('update:visible', false)
emit('close')
}
// 重置状态
function resetDrawer() {
if (assigneeSelectorRef.value) {
assigneeSelectorRef.value.resetForm()
}
}
// 监听抽屉打开/关闭
watch(() => props.visible, (newVal) => {
if (!newVal) {
resetDrawer()
}
})
// 暴露方法
defineExpose({
resetDrawer,
getAssigneeData: () => assigneeSelectorRef.value?.getAssigneeData(),
submit: () => assigneeSelectorRef.value?.handleConfirm()
})
</script>
<style scoped lang="scss">
.task-assignee-drawer {
:deep(.ant-drawer-body) {
padding: 0;
height: 100%;
overflow: hidden;
}
:deep(.ant-drawer-header) {
background-color: #f5f7fa;
border-bottom: 1px solid #e8eef2;
padding: 16px 24px;
.ant-drawer-title {
font-size: 16px;
font-weight: 500;
color: #1f2f3d;
}
.ant-drawer-close {
color: #8c8c8c;
&:hover {
color: #1f2f3d;
}
}
}
}
.drawer-content {
height: 100%;
overflow-y: auto;
padding: 16px 20px;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 2px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 2px;
&:hover {
background: #a8a8a8;
}
}
}
// 分配区域
.assignee-section {
height: 100%;
:deep(.task-assignee-selector) {
height: 100%;
.assignee-card {
height: 100%;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.03);
:deep(.ant-card-head) {
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
}
:deep(.ant-card-body) {
height: calc(100% - 57px);
}
}
}
}
</style>
\ No newline at end of file
<template>
<div class="multi-form-preview">
<!-- 只读表单列表:前 N-1 个 -->
<div v-for="(node, idx) in previousNodes" :key="`readonly-${idx}`" class="form-readonly-item">
<div class="form-header">
<span class="form-name">{{ node.name }}</span>
<a-tag color="blue" class="readonly-tag">只读</a-tag>
</div>
<div class="form-content readonly">
<component
:is="loadComponent(node.formUrl)"
:disabled="true"
:readonly="true"
:form-data="formDataMap[node.id]"
:current-flow-node="node"
/>
</div>
</div>
<!-- 可编辑表单:第 N 个(当前节点) -->
<div v-if="currentNode" class="form-editable-item">
<div class="form-header">
<span class="form-name">{{ currentNode.name }}</span>
<a-tag color="green">可编辑</a-tag>
</div>
<div class="form-content editable">
<component
:is="loadComponent(currentNode.formUrl)"
:disabled="false"
:readonly="false"
:form-data="currentFormData"
:current-flow-node="currentNode"
@update:form-data="handleFormDataUpdate"
@submit="handleSubmit"
/>
</div>
</div>
<!-- 加载错误提示 -->
<a-empty v-if="!currentNode && previousNodes.length === 0" description="未找到有效表单节点" />
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, defineAsyncComponent, h, watch } from 'vue';
import { message } from 'ant-design-vue';
// 定义组件属性
const props = defineProps({
// 当前节点在 workflowNodes 中的索引(从 0 开始)
currentNodeIndex: {
type: Number,
required: true,
default: 2 // 默认第三个(索引2)
},
// 工作流节点列表(从父组件传入)
workflowNodes: {
type: Array as () => Array<any>,
required: true,
default: () => []
},
// 外部传入的表单数据(用于回显)
externalFormData: {
type: Object,
default: () => ({})
}
});
// 定义事件
const emit = defineEmits(['form-data-update', 'submit']);
// 组件缓存
const componentCache = new Map();
// 使用 import.meta.glob 预加载所有可能的组件
const modules = import.meta.glob('@/views/**/*.vue');
// 当前节点的表单数据(可编辑)
const currentFormData = ref<any>({});
// 只读节点的表单数据映射(按节点id存储)
const formDataMap = ref<Record<string, any>>({});
// 获取前 N 个节点(只读部分)
const previousNodes = computed(() => {
if (!props.workflowNodes || props.workflowNodes.length === 0) return [];
const idx = props.currentNodeIndex;
// 取索引小于 idx 的节点(即当前节点之前的所有节点)
return props.workflowNodes.slice(0, idx);
});
// 获取当前节点(第 N 个,可编辑)
const currentNode = computed(() => {
if (!props.workflowNodes || props.workflowNodes.length === 0) return null;
const idx = props.currentNodeIndex;
if (idx < 0 || idx >= props.workflowNodes.length) return null;
return props.workflowNodes[idx];
});
// 将 URL 转换为正确的导入路径并动态加载组件
function loadComponent(url: string) {
if (!url) {
console.warn('formUrl 为空');
return createEmptyComponent();
}
if (componentCache.has(url)) {
return componentCache.get(url);
}
// 构建组件路径(使用 /@/ 格式)
let componentPath = '';
if (url.includes('/views')) {
componentPath = `/src${url}`;
} else {
componentPath = `/src/views${url}`;
}
// 确保有文件后缀
if (!componentPath.match(/\.(vue|js|ts|jsx|tsx)$/)) {
componentPath += '.vue';
}
console.log('加载组件路径:', componentPath);
// 从预加载的 modules 中获取加载器
let loader = modules[componentPath];
if (!loader) {
console.error('未找到组件:', componentPath);
console.log('可用路径示例:', Object.keys(modules).slice(0, 5));
const ErrorComponent = createErrorComponent(`组件未找到: ${componentPath}`);
componentCache.set(url, ErrorComponent);
return ErrorComponent;
}
// 创建异步组件,并注入额外的 props(disabled/readonly 等)
const AsyncComponent = defineAsyncComponent({
loader: () => loader() as Promise<{ default: any }>,
loadingComponent: {
render: () => h('div', { style: 'text-align: center; padding: 20px;' }, '加载中...')
},
errorComponent: {
render: () => h('div', { style: 'color: red; padding: 20px;' }, '组件加载失败,请刷新页面重试')
},
delay: 200,
timeout: 3000,
onError(error) {
console.error('异步组件加载错误:', error);
}
});
componentCache.set(url, AsyncComponent);
return AsyncComponent;
}
// 创建空组件(当 formUrl 为空时使用)
function createEmptyComponent() {
return {
render: () => h('div', { style: 'color: #999; padding: 20px; text-align: center;' }, '该节点未配置表单')
};
}
// 创建错误组件
function createErrorComponent(msg: string) {
return {
render: () => h('div', { style: 'color: red; padding: 20px;' }, msg)
};
}
// 处理表单数据更新(来自可编辑组件)
function handleFormDataUpdate(data: any) {
currentFormData.value = { ...currentFormData.value, ...data };
emit('form-data-update', currentFormData.value);
}
// 处理提交事件
function handleSubmit(data: any) {
emit('submit', {
nodeId: currentNode.value?.id,
nodeName: currentNode.value?.name,
formData: data || currentFormData.value
});
}
// 初始化只读节点的表单数据(从外部传入或模拟)
function initReadonlyFormData() {
// 如果外部传入了表单数据,则按节点id映射
if (props.externalFormData && Object.keys(props.externalFormData).length > 0) {
// 假设 externalFormData 的结构为 { nodeId: formData, ... }
previousNodes.value.forEach(node => {
if (props.externalFormData[node.id]) {
formDataMap.value[node.id] = props.externalFormData[node.id];
} else {
// 如果没有传入,设置空对象
formDataMap.value[node.id] = {};
}
});
} else {
// 初始化空数据
previousNodes.value.forEach(node => {
formDataMap.value[node.id] = {};
});
}
}
// 监听外部表单数据变化
watch(() => props.externalFormData, (newData) => {
if (newData && Object.keys(newData).length > 0) {
previousNodes.value.forEach(node => {
if (newData[node.id]) {
formDataMap.value[node.id] = newData[node.id];
}
});
// 如果当前节点有数据,也更新
if (currentNode.value && newData[currentNode.value.id]) {
currentFormData.value = newData[currentNode.value.id];
}
}
}, { deep: true, immediate: true });
// 组件挂载时初始化
onMounted(() => {
initReadonlyFormData();
console.log('MultiFormPreview 组件已挂载');
console.log('当前节点索引:', props.currentNodeIndex);
console.log('只读节点数量:', previousNodes.value.length);
console.log('当前节点:', currentNode.value?.name);
});
</script>
<style scoped lang="scss">
.multi-form-preview {
width: 100%;
padding: 16px;
background-color: #f5f7fa;
border-radius: 8px;
.form-readonly-item,
.form-editable-item {
margin-bottom: 24px;
background-color: #fff;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.form-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background-color: #fafbfc;
border-bottom: 1px solid #e8eef2;
.form-name {
font-size: 15px;
font-weight: 500;
color: #1f2f3d;
}
.readonly-tag,
.ant-tag {
font-size: 12px;
}
}
.form-content {
padding: 20px;
&.readonly {
background-color: #fefefe;
// 只读模式下添加半透明遮罩效果,但仍保留交互(如果子组件支持disabled)
opacity: 0.95;
}
&.editable {
background-color: #fff;
}
}
}
.form-editable-item {
border: 1px solid #d9ecff;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.08);
.form-header {
background-color: #e6f7ff;
border-bottom-color: #bae7ff;
.form-name {
color: #096dd9;
}
}
}
}
</style>
\ No newline at end of file
......@@ -548,7 +548,7 @@
submitData.values['approvalType'] = 'role';
}
// }
console.log("执行发送 ",submitData);
// 执行发送
const result = await complete(submitData);
......
......@@ -25,9 +25,22 @@
import { list} from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const emit = defineEmits(['callback'])
const props = defineProps({
beforeFlowNode: {
type: Object,
default: () => ({})
},
currentFlowNode: {
type: Object,
default: () => ({})
},
nextFlowNode: {
type: Object,
default: () => ({})
}
})
//注册model
......@@ -48,7 +61,7 @@
fieldMapToTime: [],
},
beforeFetch(params) {
params['id'] = route.query.id
params['bmpNodeId'] = props.currentFlowNode.id
},
actionColumn: {
width: 200,
......@@ -60,11 +73,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handleArchive(record: Recordable) {
openExecuteModal(true, {
record,
isUpdate: true,
showFooter: true,
});
emit("callback",record)
}
function handleSuccess() {
......
......@@ -46,7 +46,8 @@ export const columns: BasicColumn[] = [
align: 'center',
dataIndex: 'problemDes',
resizable: true,
ifShow: true,
ifShow: false,
},
{
title: '风险等级',
......
......@@ -25,7 +25,7 @@
</template>
</BasicTable>
<!-- 表单区域 -->
<StProblemCheckModal @register="registerModal" @success="handleSuccess" />
<StProblemCheckModal @register="registerModal" @success="handleSuccess" :center="true" />
</div>
</template>
......@@ -53,7 +53,7 @@
}
})
const emit = defineEmits(['callback'])
const emit = defineEmits(['callback','startWorkFlow','sendWorkFlow'])
//注册model
const [registerModal, { openModal }] = useModal();
......@@ -76,7 +76,7 @@
params['bmpNodeId'] = props.currentFlowNode.id
},
actionColumn: {
width: 400,
width: 200,
fixed: 'right',
},
},
......@@ -95,7 +95,7 @@
/**
* 新增事件
*/
function handleAdd333333() {
function handleAdd() {
openModal(true, {
isUpdate: false,
showFooter: true,
......@@ -107,12 +107,7 @@
});
}
function handleAdd() {
emit("callback",null)
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) {
openModal(true, {
record,
......@@ -120,17 +115,6 @@
showFooter: true,
});
}
function handlePlan(record: Recordable) {
openPlanModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) {
openModal(true, {
record,
......@@ -138,27 +122,22 @@
showFooter: false,
});
}
/**
* 删除事件
*/
async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess);
}
/**
* 批量删除事件
*/
async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
}
/**
* 成功回调
*/
function handleSuccess() {
function handleSuccess(retData) {
(selectedRowKeys.value = []) && reload();
if(retData){
emit('startWorkFlow',retData)
}
/**
* 操作栏
*/
}
function getTableAction(record) {
return [
{
......@@ -189,17 +168,14 @@
}
async function handleFlow(record: Recordable) {
//alert(JSON.stringify(props.currentFlowNode))
//alert(JSON.stringify(props.nextFlowNode))
///alert(JSON.stringify(props.beforeFlowNode))
record['deployId'] = props.nextFlowNode.deployId
record['bmpNodeId'] = props.nextFlowNode.id
record['bpmStatus'] = 2
await saveOrUpdate(record,true).then(res => {
handleSuccess()
handleSuccess(record)
})
emit("sendWorkFlow",record)
}
......
......@@ -25,6 +25,7 @@
import { list} from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
const emit = defineEmits(['callback'])
const props = defineProps({
beforeFlowNode: {
......@@ -59,7 +60,7 @@
fieldMapToTime: [],
},
beforeFetch(params) {
params['id'] = route.query.id
params['bmpNodeId'] = props.currentFlowNode.id
},
actionColumn: {
width: 200,
......@@ -71,11 +72,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handleExecuteApproval(record: Recordable) {
openExecuteModal(true, {
record,
isUpdate: true,
showFooter: true,
});
emit("callback",record)
}
function handleSuccess() {
......
......@@ -25,6 +25,8 @@
import { list} from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
const emit = defineEmits(['callback'])
const props = defineProps({
beforeFlowNode: {
type: Object,
......@@ -58,7 +60,7 @@
fieldMapToTime: [],
},
beforeFetch(params) {
params['id'] = route.query.id
params['bmpNodeId'] = props.currentFlowNode.id
},
actionColumn: {
width: 200,
......@@ -70,11 +72,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handleExecute(record: Recordable) {
openExecuteModal(true, {
record,
isUpdate: true,
showFooter: true,
});
emit("callback",record)
}
function handleSuccess() {
......
......@@ -5,6 +5,7 @@
<div v-if="node.formListUrl" class="tab-content">
<component :is="loadComponent(node.formListUrl)"
@startWorkFlow="handleDefinitionStart"
@sendWorkFlow="handleDefinitionSend"
:beforeFlowNode="workflowNodes[index-1]"
:currentFlowNode="node"
:nextFlowNode="workflowNodes[index+1]"
......@@ -17,8 +18,6 @@
</div>
</a-tab-pane>
</a-tabs>
<!-- 多表单抽屉组件 -->
<WorkFlowFormDrawer
v-model:visible="drawerVisible"
:title="drawerTitle"
......@@ -26,22 +25,59 @@
:workflow-nodes="workflowNodes"
:external-form-data="externalFormData"
:proc-def-id="currentProcDefId"
:data-id="dataId"
:show-approval-panel="isShowApprovalPanel"
@submit="handleMultiFormSubmit"
@close="handleDrawerClose"
@form-data-update="handleMultiFormDataUpdate"
width="50%"
width="90%"
/>
<TaskAssigneeDrawer
v-model:visible="drawerTaskVisible"
:task-id="taskId"
:task-name="taskName"
:proc-def-id="procDefId"
:proc-def-name="procDefName"
:form-data="formData"
:show-task-info="true"
:show-form-data="true"
@success="handleSuccess"
@error="handleError"
/>
</div>
</template>
<script lang="ts" name="problem-stProblemCheck" setup>
import { ref, nextTick, onMounted, defineAsyncComponent, h } from 'vue';
import { getNodesByTableName } from '/@/components/Process/api/definition';
import { definitionStart, flowXmlAndNode } from "/@/components/Process/api/definition";
import { definitionStart, definitionStartByDeployId } from "/@/components/Process/api/definition";
import WorkFlowFormDrawer from '/@/views/common/WorkFlowFormDrawer.vue';
import TaskAssigneeDrawer from '/@/views/common/TaskAssigneeDrawer.vue'
const workflowNodes = ref<any[]>([]);
const activeTab = ref(1);
const dataId = ref('');
const currentNode = ref<any>({});
const isShowApprovalPanel = ref(true);
// 任务指派抽屉相关状态
const drawerTaskVisible = ref(false);
const taskId = ref('task_123456')
const taskName = ref('部门经理审批')
const procDefId = ref('process_001')
const procDefName = ref('请假流程')
const formData = ref({
申请人: '张三',
请假类型: '年假',
开始时间: '2024-01-15',
结束时间: '2024-01-20',
请假天数: 5,
请假事由: '家庭旅游'
})
// 抽屉相关状态
const drawerVisible = ref(false);
......@@ -56,6 +92,7 @@
function handleTabChange(key) {
activeTab.value = key;
currentMultiFormIndex.value = key - 1;
currentNode.value = workflowNodes.value[key - 1];
}
function loadComponent(url: string) {
......@@ -106,12 +143,27 @@
return AsyncComponent;
}
const handleDefinitionStart = (procDefId: string, submitData: any) => {
return definitionStart({
procDefId,
variables: submitData
})
const handleDefinitionStart = async (data) => {
const formData = { dataId:data.id, dataName: 'id' };
const startResRaw = await definitionStartByDeployId(
currentNode.value?.deployId || '',
formData
);
if (startResRaw?.data?.id) {
dataId.value = startResRaw.data.id;
}
}
const handleDefinitionSend = async (data) => {
drawerTaskVisible.value = true;
taskId.value = data.taskId;
taskName.value = data.taskName;
procDefId.value = data.procDefId;
procDefName.value = data.procDefName;
}
const handleOpenMultiForm = (params: {
nodeIndex?: number;
title?: string;
......@@ -150,10 +202,12 @@
}) => {
console.log('多表单提交数据:', submitData);
try {
await definitionStart({
procDefId: submitData.procDefId,
variables: submitData.formData
});
await definitionStart(
currentProcDefId.value,
{
...submitData.formData
}
);
drawerVisible.value = false;
const currentTabKey = activeTab.value;
const currentComponent = loadComponent(workflowNodes.value[currentTabKey - 1]?.formListUrl);
......@@ -180,6 +234,17 @@
});
};
function handleSuccess(response: any) {
console.log('任务处理成功:', response)
// 刷新列表等操作
}
function handleError(error: any) {
console.error('任务处理失败:', error)
}
defineExpose({
openHistoryForms,
openMultiForm: handleOpenMultiForm
......@@ -188,11 +253,8 @@
onMounted(async () => {
await nextTick();
try {
console.log('开始获取工作流节点...');
const nodes = await getNodesByTableName("st_problem_check");
workflowNodes.value = nodes;
console.log('获取到的工作流节点:', workflowNodes.value);
workflowNodes.value.forEach((node, index) => {
console.log(`节点${index + 1}:`, node.name, 'formListUrl:', node.formListUrl);
});
......@@ -206,21 +268,31 @@
}
});
}
currentNode.value = workflowNodes.value[0];
} catch (error) {
console.error('获取工作流节点失败:', error);
}
});
function handleCallback(data: any) {
drawerVisible.value = true;
const currentNode = workflowNodes.value[currentMultiFormIndex.value];
currentProcDefId.value = currentNode.procDefId || '';
dataId.value = data.id || '';
currentNode.value = workflowNodes.value[currentMultiFormIndex.value];
currentProcDefId.value = currentNode.value.procDefId || '';
if(currentNode.value.name.indexOf('审') > -1) {
isShowApprovalPanel.value = true;
} else {
isShowApprovalPanel.value = false;
}
const userid = currentNode.assignee || '';
const nodeId = currentNode.id
const deployId = currentNode.deployId || '';
const procDefId = currentNode.procDefId || '';
const attributes = currentNode.attributes || {};
const userid = currentNode.value?.assignee || '';
const nodeId = currentNode.value?.id
const procDefId = currentNode.value?.procDefId || '';
const attributes = currentNode.value?.attributes || {};
const userType = attributes.userType || [];
if (userType.length > 0) {
data['userType'] = userType[0].value || '';
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论