提交 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: {
......
<template>
<div class="approval-panel">
<div class="approval-header">
<span class="approval-title">{{ title }}</span>
<a-tag :color="tagColor" v-if="showTag">{{ tagText }}</a-tag>
</div>
<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>
</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>
</div>
</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: '审核节点'
},
// 审核结果配置
resultOptions: {
type: Array as () => ResultOption[],
default: () => [
{
value: 'approved',
label: '通过',
icon: CheckCircleOutlined,
className: 'approved',
color: '#52c41a'
},
{
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: () => []
},
// 当前节点信息(用于默认摘要)
currentNode: {
type: Object as () => WorkflowNode | null,
default: null
},
currentUser: {
type: String,
default: ''
},
showCurrentTime: {
type: Boolean,
default: true
},
// 初始数据
initialData: {
type: Object as () => {
result?: string
comment?: string
rejectNode?: string
[key: string]: any
},
default: () => ({})
},
// 其他配置
disabled: {
type: Boolean,
default: false
},
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
})
}
if (props.currentUser) {
items.push({
label: '处理人',
value: props.currentUser
})
}
if (props.showCurrentTime) {
items.push({
label: '待处理时间',
value: dayjs(currentTime.value).format('YYYY-MM-DD HH:mm:ss')
})
}
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)
}
},
{ 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)
}
})
// 暴露方法给父组件
defineExpose({
getApprovalData,
validate,
resetForm,
setFormData,
getFormRef: () => approvalForm
})
</script>
<style scoped lang="scss">
.approval-panel {
background-color: #fff;
border-left: 1px solid #e8eef2;
display: flex;
flex-direction: column;
overflow: hidden;
height: 100%;
.approval-header {
padding: 16px 20px;
border-bottom: 1px solid #e8eef2;
background-color: #fafbfc;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
.approval-title {
font-size: 16px;
font-weight: 500;
color: #1f2f3d;
}
}
.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;
}
}
}
.result-group {
display: flex;
gap: 16px;
width: 100%;
.ant-radio-wrapper {
flex: 1;
:deep(.ant-radio) {
display: none;
}
:deep(.ant-radio + *) {
padding: 0;
}
.radio-content {
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;
font-weight: 500;
}
}
&.approved .radio-content {
&:hover {
border-color: #52c41a;
background-color: #f6ffed;
}
}
&.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;
}
}
.rejected .radio-content {
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;
}
}
}
.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;
}
.summary-label {
color: #8c8c8c;
}
.summary-value {
color: #262626;
font-weight: 500;
}
}
}
}
</style>
\ No newline at end of file
<!-- 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
<!-- TaskAssigneeSelector.vue -->
<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 :model="formData" 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="true"
:validate-status="userValidateStatus"
:help="userValidateHelp"
>
<div class="assignee-selector">
<a-input
v-model:value="formData.userName"
:placeholder="userPlaceholder"
:readonly="true"
class="assignee-input"
>
<template #suffix>
<a-button
type="link"
size="small"
@click="handleSelectUser"
class="select-btn"
>
选择
</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="true"
:validate-status="roleValidateStatus"
:help="roleValidateHelp"
>
<div class="assignee-selector">
<a-input
v-model:value="formData.roleName"
:placeholder="rolePlaceholder"
:readonly="true"
class="assignee-input"
>
<template #suffix>
<a-button
type="link"
size="small"
@click="handleSelectRole"
class="select-btn"
>
选择
</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-item v-if="roleMembers.length" label="角色成员">
<a-tooltip :title="roleMembers.join(', ')">
<span>{{ roleMembers.length }}人</span>
</a-tooltip>
</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>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue'
import { message } from 'ant-design-vue'
import {
UserOutlined,
TeamOutlined,
CloseCircleOutlined
} from '@ant-design/icons-vue'
//import { nextWorkflowNode } from '@/api/workflow'
interface AssigneeData {
userType: 'user' | 'role'
assignee: string
userName?: string
roleName?: string
remark?: string
[key: string]: any
}
interface WorkflowParams {
procDefId: string
taskId: string
formData: any
assignee?: AssigneeData
[key: string]: any
}
const props = defineProps({
// 标题
title: {
type: String,
default: '任务分配'
},
// 流程定义ID
procDefId: {
type: String,
required: true
},
// 任务ID
taskId: {
type: String,
required: true
},
// 表单数据
formData: {
type: Object,
default: () => ({})
},
// 初始分配数据
initialAssignee: {
type: Object as () => AssigneeData | null,
default: null
},
// 是否必须分配
required: {
type: Boolean,
default: true
},
// 工作流下一步接口配置
nextApi: {
type: Function,
default: null
}
})
const emit = defineEmits([
'confirm',
'cancel',
'success',
'error',
'select-user', // 选择用户事件
'select-role' // 选择角色事件
])
// 表单数据
const formData = ref<AssigneeData>({
userType: 'user',
assignee: '',
userName: '',
roleName: '',
remark: ''
})
// 状态
const confirmLoading = ref(false)
// 验证状态
const userValidateStatus = computed(() => {
if (!props.required) return ''
if (formData.value.userType === 'user' && !formData.value.assignee) {
return 'error'
}
return ''
})
const userValidateHelp = computed(() => {
if (userValidateStatus.value === 'error') {
return '请选择用户'
}
return ''
})
const roleValidateStatus = computed(() => {
if (!props.required) return ''
if (formData.value.userType === 'role' && !formData.value.assignee) {
return 'error'
}
return ''
})
const roleValidateHelp = computed(() => {
if (roleValidateStatus.value === 'error') {
return '请选择角色'
}
return ''
})
// 占位符
const userPlaceholder = computed(() => {
if (formData.value.assignee) {
return formData.value.userName || '已选择用户'
}
return '点击"选择"按钮选择用户'
})
const rolePlaceholder = computed(() => {
if (formData.value.assignee) {
return formData.value.roleName || '已选择角色'
}
return '点击"选择"按钮选择角色'
})
// 分配状态
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 roleMembers = ref<string[]>([])
// 是否有效
const isValid = computed(() => {
if (!props.required) return true
if (!formData.value.assignee) return false
return true
})
// 处理用户类型切换
function handleUserTypeChange() {
clearAssignee()
}
// 处理选择用户(触发父组件事件)
function handleSelectUser() {
emit('select-user', {
currentAssignee: formData.value.assignee,
onSelect: (user: any) => {
setUserData(user)
}
})
}
// 处理选择角色(触发父组件事件)
function handleSelectRole() {
emit('select-role', {
currentAssignee: formData.value.assignee,
onSelect: (role: any) => {
setRoleData(role)
}
})
}
// 设置用户数据(供父组件调用)
function setUserData(user: any) {
formData.value.assignee = user.id
formData.value.userName = user.name || user.realName || user.username
message.success(`已选择用户: ${formData.value.userName}`)
}
// 设置角色数据(供父组件调用)
async function setRoleData(role: any) {
formData.value.assignee = role.id
formData.value.roleName = role.name || role.roleName
// 获取角色成员(可选)
try {
// const members = await getRoleMembers(role.id)
// roleMembers.value = members
roleMembers.value = [] // 示例数据
} catch (error) {
console.error('获取角色成员失败:', error)
}
message.success(`已选择角色: ${formData.value.roleName}`)
}
// 清空分配
function clearAssignee() {
formData.value.assignee = ''
formData.value.userName = ''
formData.value.roleName = ''
roleMembers.value = []
}
// 取消
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
}
// 构建工作流参数
const workflowParams: WorkflowParams = {
procDefId: props.procDefId,
taskId: props.taskId,
formData: props.formData,
assignee: assigneeData,
completeTime: new Date().toISOString()
}
// 触发确认事件
emit('confirm', assigneeData)
// 调用工作流下一步接口
let response
if (props.nextApi) {
response = await props.nextApi(workflowParams)
} else {
//response = await nextWorkflowNode(workflowParams)
}
if (response && response.success !== false) {
message.success('任务提交成功')
emit('success', response)
return response
} else {
throw new Error(response?.message || '提交失败')
}
} catch (error: any) {
console.error('提交失败:', error)
message.error(error?.message || '提交失败,请重试')
emit('error', error)
throw 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 setAssigneeData(data: AssigneeData) {
if (data) {
formData.value = {
userType: data.userType || 'user',
assignee: data.assignee || '',
userName: data.userName || '',
roleName: data.roleName || '',
remark: data.remark || ''
}
}
}
// 重置表单
function resetForm() {
formData.value = {
userType: 'user',
assignee: '',
userName: '',
roleName: '',
remark: ''
}
roleMembers.value = []
}
// 监听初始数据
watch(() => props.initialAssignee, (newVal) => {
if (newVal) {
setAssigneeData(newVal)
}
}, { immediate: true })
// 暴露方法
defineExpose({
getAssigneeData,
setAssigneeData,
resetForm,
setUserData, // 暴露设置用户数据的方法
setRoleData, // 暴露设置角色数据的方法
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;
}
}
.select-btn {
padding: 0 8px;
color: #1890ff;
}
}
.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;
}
.ant-descriptions-item-content {
background-color: #fff;
}
}
}
.assignee-actions {
margin-top: auto;
padding-top: 24px;
text-align: center;
border-top: 1px solid #f0f0f0;
}
:deep(.ant-form-item) {
margin-bottom: 20px;
&:last-child {
margin-bottom: 0;
}
}
}
</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
<template>
<a-drawer
:title="drawerTitle"
:visible="visible"
:width="drawerWidth"
:closable="true"
:mask-closable="maskClosable"
:header-style="{ backgroundColor: '#f5f7fa', borderBottom: '1px solid #e8eef2' }"
@close="handleClose"
class="workflow-form-drawer"
style="padding: 0px;"
>
<div class="drawer-layout" :class="{ 'three-columns': isApprovalNode }">
<!-- 左侧:历史节点区域 -->
<HistoryPanel
:readonly-nodes="readonlyNodes"
:data-id="dataId"
:show-history-form-data="showHistoryFormData"
/>
<!-- 中间:当前节点表单区域 -->
<div class="form-wrapper">
<CurrentFormPanel
ref="currentFormPanelRef"
:editable-node="editableNode"
:data-id="dataId"
:external-form-data="externalFormData"
@update:form-data="handleFormDataUpdate"
@form-mounted="handleFormMounted"
/>
</div>
<!-- 右侧:审核意见区域(仅审核节点显示) -->
<ApprovalPanel
v-if="isApprovalNode"
ref="approvalPanelRef"
:current-node="editableNode"
:is-approval-node="isApprovalNode"
:reject-nodes="rejectNodes"
:can-select-reject-node="canSelectRejectNode"
:current-user="currentUser"
:initial-data="initialApprovalData"
@update:approval-data="handleApprovalDataUpdate"
/>
</div>
<template #footer>
<div class="drawer-footer">
<a-space>
<a-button @click="handleClose">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
{{ isApprovalNode ? '提交审核' : '提交' }}
</a-button>
</a-space>
</div>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import HistoryPanel from './HistoryPanel.vue'
import CurrentFormPanel from './CurrentFormPanel.vue'
import ApprovalPanel from './ApprovalPanel.vue'
interface WorkflowNode {
id: string
name: string
formName?: string
formUrl?: string
formListUrl?: string
procDefId?: string
dataId?: string
processTime?: string
processor?: string
comment?: string
nodeType?: string // 'form' | 'approval'
[key: string]: any
}
const props = defineProps({
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '表单处理'
},
width: {
type: [Number, String],
default: "90%"
},
maskClosable: {
type: Boolean,
default: false
},
// 当前节点索引(从0开始),这个索引对应的节点是可编辑的
currentNodeIndex: {
type: Number,
required: true,
default: 2
},
workflowNodes: {
type: Array as () => WorkflowNode[],
required: true,
default: () => []
},
externalFormData: {
type: Object as () => Record<string, any>,
default: () => ({})
},
procDefId: {
type: String,
default: ''
},
dataId: {
type: String,
default: ''
},
// 是否显示历史节点的表单数据
showHistoryFormData: {
type: Boolean,
default: false
},
// 当前节点是否为审核节点(如果是审核节点,显示三栏布局)
isApprovalNode: {
type: Boolean,
default: false
},
// 可退回的节点列表
rejectNodes: {
type: Array as () => WorkflowNode[],
default: () => []
},
// 是否可以手动选择退回节点
canSelectRejectNode: {
type: Boolean,
default: true
},
// 当前用户
currentUser: {
type: String,
default: ''
},
// 初始审核数据
initialApprovalData: {
type: Object as () => {
result?: string
comment?: string
rejectNode?: string
},
default: () => ({})
}
})
const emit = defineEmits(['update:visible', 'submit', 'close', 'form-data-update'])
// 状态
const submitLoading = ref(false)
const currentFormData = ref<any>({})
const approvalData = ref<any>({})
const currentFormPanelRef = ref<InstanceType<typeof CurrentFormPanel> | null>(null)
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) {
return []
}
const idx = props.currentNodeIndex
if (idx <= 0) return []
return props.workflowNodes.slice(0, idx)
})
// 可编辑节点:索引等于 currentNodeIndex 的节点
const editableNode = 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]
})
// 处理表单数据更新
function handleFormDataUpdate(data: any) {
currentFormData.value = data
emit('form-data-update', data)
}
// 处理审核数据更新
function handleApprovalDataUpdate(data: any) {
approvalData.value = data
}
// 处理表单组件挂载
function handleFormMounted({ nodeId, instance }: { nodeId: string; instance: any }) {
console.log('表单组件已挂载 - 节点:', nodeId)
}
// 提交处理
async function handleSubmit() {
if (!editableNode.value) {
message.warning('没有可编辑的表单')
return
}
if (!currentFormPanelRef.value) {
message.warning('表单组件未加载')
return
}
// 如果是审核节点,验证审核意见
if (props.isApprovalNode && approvalPanelRef.value) {
const isValidApproval = await approvalPanelRef.value.validate()
if (!isValidApproval) {
message.error('请填写完整的审核信息')
return
}
}
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,
formData: submitData,
procDefId: props.procDefId,
formComponent: currentFormPanelRef.value.getFormInstance()
}
// 如果是审核节点,添加审核数据
if (props.isApprovalNode && approvalPanelRef.value) {
const approval = await approvalPanelRef.value.getApprovalData()
finalSubmitData.approval = approval
}
console.log('最终提交数据:', finalSubmitData)
// 4. 触发提交事件
emit('submit', finalSubmitData)
message.success(props.isApprovalNode ? '审核提交成功' : '提交成功')
handleClose()
} catch (error: any) {
console.error('提交失败:', error)
message.error(error?.message || '提交失败,请重试')
} finally {
submitLoading.value = false
}
}
// 关闭抽屉
function handleClose() {
emit('update:visible', false)
emit('close')
}
// 重置数据
function resetFormData() {
if (currentFormPanelRef.value) {
currentFormPanelRef.value.resetFormData()
}
if (approvalPanelRef.value) {
approvalPanelRef.value.resetForm()
}
currentFormData.value = {}
approvalData.value = {}
}
// 监听抽屉打开
watch(() => props.visible, async (newVal) => {
if (newVal) {
resetFormData()
// 等待子组件渲染完成后重新加载数据
await nextTick()
if (currentFormPanelRef.value) {
await currentFormPanelRef.value.reloadFormData()
}
}
})
// 监听 dataId 变化
watch(() => props.dataId, async () => {
if (props.visible && currentFormPanelRef.value) {
await nextTick()
await currentFormPanelRef.value.reloadFormData()
}
})
// 监听外部数据变化
watch(() => props.externalFormData, (newData) => {
if (newData && editableNode.value && newData[editableNode.value.id]) {
currentFormData.value = newData[editableNode.value.id]
}
}, { deep: true })
onMounted(() => {
resetFormData()
})
defineExpose({
resetFormData,
getFormData: () => currentFormData.value,
getApprovalData: () => approvalData.value,
getCurrentFormData: async () => {
if (currentFormPanelRef.value) {
return await currentFormPanelRef.value.getFormData()
}
return currentFormData.value
},
validate: async () => {
if (currentFormPanelRef.value) {
const formValid = await currentFormPanelRef.value.validateForm()
if (props.isApprovalNode && approvalPanelRef.value) {
const approvalValid = await approvalPanelRef.value.validate()
return formValid && approvalValid
}
return formValid
}
return true
},
submit: handleSubmit
})
</script>
<style scoped lang="scss">
.workflow-form-drawer {
: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;
}
}
}
:deep(.ant-drawer-body) {
padding: 0;
height: 100%;
overflow: hidden;
}
:deep(.ant-drawer-footer) {
padding: 12px 24px;
border-top: 1px solid #e8eef2;
background-color: #fff;
}
}
.drawer-layout {
display: flex;
height: 100%;
overflow: hidden;
// 三栏布局
&.three-columns {
.history-panel {
width: 30%;
}
.form-wrapper {
width: 40%;
}
.approval-panel {
width: 30%;
}
}
// 两栏布局(普通表单节点)
&:not(.three-columns) {
.history-panel {
width: 40%;
}
.form-wrapper {
flex: 1;
}
}
}
.form-wrapper {
overflow: hidden;
display: flex;
flex-direction: column;
}
.drawer-footer {
display: flex;
justify-content: flex-end;
}
</style>
\ No newline at end of file
......@@ -5,91 +5,82 @@
:width="drawerWidth"
:closable="true"
:mask-closable="maskClosable"
:header-style="{ backgroundColor: '#018ffb', borderBottom: '1px solid #e8eef2' }"
@close="handleClose"
class="workflow-form-drawer"
style="padding: 0px;"
>
<div class="drawer-layout" :class="{ 'three-columns': showApprovalPanel }">
<HistoryPanel
:readonly-nodes="readonlyNodes"
:data-id="dataId"
:show-history-form-data="showHistoryFormData"
/>
<div class="form-wrapper">
<CurrentFormPanel
ref="currentFormPanelRef"
:editable-node="editableNode"
:data-id="dataId"
:external-form-data="externalFormData"
@update:form-data="handleFormDataUpdate"
@form-mounted="handleFormMounted"
/>
</div>
<!-- 选项卡模式 -->
<a-tabs v-model:activeKey="activeTabKey" type="card" class="form-tabs">
<!-- 只读选项卡:索引小于 currentNodeIndex 的节点 -->
<a-tab-pane
v-for="node in readonlyNodes"
:key="node.id"
:tab="node.name"
>
<template #tab>
<span>
{{ node.name }}
<a-tag color="blue" size="small" class="tab-tag">只读</a-tag>
</span>
</template>
<div class="tab-content readonly-content">
<component
:is="getComponent(node.formUrl || node.formListUrl)"
:disabled="true"
:readonly="true"
:form-data="getFormData(node.id)"
:current-flow-node="node"
/>
</div>
</a-tab-pane>
<!-- 可编辑选项卡:索引等于 currentNodeIndex 的节点 -->
<a-tab-pane
v-if="editableNode"
:key="editableNode.id"
:tab="editableNode.name"
>
<template #tab>
<span>
{{ editableNode.name }}
<a-tag color="green" size="small" class="tab-tag">可编辑</a-tag>
</span>
</template>
<div class="tab-content editable-content">
<component
:is="getComponent(editableNode.formUrl || editableNode.formListUrl)"
ref="editableFormRef"
:disabled="false"
:readonly="false"
:form-data="currentFormData"
:current-flow-node="editableNode"
@update:form-data="handleFormDataUpdate"
/>
</div>
</a-tab-pane>
</a-tabs>
<!-- 空状态 -->
<div v-if="!editableNode && readonlyNodes.length === 0" class="empty-state">
<a-empty description="未找到有效表单节点" />
</div>
<div v-if="showApprovalPanel" class="approval-wrapper">
<slot name="approval-panel" :approval-data="approvalData" :node="editableNode">
<ApprovalPanel
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"
@update:approval-data="handleApprovalDataUpdate"
/>
</slot>
</div>
</div>
<template #footer>
<div class="drawer-footer">
<a-button @click="handleClose">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
提交
</a-button>
</div>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted, defineAsyncComponent, h, watch } from 'vue'
import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import HistoryPanel from './HistoryPanel.vue'
import CurrentFormPanel from './CurrentFormPanel.vue'
import ApprovalPanel from './ApprovalPanel.vue'
interface WorkflowNode {
id: string
name: string
formName?: string
formUrl?: string
formListUrl?: string
procDefId?: string
dataId?: string
processTime?: string
processor?: string
comment?: string
nodeType?: string
[key: string]: any
}
const props = defineProps({
// 基础配置
visible: {
type: Boolean,
default: false
......@@ -100,13 +91,14 @@ const props = defineProps({
},
width: {
type: [Number, String],
default: 720
default: "90%"
},
maskClosable: {
type: Boolean,
default: false
},
// 当前节点索引(从0开始),这个索引对应的节点是可编辑的
// 工作流配置
currentNodeIndex: {
type: Number,
required: true,
......@@ -124,22 +116,104 @@ const props = defineProps({
procDefId: {
type: String,
default: ''
},
dataId: {
type: String,
default: ''
},
showHistoryFormData: {
type: Boolean,
default: false
},
// 审核面板配置
showApprovalPanel: {
type: Boolean,
default: false
},
approvalPanelTitle: {
type: String,
default: '审核意见'
},
submitButtonText: {
type: String,
default: '提交'
},
// 审核结果配置
approvalResultOptions: {
type: Array,
default: () => [
{
value: 'approved',
label: '通过',
icon: 'CheckCircleOutlined',
className: 'approved'
},
{
value: 'rejected',
label: '退回',
icon: 'CloseCircleOutlined',
className: 'rejected'
}
]
},
defaultApprovalResult: {
type: String,
default: 'approved'
},
// 审核意见配置
commentPlaceholder: {
type: String,
default: '请输入审核意见...'
},
requireCommentOnReject: {
type: Boolean,
default: true
},
// 退回节点配置
showRejectNodeSelect: {
type: Boolean,
default: true
},
rejectNodes: {
type: Array as () => WorkflowNode[],
default: () => []
},
canSelectRejectNode: {
type: Boolean,
default: true
},
// 其他配置
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'])
// 组件缓存
const componentCache = new Map()
const modules = import.meta.glob('@/views/**/*.vue')
// 状态
const loading = ref(false)
const submitLoading = ref(false)
const editableFormRef = ref()
const currentFormData = ref<any>({})
const formDataMap = ref<Record<string, any>>({})
const activeTabKey = ref<string>('')
const approvalData = ref<any>({})
const currentFormPanelRef = ref<InstanceType<typeof CurrentFormPanel> | null>(null)
const approvalPanelRef = ref<InstanceType<typeof ApprovalPanel> | null>(null)
// 计算属性
const drawerTitle = computed(() => props.title)
......@@ -151,14 +225,8 @@ const readonlyNodes = computed(() => {
return []
}
const idx = props.currentNodeIndex
console.log('只读节点 - 当前索引:', idx)
console.log('只读节点 - 所有节点:', props.workflowNodes.map((n, i) => `${i}:${n.name}`))
if (idx <= 0) return []
// 返回索引小于 idx 的节点
const nodes = props.workflowNodes.slice(0, idx)
console.log('只读节点:', nodes.map(n => n.name))
return nodes
return props.workflowNodes.slice(0, idx)
})
// 可编辑节点:索引等于 currentNodeIndex 的节点
......@@ -168,86 +236,25 @@ const editableNode = computed(() => {
}
const idx = props.currentNodeIndex
if (idx < 0 || idx >= props.workflowNodes.length) {
console.warn('可编辑节点索引无效:', idx)
return null
}
const node = props.workflowNodes[idx]
console.log('可编辑节点:', node?.name, '索引:', idx)
return node
return props.workflowNodes[idx]
})
// 获取表单数据
function getFormData(nodeId: string): any {
const data = formDataMap.value[nodeId] || {}
console.log('获取表单数据 - 节点:', nodeId, '数据:', data)
return data
}
// 获取或加载组件
function getComponent(url: string) {
if (!url) {
console.warn('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'
}
console.log('加载组件路径:', componentPath)
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 handleFormDataUpdate(data: any) {
currentFormData.value = data
emit('form-data-update', data)
}
function createEmptyComponent() {
return {
render: () => h('div', { style: 'color: #999; padding: 20px; text-align: center;' }, '该节点未配置表单')
}
// 处理审核数据更新
function handleApprovalDataUpdate(data: any) {
approvalData.value = data
}
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 handleFormMounted({ nodeId, instance }: { nodeId: string; instance: any }) {
console.log('表单组件已挂载 - 节点:', nodeId)
}
// 提交处理
......@@ -257,35 +264,61 @@ async function handleSubmit() {
return
}
if (editableFormRef.value && editableFormRef.value.validate) {
try {
await editableFormRef.value.validate()
} catch (error) {
message.error('请完善表单信息')
if (!currentFormPanelRef.value) {
message.warning('表单组件未加载')
return
}
// 如果显示审核面板,验证审核数据
if (props.showApprovalPanel && approvalPanelRef.value) {
const result = await approvalPanelRef.value.validate()
if (!result.valid) {
message.error(result.errors[0] || '请填写完整的审核信息')
return
}
}
let submitData = currentFormData.value
if (editableFormRef.value && editableFormRef.value.getFormData) {
submitData = editableFormRef.value.getFormData()
} else if (editableFormRef.value && editableFormRef.value.formData) {
submitData = editableFormRef.value.formData
}
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,
formData: submitData,
procDefId: props.procDefId,
formComponent: currentFormPanelRef.value.getFormInstance()
}
// 如果显示审核面板,添加审核数据
if (props.showApprovalPanel && approvalPanelRef.value) {
const approval = await approvalPanelRef.value.getApprovalData()
finalSubmitData.approval = approval
}
console.log('最终提交数据:', finalSubmitData)
// 4. 触发提交事件
emit('submit', finalSubmitData)
console.log('提交数据:', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: submitData,
procDefId: props.procDefId
})
emit('submit', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: submitData,
procDefId: props.procDefId
})
message.success(props.showApprovalPanel ? '审核提交成功' : '提交成功')
handleClose()
} catch (error: any) {
console.error('提交失败:', error)
message.error(error?.message || '提交失败,请重试')
} finally {
submitLoading.value = false
}
}
// 关闭抽屉
......@@ -296,208 +329,154 @@ function handleClose() {
// 重置数据
function resetFormData() {
currentFormData.value = {}
const newFormDataMap: Record<string, any> = {}
readonlyNodes.value.forEach(node => {
newFormDataMap[node.id] = props.externalFormData[node.id] || {}
})
formDataMap.value = newFormDataMap
if (editableNode.value && props.externalFormData[editableNode.value.id]) {
currentFormData.value = { ...props.externalFormData[editableNode.value.id] }
if (currentFormPanelRef.value) {
currentFormPanelRef.value.resetFormData()
}
// 设置默认激活的选项卡为可编辑的选项卡
if (editableNode.value) {
activeTabKey.value = editableNode.value.id
console.log('设置激活选项卡为可编辑节点:', editableNode.value.name)
} else if (readonlyNodes.value.length > 0) {
activeTabKey.value = readonlyNodes.value[0].id
console.log('设置激活选项卡为第一个只读节点:', readonlyNodes.value[0].name)
if (approvalPanelRef.value) {
approvalPanelRef.value.resetForm()
}
}
// 预加载组件
function preloadComponents() {
props.workflowNodes.forEach(node => {
const url = node.formUrl || node.formListUrl
if (url) {
getComponent(url)
}
})
currentFormData.value = {}
approvalData.value = {}
}
// 监听抽屉打开
watch(() => props.visible, (newVal) => {
watch(() => props.visible, async (newVal) => {
if (newVal) {
console.log('抽屉打开,currentNodeIndex:', props.currentNodeIndex)
console.log('所有节点:', props.workflowNodes)
resetFormData()
preloadComponents()
await nextTick()
if (currentFormPanelRef.value) {
await currentFormPanelRef.value.reloadFormData()
}
}
}, { immediate: true })
})
// 监听 dataId 变化
watch(() => props.dataId, async () => {
if (props.visible && currentFormPanelRef.value) {
await nextTick()
await currentFormPanelRef.value.reloadFormData()
}
})
// 监听外部数据变化
watch(() => props.externalFormData, (newData) => {
if (newData && Object.keys(newData).length > 0) {
console.log('外部数据变化:', newData)
const newFormDataMap = { ...formDataMap.value }
readonlyNodes.value.forEach(node => {
if (newData[node.id]) {
newFormDataMap[node.id] = newData[node.id]
}
})
formDataMap.value = newFormDataMap
if (editableNode.value && newData[editableNode.value.id]) {
currentFormData.value = newData[editableNode.value.id]
}
if (newData && editableNode.value && newData[editableNode.value.id]) {
currentFormData.value = newData[editableNode.value.id]
}
}, { deep: true })
onMounted(() => {
console.log('组件挂载,workflowNodes:', props.workflowNodes)
console.log('currentNodeIndex:', props.currentNodeIndex)
resetFormData()
preloadComponents()
})
defineExpose({
resetFormData,
getFormData: () => currentFormData.value,
getApprovalData: () => approvalData.value,
getCurrentFormData: async () => {
if (currentFormPanelRef.value) {
return await currentFormPanelRef.value.getFormData()
}
return currentFormData.value
},
validate: async () => {
if (editableFormRef.value && editableFormRef.value.validate) {
return await editableFormRef.value.validate()
if (currentFormPanelRef.value) {
const formValid = await currentFormPanelRef.value.validateForm()
if (props.showApprovalPanel && approvalPanelRef.value) {
const approvalResult = await approvalPanelRef.value.validate()
return formValid && approvalResult.valid
}
return formValid
}
return true
}
},
submit: handleSubmit
})
</script>
<style scoped lang="scss">
.workflow-form-drawer {
width: 100%;
height: 100%;
overflow-y: auto;
.form-card {
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
:deep(.ant-card-body) {
padding: 0;
}
}
.form-tabs {
:deep(.ant-tabs-nav) {
margin-bottom: 0;
background-color: #fafbfc;
padding: 0 16px;
border-bottom: 1px solid #e8eef2;
}
:deep(.ant-tabs-tab) {
padding: 12px 20px;
:deep(.ant-drawer-header) {
background-color: #f5f7fa;
border-bottom: 1px solid #e8eef2;
padding: 16px 24px;
.ant-drawer-title {
font-size: 16px;
font-weight: 500;
transition: all 0.3s;
&:hover {
color: #1890ff;
}
}
:deep(.ant-tabs-tab-active) {
.ant-tabs-tab-btn {
color: #1890ff;
}
}
:deep(.ant-tabs-ink-bar) {
background: #1890ff;
color: #1f2f3d;
}
}
.tab-content {
padding: 24px;
min-height: 400px;
background-color: #fff;
&.readonly-content {
background-color: #f5f5f5;
// 让只读内容区域内的所有表单输入框都显示为只读样式
:deep(input),
:deep(textarea),
:deep(.ant-input),
:deep(.ant-select-selector),
:deep(.ant-picker) {
background-color: #f5f5f5 !important;
cursor: not-allowed !important;
color: #666 !important;
}
.ant-drawer-close {
color: #8c8c8c;
:deep(.ant-input-affix-wrapper) {
background-color: #f5f5f5 !important;
&:hover {
color: #1f2f3d;
}
}
&.editable-content {
background-color: #fff;
}
}
.tab-tag {
margin-left: 8px;
font-size: 12px;
transform: scale(0.9);
display: inline-block;
:deep(.ant-drawer-body) {
padding: 0;
height: 100%;
overflow: hidden;
}
.empty-state {
padding: 60px 0;
text-align: center;
:deep(.ant-drawer-footer) {
padding: 12px 24px;
border-top: 1px solid #e8eef2;
background-color: red;
}
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
.drawer-layout {
display: flex;
height: 100%;
overflow: hidden;
// 三栏布局
&.three-columns {
.history-panel {
width: 30%;
}
.form-wrapper {
width: 40%;
}
.approval-wrapper {
width: 30%;
}
}
}
.drawer-footer {
text-align: right;
:deep(.ant-btn) {
margin-left: 8px;
// 两栏布局
&:not(.three-columns) {
.history-panel {
width: 40%;
}
&:first-child {
margin-left: 0;
.form-wrapper {
flex: 1;
}
}
}
:deep(.ant-drawer-body) {
padding: 16px;
background-color: #f5f7fa;
.form-wrapper {
overflow: hidden;
display: flex;
flex-direction: column;
}
.approval-wrapper {
overflow: hidden;
display: flex;
flex-direction: column;
}
:deep(.ant-drawer-footer) {
padding: 12px 16px;
border-top: 1px solid #e8eef2;
.drawer-footer {
display: flex;
justify-content: flex-end;
height: 2px;
background: #018ffb;
}
</style>
\ No newline at end of file
<template>
<BasicModal
@register="registerModal"
v-bind="$attrs"
:title="modalTitle"
:canFullscreen="true"
:maxHeight="600"
width="50%"
:showOkBtn="false"
cancelText="关闭"
@cancel="handleCancel"
<a-drawer
:title="drawerTitle"
:visible="visible"
:width="drawerWidth"
:closable="true"
:mask-closable="maskClosable"
@close="handleClose"
class="workflow-form-drawer"
>
<Card :bordered="false" class="form-modal-card">
<!-- 加载状态 -->
<div v-show="loading" class="loading-container">
<div class="loading-content">
<a-spin size="large" tip="表单加载中..." />
<!-- 选项卡模式 -->
<a-tabs v-model:activeKey="activeTabKey" type="card" class="form-tabs">
<!-- 只读选项卡:索引小于 currentNodeIndex 的节点 -->
<a-tab-pane
v-for="node in readonlyNodes"
:key="node.id"
:tab="node.name"
>
<template #tab>
<span>
{{ node.name }}
<a-tag color="blue" size="small" class="tab-tag">只读</a-tag>
</span>
</template>
<div class="tab-content readonly-content">
<component
:is="getComponent(node.formUrl || node.formListUrl)"
:disabled="true"
:readonly="true"
:form-data="getFormData(node.id)"
:current-flow-node="node"
/>
</div>
</div>
<!-- 错误状态 -->
<div v-show="hasError" class="error-container">
<a-result
status="error"
:title="errorTitle"
:sub-title="errorMessage"
>
<template #extra>
<a-button type="primary" @click="retryLoad">重新加载</a-button>
</template>
</a-result>
</div>
<!-- 内容区域 -->
<div class="form-content-container">
<!-- 外部表单(iframe类型) -->
<div v-show="formType == 2">
<!-- <div class="iframe-header">
<a-alert
type="info"
show-icon
message="外部表单"
description="此表单通过外部链接加载,请确保网络连接正常"
class="iframe-alert"
/>
</div> -->
<div class="iframe-container">
<iframe
v-if="formUrl"
:src="formUrl"
class="responsive-iframe"
:title="'表单-' + formTitle"
frameborder="0"
@load="handleIframeLoad"
@error="handleIframeError"
></iframe>
</div>
</div>
<div v-show="formType == 1" class="inner-form-wrapper">
<div class="form-body">
<FlowInnerForm ref="refInnerForm"/>
</div>
</div>
<div v-show="formType == 0 || !formType" class="unsupported-container">
<a-result
status="warning"
title="暂不支持的表单类型"
sub-title="当前表单类型配置有误或暂不支持"
>
<template #extra>
<a-space>
<a-button type="primary" @click="closeModal">关闭</a-button>
<a-button @click="handleReport">报告问题</a-button>
</a-space>
</template>
</a-result>
</a-tab-pane>
<!-- 可编辑选项卡:索引等于 currentNodeIndex 的节点 -->
<a-tab-pane
v-if="editableNode"
:key="editableNode.id"
:tab="editableNode.name"
>
<template #tab>
<span>
{{ editableNode.name }}
<a-tag color="green" size="small" class="tab-tag">可编辑</a-tag>
</span>
</template>
<div class="tab-content editable-content">
<component
:is="getComponent(editableNode.formUrl || editableNode.formListUrl)"
:ref="setEditableFormRef"
:disabled="false"
:readonly="false"
:form-data="currentFormData"
:current-flow-node="editableNode"
@update:form-data="handleFormDataUpdate"
/>
</div>
</a-tab-pane>
</a-tabs>
<!-- 空状态 -->
<div v-if="!editableNode && readonlyNodes.length === 0" class="empty-state">
<a-empty description="未找到有效表单节点" />
</div>
<template #footer>
<div class="drawer-footer">
<a-button @click="handleClose">取消</a-button>
<a-button type="primary" :loading="submitLoading" @click="handleSubmit">
提交
</a-button>
</div>
</Card>
</BasicModal>
</template>
</a-drawer>
</template>
<script lang="ts" setup>
import { ref, computed, onUnmounted, nextTick, watch } from 'vue';
import { Card, message } from 'ant-design-vue';
import { BasicModal, useModalInner } from '/@/components/Modal';
import FlowInnerForm from './FlowInnerForm.vue';
import { findFlowFormVal} from "/@/components/Process/api/todo"
// 状态定义
const loading = ref(false);
const submitLoading = ref(false);
const hasError = ref(false);
const errorTitle = ref('');
const errorMessage = ref('');
const showInnerForm = ref(false); // 控制内部表单显示
// 表单相关
const formType = ref<number>(0);
const formUrl = ref<string>('');
const formTitle = ref<string>('节点表单');
const formContent = ref<string>('');
const refInnerForm = ref<InstanceType<typeof FlowInnerForm> | null>(null);
import { ref, computed, onMounted, defineAsyncComponent, h, watch, ComponentPublicInstance } from 'vue'
import { message } from 'ant-design-vue'
interface WorkflowNode {
id: string
name: string
formUrl?: string
formListUrl?: string
procDefId?: string
[key: string]: any
}
// 计算属性
const modalTitle = computed(() => {
return formTitle.value || '节点表单';
});
// 监听 formType 变化
watch(() => formType.value, (newVal) => {
if (newVal == 1) {
// 当表单类型变为内部表单时,显示组件
showInnerForm.value = true;
// 等待组件渲染完成后再初始化数据
nextTick(() => {
//if (refInnerForm.value && formContent.value) {
initializeInnerForm();
//}
});
} else {
// 其他表单类型时隐藏
showInnerForm.value = false;
// 表单组件实例类型
interface FormComponentInstance extends ComponentPublicInstance {
validate?: () => Promise<any>
getFormData?: () => any
submitForm?: () => Promise<any> // 添加 submitForm 方法类型
formData?: any
[key: string]: any
}
const props = defineProps({
visible: {
type: Boolean,
default: false
},
title: {
type: String,
default: '表单处理'
},
width: {
type: [Number, String],
default: 720
},
maskClosable: {
type: Boolean,
default: false
},
// 当前节点索引(从0开始),这个索引对应的节点是可编辑的
currentNodeIndex: {
type: Number,
required: true,
default: 2
},
workflowNodes: {
type: Array as () => WorkflowNode[],
required: true,
default: () => []
},
externalFormData: {
type: Object as () => Record<string, any>,
default: () => ({})
},
procDefId: {
type: String,
default: ''
}
});
})
// 初始化内部表单数据
const initializeInnerForm = async () => {
if (!refInnerForm.value) {
console.warn('Inner form ref is not available');
return;
const emit = defineEmits(['update:visible', 'submit', 'close', 'form-data-update'])
// 组件缓存
const componentCache = new Map()
const modules = import.meta.glob('@/views/**/*.vue')
// 状态
const loading = ref(false)
const submitLoading = ref(false)
// 使用数组来存储表单组件实例
const editableFormRefs = ref<FormComponentInstance[]>([])
const currentFormData = ref<any>({})
const formDataMap = ref<Record<string, any>>({})
const activeTabKey = ref<string>('')
// 计算属性
const drawerTitle = computed(() => props.title)
const drawerWidth = computed(() => props.width)
// 只读节点:索引小于 currentNodeIndex 的节点
const readonlyNodes = computed(() => {
if (!props.workflowNodes || props.workflowNodes.length === 0) {
return []
}
const idx = props.currentNodeIndex
console.log('只读节点 - 当前索引:', idx)
console.log('只读节点 - 所有节点:', props.workflowNodes.map((n, i) => `${i}:${n.name}`))
try {
await refInnerForm.value.iniData(formContent.value);
console.log('Inner form initialized successfully');
} catch (error) {
console.error('Failed to initialize inner form:', error);
handleError('表单初始化失败', error);
if (idx <= 0) return []
const nodes = props.workflowNodes.slice(0, idx)
console.log('只读节点:', nodes.map(n => n.name))
return nodes
})
// 可编辑节点:索引等于 currentNodeIndex 的节点
const editableNode = computed(() => {
if (!props.workflowNodes || props.workflowNodes.length === 0) {
return null
}
};
const idx = props.currentNodeIndex
if (idx < 0 || idx >= props.workflowNodes.length) {
console.warn('可编辑节点索引无效:', idx)
return null
}
const node = props.workflowNodes[idx]
console.log('可编辑节点:', node?.name, '索引:', idx)
return node
})
// 设置表单组件 ref 的方法
function setEditableFormRef(el: any) {
if (el) {
// 清除旧的引用,只保留当前激活的可编辑表单
editableFormRefs.value = [el]
console.log('表单组件已挂载,组件方法:', Object.keys(el))
console.log('是否有 submitForm 方法:', typeof el.submitForm === 'function')
}
}
// 获取当前可编辑的表单组件实例
function getCurrentEditableForm(): FormComponentInstance | null {
return editableFormRefs.value[0] || null
}
// 获取表单数据
function getFormData(nodeId: string): any {
const data = formDataMap.value[nodeId] || {}
console.log('获取表单数据 - 节点:', nodeId, '数据:', data)
return data
}
// 表单准备就绪的回调
const handleFormReady = () => {
console.log('Inner form component is ready');
// 如果已经有数据,可以在这里初始化
if (formContent.value && refInnerForm.value) {
initializeInnerForm();
// 获取或加载组件
function getComponent(url: string) {
if (!url) {
console.warn('URL为空,返回空组件')
return createEmptyComponent()
}
};
// 模态框注册
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
try {
resetState();
loading.value = true;
// 重置内部表单显示状态
showInnerForm.value = false;
if (componentCache.has(url)) {
return componentCache.get(url)
}
const taskId = data.data.taskId
// 设置表单属性
formType.value = data.data?.formTp || 0;
formUrl.value = data.data?.formUrl || '';
formTitle.value = data.data?.formTitle || '节点表单';
formContent.value = data.data?.formContent || '';
console.log('Modal data received:', {
formType: formType.value,
formTitle: formTitle.value,
hasContent: !!formContent.value
});
// 根据表单类型处理
if (formType.value == 1) {
showInnerForm.value = true;
await nextTick();
if (refInnerForm.value) {
await initializeInnerForm();
await setNodeVals(taskId)
}
} else if (formType.value == 2) {
// if (!formUrl.value || !isValidUrl(formUrl.value)) {
// throw new Error('表单链接无效');
// }
}
// 设置模态框属性
setModalProps({
title: modalTitle.value,
confirmLoading: false,
});
loading.value = false;
} catch (error) {
handleError('表单加载失败', error);
}
});
// 方法定义
const isValidUrl = (url: string): boolean => {
try {
new URL(url);
return true;
} catch {
return false;
}
};
const resetState = () => {
loading.value = false;
submitLoading.value = false;
hasError.value = false;
errorTitle.value = '';
errorMessage.value = '';
// 不重置 showInnerForm,由 watch 控制
};
const handleError = (title: string, error: any) => {
console.error('表单错误:', error);
loading.value = false;
hasError.value = true;
errorTitle.value = title;
errorMessage.value = error.message || '未知错误,请检查配置或联系管理员';
message.error(title);
};
const retryLoad = () => {
resetState();
window.location.reload();
};
const handleIframeLoad = () => {
console.log('iframe加载完成');
loading.value = false;
};
const handleIframeError = () => {
handleError('外部表单加载失败', new Error('无法加载外部表单,请检查链接或网络连接'));
};
const handleCancel = () => {
closeModal();
};
const handleReport = () => {
message.info('问题报告功能开发中...');
};
const setNodeVals = async (taskId)=> {
if(taskId) {
await findFlowFormVal({taskId: taskId }).then((resValues) => {
nextTick(() => {
if(refInnerForm.value) {
refInnerForm.value.setFormData(resValues)
}
})
})
let componentPath = ''
if (url.includes('/views')) {
componentPath = `/src${url}`
} else {
componentPath = `/src/views${url}`
}
}
if (!componentPath.match(/\.(vue|js|ts|jsx|tsx)$/)) {
componentPath += '.vue'
}
onUnmounted(() => {
formUrl.value = '';
formContent.value = '';
refInnerForm.value = null;
});
</script>
console.log('加载组件路径:', componentPath)
<style lang="scss" scoped>
.form-modal-card {
height: 100%;
display: flex;
flex-direction: column;
:deep(.ant-card-body) {
flex: 1;
padding: 0;
display: flex;
flex-direction: column;
const loader = modules[componentPath]
if (!loader) {
console.error('未找到组件:', componentPath)
const ErrorComponent = createErrorComponent(`组件未找到: ${componentPath}`)
componentCache.set(url, ErrorComponent)
return ErrorComponent
}
}
.loading-container,
.error-container,
.unsupported-container {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
min-height: 500px;
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
}
.loading-content {
text-align: center;
:deep(.ant-spin-text) {
margin-top: 12px;
font-size: 14px;
color: #1890ff;
function createEmptyComponent() {
return {
render: () => h('div', { style: 'color: #999; padding: 20px; text-align: center;' }, '该节点未配置表单')
}
}
.form-content-container {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
function createErrorComponent(msg: string) {
return {
render: () => h('div', { style: 'color: red; padding: 20px;' }, msg)
}
}
.iframe-wrapper,
.inner-form-wrapper {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
// 处理表单数据更新
function handleFormDataUpdate(data: any) {
currentFormData.value = { ...currentFormData.value, ...data }
emit('form-data-update', currentFormData.value)
}
.iframe-header {
padding: 16px 24px 0;
/**
* 从表单组件获取数据
* 优先调用组件的 getFormData 方法,如果没有则返回 formData 属性或 currentFormData
*/
async function getFormDataFromComponent(): Promise<any> {
const formComponent = getCurrentEditableForm()
.iframe-alert {
border-radius: 6px;
:deep(.ant-alert-message) {
font-weight: 500;
if (!formComponent) {
console.warn('未找到表单组件实例')
return currentFormData.value
}
console.log('当前表单组件实例:', formComponent)
console.log('组件方法列表:', Object.keys(formComponent))
// 方式1:调用组件的 getFormData 方法
if (typeof formComponent.getFormData === 'function') {
try {
const data = await formComponent.getFormData()
console.log('通过 getFormData 方法获取的数据:', data)
return data
} catch (error) {
console.error('调用 getFormData 失败:', error)
}
}
// 方式2:获取组件的 formData 属性
if (formComponent.formData !== undefined) {
console.log('通过 formData 属性获取的数据:', formComponent.formData)
return formComponent.formData
}
// 方式3:如果组件有内部表单数据,尝试获取
if (formComponent.getValues && typeof formComponent.getValues === 'function') {
try {
const data = await formComponent.getValues()
console.log('通过 getValues 方法获取的数据:', data)
return data
} catch (error) {
console.error('调用 getValues 失败:', error)
}
}
// 方式4:返回本地维护的 currentFormData
console.log('使用本地维护的 currentFormData:', currentFormData.value)
return currentFormData.value
}
.iframe-container {
flex: 1;
padding: 16px 24px 24px;
height: calc(100vh - 50px);
min-height: 400px;
.responsive-iframe {
width: 100%;
height: 100%;
border: none;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: box-shadow 0.3s ease;
&:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
/**
* 验证表单数据
*/
async function validateForm(): Promise<boolean> {
const formComponent = getCurrentEditableForm()
if (!formComponent) {
return true
}
// 方式1:调用组件的 validate 方法
if (typeof formComponent.validate === 'function') {
try {
await formComponent.validate()
return true
} catch (error) {
console.error('表单验证失败:', error)
return false
}
}
// 方式2:如果组件有 vee-validate 或其他验证库的实例
if (formComponent.v$ && typeof formComponent.v$.$validate === 'function') {
try {
const isValid = await formComponent.v$.$validate()
return isValid
} catch (error) {
console.error('验证失败:', error)
return false
}
}
// 如果没有验证方法,默认通过
return true
}
.inner-form-wrapper {
.form-header {
padding: 20px 24px 0;
// 提交处理
async function handleSubmit() {
if (!editableNode.value) {
message.warning('没有可编辑的表单')
return
}
submitLoading.value = true
try {
const formComponent = getCurrentEditableForm()
.form-title {
margin: 0;
font-size: 18px;
font-weight: 600;
color: #1f2d3d;
// 🔥 优先调用子组件的 submitForm 方法
if (formComponent && typeof formComponent.submitForm === 'function') {
console.log('调用子组件的 submitForm 方法')
// 调用子组件的 submitForm 方法,并等待返回结果
const result = await formComponent.submitForm()
// 如果子组件的 submitForm 返回了数据,则使用返回的数据
if (result !== undefined) {
console.log('submitForm 返回的数据:', result)
// 触发提交事件
emit('submit', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: result,
procDefId: props.procDefId,
formComponent: formComponent
})
message.success('提交成功')
handleClose()
return
}
}
:deep(.ant-divider) {
margin: 16px 0;
// 如果没有 submitForm 方法或 submitForm 没有返回数据,则使用原来的逻辑
console.log('使用默认提交逻辑')
// 1. 先进行表单验证
const isValid = await validateForm()
if (!isValid) {
message.error('请完善表单信息')
return
}
// 2. 获取表单数据
const submitData = await getFormDataFromComponent()
console.log('最终提交数据:', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: submitData,
procDefId: props.procDefId
})
// 3. 触发提交事件
emit('submit', {
nodeId: editableNode.value.id,
nodeName: editableNode.value.name,
formData: submitData,
procDefId: props.procDefId,
formComponent: formComponent
})
message.success('提交成功')
// 提交成功后关闭抽屉
handleClose()
} catch (error: any) {
console.error('提交失败:', error)
message.error(error?.message || '提交失败,请重试')
} finally {
submitLoading.value = false
}
}
// 关闭抽屉
function handleClose() {
emit('update:visible', false)
emit('close')
// 关闭后清空表单引用
editableFormRefs.value = []
}
// 重置数据
function resetFormData() {
currentFormData.value = {}
const newFormDataMap: Record<string, any> = {}
readonlyNodes.value.forEach(node => {
newFormDataMap[node.id] = props.externalFormData[node.id] || {}
})
formDataMap.value = newFormDataMap
if (editableNode.value && props.externalFormData[editableNode.value.id]) {
currentFormData.value = { ...props.externalFormData[editableNode.value.id] }
}
.form-body {
flex: 1;
padding: 0 24px 24px;
overflow-y: auto;
min-height: 400px;
// 设置默认激活的选项卡为可编辑的选项卡
if (editableNode.value) {
activeTabKey.value = editableNode.value.id
console.log('设置激活选项卡为可编辑节点:', editableNode.value.name)
} else if (readonlyNodes.value.length > 0) {
activeTabKey.value = readonlyNodes.value[0].id
console.log('设置激活选项卡为第一个只读节点:', readonlyNodes.value[0].name)
}
}
// 响应式设计
@media (max-width: 768px) {
.form-modal-card {
:deep(.ant-card-body) {
padding: 0;
// 预加载组件
function preloadComponents() {
props.workflowNodes.forEach(node => {
const url = node.formUrl || node.formListUrl
if (url) {
getComponent(url)
}
})
}
// 监听抽屉打开
watch(() => props.visible, (newVal) => {
if (newVal) {
console.log('抽屉打开,currentNodeIndex:', props.currentNodeIndex)
console.log('所有节点:', props.workflowNodes)
resetFormData()
preloadComponents()
// 清空之前的表单引用
editableFormRefs.value = []
}
.iframe-container {
height: calc(100vh - 220px);
padding: 12px;
}, { immediate: true })
// 监听外部数据变化
watch(() => props.externalFormData, (newData) => {
if (newData && Object.keys(newData).length > 0) {
console.log('外部数据变化:', newData)
const newFormDataMap = { ...formDataMap.value }
readonlyNodes.value.forEach(node => {
if (newData[node.id]) {
newFormDataMap[node.id] = newData[node.id]
}
})
formDataMap.value = newFormDataMap
if (editableNode.value && newData[editableNode.value.id]) {
currentFormData.value = newData[editableNode.value.id]
}
}
.inner-form-wrapper {
.form-header,
.form-body {
padding-left: 16px;
padding-right: 16px;
}, { deep: true })
onMounted(() => {
console.log('组件挂载,workflowNodes:', props.workflowNodes)
console.log('currentNodeIndex:', props.currentNodeIndex)
resetFormData()
preloadComponents()
})
defineExpose({
resetFormData,
getFormData: () => currentFormData.value,
getCurrentFormData: getFormDataFromComponent,
validate: validateForm,
submit: handleSubmit
})
</script>
<style scoped lang="scss">
.workflow-form-drawer {
width: 100%;
height: 100%;
overflow-y: auto;
.form-tabs {
:deep(.ant-tabs-nav) {
margin-bottom: 0;
background-color: #fafbfc;
padding: 0 16px;
border-bottom: 1px solid #e8eef2;
}
:deep(.ant-tabs-tab) {
padding: 12px 20px;
font-weight: 500;
transition: all 0.3s;
&:hover {
color: #1890ff;
}
}
:deep(.ant-tabs-tab-active) {
.ant-tabs-tab-btn {
color: #1890ff;
}
}
:deep(.ant-tabs-ink-bar) {
background: #1890ff;
}
}
}
// 动画效果
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.tab-content {
padding: 24px;
min-height: 400px;
background-color: #fff;
&.readonly-content {
background-color: #f5f5f5;
:deep(input),
:deep(textarea),
:deep(.ant-input),
:deep(.ant-select-selector),
:deep(.ant-picker) {
background-color: #f5f5f5 !important;
cursor: not-allowed !important;
color: #666 !important;
}
:deep(.ant-input-affix-wrapper) {
background-color: #f5f5f5 !important;
}
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
&.editable-content {
background-color: #fff;
}
}
// 滚动条美化
.form-body::-webkit-scrollbar {
width: 6px;
.tab-tag {
margin-left: 8px;
font-size: 12px;
transform: scale(0.9);
display: inline-block;
}
.empty-state {
padding: 60px 0;
text-align: center;
}
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
&:hover {
background: #a8a8a8;
}
}
}
.form-body::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
.drawer-footer {
text-align: right;
:deep(.ant-btn) {
margin-left: 8px;
&:first-child {
margin-left: 0;
}
}
}
.form-body::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
:deep(.ant-drawer-body) {
padding: 16px;
background-color: #f5f7fa;
}
.form-body::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
:deep(.ant-drawer-footer) {
padding: 12px 16px;
border-top: 1px solid #e8eef2;
}
</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,23 +25,60 @@
: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);
const drawerTitle = ref('表单处理');
......@@ -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);
......@@ -179,6 +233,17 @@
formData
});
};
function handleSuccess(response: any) {
console.log('任务处理成功:', response)
// 刷新列表等操作
}
function handleError(error: any) {
console.error('任务处理失败:', error)
}
defineExpose({
openHistoryForms,
......@@ -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 || '';
const userid = currentNode.assignee || '';
const nodeId = currentNode.id
const deployId = currentNode.deployId || '';
const procDefId = currentNode.procDefId || '';
const attributes = currentNode.attributes || {};
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.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 || '';
......
......@@ -40,6 +40,7 @@
}
})
const emit = defineEmits(['callback'])
//注册model
const [registerExecuteModal, { openModal: openExecuteModal }] = useModal();
......@@ -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 handlePlanApproval(record: Recordable) {
openExecuteModal(true, {
record,
isUpdate: true,
showFooter: true,
});
emit("callback",record)
}
function handleSuccess() {
......
......@@ -25,7 +25,7 @@
import { list} from './StProblemCheck.api';
import StProblemCheckExecuteModal from './components/StProblemCheckExecuteModal.vue';
const emit = defineEmits(['callback'])
const emit = defineEmits(['callback','sendWorkFlow'])
const props = defineProps({
beforeFlowNode: {
......@@ -72,6 +72,10 @@
function handlePlan(record: Recordable) {
emit("callback",record)
}
function handleSendNext(record: Recordable) {
emit("sendWorkFlow",record)
}
function handleSuccess() {
(selectedRowKeys.value = []) && reload();
......@@ -80,12 +84,18 @@
function getTableAction(record) {
return [
{
label: '制定整改计划',
label: '制定计划',
onClick: handlePlan.bind(null, record),
},
{
label: '提交',
onClick: handleSendNext.bind(null, record),
},
];
}
</script>
<style scoped></style>
......@@ -23,6 +23,7 @@
props: {
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
dataId: propTypes.string.def(''),
},
setup(props) {
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
......@@ -41,8 +42,8 @@
let formData = {};
const queryByIdUrl = '/problem/stProblemCheck/queryById/';
async function initFormData() {
let params = { id: props.formData.dataId };
async function initFormData(did) {
let params = { id: props.dataId||did };
const data = await defHttp.get({ url: queryByIdUrl, params });
formData = { ...data };
//设置表单的值
......@@ -52,19 +53,17 @@
}
async function submitForm() {
alert(888)
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params);
await saveOrUpdate(params, true);
}
//initFormData();
return {
registerForm,
formDisabled,
submitForm,
initFormData
};
},
});
......
......@@ -51,11 +51,11 @@
values.deployId = bpmFlowData.value?.deployId || '';
setModalProps({ confirmLoading: true });
//提交表单
await saveOrUpdate(values, isUpdate.value);
const retData = await saveOrUpdate(values, isUpdate.value);
//关闭弹窗
closeModal();
//刷新列表
emit('success');
emit('success',retData);
} finally {
setModalProps({ confirmLoading: false });
}
......
......@@ -12,7 +12,7 @@
import { computed, defineComponent } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { propTypes } from '/@/utils/propTypes';
import { getPlanFormSchema } from '../StProblemCheck.data';
import { getPlanFormSchema,formSchema } from '../StProblemCheck.data';
import { saveOrUpdate, } from '../StProblemCheck.api';
export default defineComponent({
......@@ -23,11 +23,12 @@
props: {
formData: propTypes.object.def({}),
formBpm: propTypes.bool.def(true),
dataId: propTypes.string.def(''),
},
setup(props) {
const [registerForm, { setFieldsValue, setProps, getFieldsValue }] = useForm({
labelWidth: 150,
schemas: getPlanFormSchema(props.formData),
schemas: formSchema,
showActionButtonGroup: false,
baseColProps: { span: 24 },
});
......@@ -41,29 +42,24 @@
let formData = {};
const queryById = '/problem/stProblemCheck/queryById';
async function initFormData() {
let params = { id: props.formData.dataId };
async function initFormData(did) {
let params = { id: props.dataId||did };
const data = await defHttp.get({ url: queryById, params });
formData = { ...data };
//设置表单的值
await setFieldsValue(formData);
//默认是禁用
await setProps({ disabled: formDisabled.value });
}
async function submitForm() {
let data = getFieldsValue();
let params = Object.assign({}, formData, data);
console.log('表单数据', params);
await saveOrUpdate(params, true);
}
initFormData();
return {
registerForm,
formDisabled,
submitForm,
initFormData
};
},
});
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论