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

问题模块

上级 ca1f544f
...@@ -14,7 +14,6 @@ export const listDefinition = async (params) => { ...@@ -14,7 +14,6 @@ export const listDefinition = async (params) => {
// 部署流程实例 // 部署流程实例
export function definitionStart(procDefId, data) { export function definitionStart(procDefId, data) {
alert(JSON.stringify(procDefId))
return defHttp.post({ return defHttp.post({
url: '/flowable/definition/startByProcDefId', url: '/flowable/definition/startByProcDefId',
data: { 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 @@ ...@@ -548,7 +548,7 @@
submitData.values['approvalType'] = 'role'; submitData.values['approvalType'] = 'role';
} }
// } // }
console.log("执行发送 ",submitData);
// 执行发送 // 执行发送
const result = await complete(submitData); const result = await complete(submitData);
......
...@@ -25,9 +25,22 @@ ...@@ -25,9 +25,22 @@
import { list} from './StProblemCheck.api'; import { list} from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue'; import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
import { useRoute } from 'vue-router'; const emit = defineEmits(['callback'])
const route = useRoute();
const props = defineProps({
beforeFlowNode: {
type: Object,
default: () => ({})
},
currentFlowNode: {
type: Object,
default: () => ({})
},
nextFlowNode: {
type: Object,
default: () => ({})
}
})
//注册model //注册model
...@@ -48,7 +61,7 @@ ...@@ -48,7 +61,7 @@
fieldMapToTime: [], fieldMapToTime: [],
}, },
beforeFetch(params) { beforeFetch(params) {
params['id'] = route.query.id params['bmpNodeId'] = props.currentFlowNode.id
}, },
actionColumn: { actionColumn: {
width: 200, width: 200,
...@@ -60,11 +73,7 @@ ...@@ -60,11 +73,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext; const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handleArchive(record: Recordable) { function handleArchive(record: Recordable) {
openExecuteModal(true, { emit("callback",record)
record,
isUpdate: true,
showFooter: true,
});
} }
function handleSuccess() { function handleSuccess() {
......
...@@ -46,7 +46,8 @@ export const columns: BasicColumn[] = [ ...@@ -46,7 +46,8 @@ export const columns: BasicColumn[] = [
align: 'center', align: 'center',
dataIndex: 'problemDes', dataIndex: 'problemDes',
resizable: true, resizable: true,
ifShow: true, ifShow: false,
}, },
{ {
title: '风险等级', title: '风险等级',
......
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
</template> </template>
</BasicTable> </BasicTable>
<!-- 表单区域 --> <!-- 表单区域 -->
<StProblemCheckModal @register="registerModal" @success="handleSuccess" /> <StProblemCheckModal @register="registerModal" @success="handleSuccess" :center="true" />
</div> </div>
</template> </template>
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
} }
}) })
const emit = defineEmits(['callback']) const emit = defineEmits(['callback','startWorkFlow','sendWorkFlow'])
//注册model //注册model
const [registerModal, { openModal }] = useModal(); const [registerModal, { openModal }] = useModal();
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
params['bmpNodeId'] = props.currentFlowNode.id params['bmpNodeId'] = props.currentFlowNode.id
}, },
actionColumn: { actionColumn: {
width: 400, width: 200,
fixed: 'right', fixed: 'right',
}, },
}, },
...@@ -95,7 +95,7 @@ ...@@ -95,7 +95,7 @@
/** /**
* 新增事件 * 新增事件
*/ */
function handleAdd333333() { function handleAdd() {
openModal(true, { openModal(true, {
isUpdate: false, isUpdate: false,
showFooter: true, showFooter: true,
...@@ -107,12 +107,7 @@ ...@@ -107,12 +107,7 @@
}); });
} }
function handleAdd() {
emit("callback",null)
}
/**
* 编辑事件
*/
function handleEdit(record: Recordable) { function handleEdit(record: Recordable) {
openModal(true, { openModal(true, {
record, record,
...@@ -120,17 +115,6 @@ ...@@ -120,17 +115,6 @@
showFooter: true, showFooter: true,
}); });
} }
function handlePlan(record: Recordable) {
openPlanModal(true, {
record,
isUpdate: true,
showFooter: true,
});
}
/**
* 详情
*/
function handleDetail(record: Recordable) { function handleDetail(record: Recordable) {
openModal(true, { openModal(true, {
record, record,
...@@ -138,27 +122,22 @@ ...@@ -138,27 +122,22 @@
showFooter: false, showFooter: false,
}); });
} }
/**
* 删除事件
*/
async function handleDelete(record) { async function handleDelete(record) {
await deleteOne({ id: record.id }, handleSuccess); await deleteOne({ id: record.id }, handleSuccess);
} }
/**
* 批量删除事件
*/
async function batchHandleDelete() { async function batchHandleDelete() {
await batchDelete({ ids: selectedRowKeys.value }, handleSuccess); await batchDelete({ ids: selectedRowKeys.value }, handleSuccess);
} }
/**
* 成功回调 function handleSuccess(retData) {
*/
function handleSuccess() {
(selectedRowKeys.value = []) && reload(); (selectedRowKeys.value = []) && reload();
if(retData){
emit('startWorkFlow',retData)
}
} }
/**
* 操作栏
*/
function getTableAction(record) { function getTableAction(record) {
return [ return [
{ {
...@@ -189,17 +168,14 @@ ...@@ -189,17 +168,14 @@
} }
async function handleFlow(record: Recordable) { 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['deployId'] = props.nextFlowNode.deployId
record['bmpNodeId'] = props.nextFlowNode.id record['bmpNodeId'] = props.nextFlowNode.id
record['bpmStatus'] = 2 record['bpmStatus'] = 2
await saveOrUpdate(record,true).then(res => { await saveOrUpdate(record,true).then(res => {
handleSuccess() handleSuccess(record)
}) })
emit("sendWorkFlow",record)
} }
......
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
import { list} from './StProblemCheck.api'; import { list} from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue'; import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
const emit = defineEmits(['callback'])
const props = defineProps({ const props = defineProps({
beforeFlowNode: { beforeFlowNode: {
...@@ -59,7 +60,7 @@ ...@@ -59,7 +60,7 @@
fieldMapToTime: [], fieldMapToTime: [],
}, },
beforeFetch(params) { beforeFetch(params) {
params['id'] = route.query.id params['bmpNodeId'] = props.currentFlowNode.id
}, },
actionColumn: { actionColumn: {
width: 200, width: 200,
...@@ -71,11 +72,7 @@ ...@@ -71,11 +72,7 @@
const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext; const [registerTable, { reload }, { rowSelection, selectedRowKeys }] = tableContext;
function handleExecuteApproval(record: Recordable) { function handleExecuteApproval(record: Recordable) {
openExecuteModal(true, { emit("callback",record)
record,
isUpdate: true,
showFooter: true,
});
} }
function handleSuccess() { function handleSuccess() {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论