columnNumber: 14; 元素类型为 “id“ 的内容必须匹配 “EMPTY“。

本文介绍如何优化MyBatis的结果映射,通过简化XML配置来提高代码的可读性和维护性。从原始的冗长配置转换为简洁的单行配置,详细展示了属性与字段的映射过程。

<id property="goods.goodsId" column="goods_id"></id>

<!-- 普通字段与属性的映射 -->

<result property="goods.title" column="title"></result>

<result property="goods.subTitle" column="sub_title"></result>

<result property="goods.originalCost" column="original_cost">

</result>

<result property="goods.currentPrice" column="current_price">

</result>

<result property="goods.discount" column="discount"></result>

<result property="goods.isFreeDelivery"

column="is_free_delivery"></result>

<result property="goods.categoryId" column="category_id">

</result>

<result property="categoryName" column="category_name">

</result>

 

改成

 

<id column="goods_id" property="goods.goodsId"/>
<!-- 普通字段与属性的映射 -->
<result property="goods.title" column="title"/>
<result property="goods.subTitle" column="sub_title"/>
<result property="goods.originalCost" column="original_cost"/>
<result property="goods.currentPrice" column="current_price"/>
<result property="goods.discount" column="discount"/>
<result property="goods.isFreeDelivery" column="is_free_delivery"/>
<result property="goods.categoryId" column="category_id"/>
<result property="categoryName" column="category_name"/>
{"code":200,"msg":"Operation successful","data":{"records":[{"id":"1966467227383889922","appId":3,"promoterId":12345,"targetUserId":12452903,"amount":"2.43","level":0,"settlementStatus":0,"settlementAt":null,"isDelete":0,"calculate":"1","createTime":"2025-09-12 11:41:37","updateTime":"2025-09-12 11:49:58","deleteTime":0,"remark":"昵称:兔兔很饿"}],"total":1,"current":1,"size":20,"last":true,"pages":1}} <template> <view class="distribution-list"> <wd-navbar :showBack="true" title="业绩明细" /> <!-- 列表内容 --> <scroll-view class="list-container" scroll-y> <!-- 列表项 - 添加点击事件 --> <view class="list-item" v-for="(user, index) in list" :key="index" @click="handleItemClick(user)" > <view class="user-info"> <!-- 用户基本信息 --> <view class="user-basic"> <view class="user-main"> <view class="user-nickname">{{ user.nickname }}</view> <view class="user-id">ID: {{ user.myselfId }}</view> </view> </view> <!-- 用户电话 --> <view class="user-phone"> {{ formatPhone(user.mobile) }} </view> </view> <!-- 分割线 --> <view class="divider" v-if="index !== list.length - 1"></view> </view> <!-- 空状态 --> <view v-if="list.length === 0" class="empty-state"> <uni-icons type="contact" size="60" color="#ccc"></uni-icons> <view class="empty-text">暂无分销用户</view> </view> </scroll-view> </view> </template> <script lang="ts" setup> import { getPageUserList, getPageUserListByparentId } from '@/api/business/mobile/appApi' import { getPageUsers } from '@/api/business/mobile/homeApi' import { ref } from 'vue' // 导入路由相关方法(根据实际项目路由方案调整) import { useRouter } from 'vue-router' const router = useRouter() const list = ref([]) const page = ref(1) const limit = ref(200) const isLoading = ref(true) const pageUsers = ref({}) // 列表项点击事件处理函数 const handleItemClick = (user) => { // 跳转到业绩详情页,并携带myselfId参数 uni.navigateTo({ url: `/pages/center/distributionDetail?targetUserId=${user.myselfId}` }) } // 获取列表 const getList = (init: boolean) => { if (init) { page.value = 1 list.value = [] isLoading.value = true } if (!isLoading.value) { return false } const userId = uni.getStorageSync('userId') if (!userId) { console.error('userId 无效') isLoading.value = false return; } getPageUserListByparentId(page.value, limit.value, userId) .then(res => { const resList = res[1]; if (!resList) { isLoading.value = false; return; } const totalPages = resList.pages const currentPage = resList.current if (currentPage < totalPages) { page.value++; } else { isLoading.value = false; } const currentMyselfId = resList.records[0]?.myselfId; if (!currentMyselfId) { isLoading.value = false; return; } getPageUserList(page.value, limit.value, currentMyselfId) .then(res2 => { const resList2 = res2[1]; if (!resList2) { isLoading.value = false; return; } list.value = list.value.concat(resList2.records); const totalPages2 = resList2.pages; const currentPage2 = resList2.current; if (currentPage2 < totalPages2) { page.value++; } isLoading.value = false; }) .catch(err2 => { console.error('getPageUserList 失败', err2); isLoading.value = false; }); }) .catch(err => { console.error('getPageUserListByparentId 失败', err); isLoading.value = false; }); }; onLoad(() => { getPageUsers(uni.getStorageSync('userId')).then(res => { pageUsers.value = res[1]['records'][0] getList(true) }) }) onReachBottom(() => { getList(false) }) // 格式化电话号码(中间四位用*代替) const formatPhone = (phone) => { if (!phone) return ''; return phone.replace(/(\d{3})(\d{4})(\d{4})/, '$1****$3'); }; </script> <style scoped> .distribution-list { min-height: 100vh; background-color: #f5f5f5; } /* 导航栏样式 */ .navbar { display: flex; align-items: center; padding: 20rpx 30rpx; background-color: #fff; border-bottom: 1px solid #eee; } .back-btn { width: 60rpx; height: 60rpx; display: flex; align-items: center; justify-content: center; } .nav-title { flex: 1; text-align: center; font-size: 36rpx; font-weight: 600; color: #333; } /* 列表容器 */ .list-container { height: calc(100vh - 200rpx); } /* 列表项样式 - 添加点击反馈 */ .list-item { background-color: #fff; padding: 25rpx 30rpx; cursor: pointer; /* 显示指针样式 */ transition: background-color 0.2s; /* 过渡效果 */ } /* 点击时的背景色变化 */ .list-item:active { background-color: #f0f0f0; } .user-info { display: flex; justify-content: space-between; align-items: center; } .user-basic { display: flex; align-items: center; } .user-main { display: flex; flex-direction: column; } .user-nickname { font-size: 32rpx; color: #333; margin-bottom: 5rpx; } .user-id { font-size: 26rpx; color: #888; } .user-phone { font-size: 30rpx; color: #555; } /* 分割线 */ .divider { height: 1px; background-color: #f5f5f5; margin-top: 25rpx; } /* 空状态样式 */ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 200rpx; gap: 30rpx; } .empty-text { font-size: 32rpx; color: #888; } </style> <template> <view class="distribution-detail"> <!-- 导航栏:增加背景色与文字加粗 --> <wd-navbar :showBack="true" title="分销业绩详情" title-style="font-weight: 600;" background="#ffffff" border-bottom="true" /> <!-- 业绩列表:优化滚动区域与间距 --> <scroll-view class="detail-list" scroll-y> <!-- 业绩统计卡片:顶部展示核心数据 --> <view class="stat-card" v-if="detailList.length > 0 && !isLoading"> <view class="stat-item"> <view class="stat-label">累计业绩</view> <view class="stat-value">{{ totalAmount }} 元</view> </view> <view class="stat-item"> <view class="stat-label">有效记录</view> <view class="stat-value">{{ totalCount }} 条</view> </view> <view class="stat-item"> <view class="stat-label">最近更新</view> <view class="stat-value">{{ lastUpdateTime }}</view> </view> </view> <!-- 业绩列表项:展示完整信息 --> <view class="detail-item" v-for="(item, index) in detailList" :key="item.id"> <!-- 左侧:用户信息与业绩标题 --> <view class="item-left"> <!-- 用户昵称标签 --> <view class="user-tag"> <uni-icons type="person" size="20" color="#409eff" class="tag-icon" /> <view class="tag-text">{{ item.remark || '未知用户' }}</view> </view> <!-- 业绩标题:区分业绩类型 --> <view class="info-title"> <text>{{ getLevelText(item.level) }}</text> <text class="title-divider">|</text> <text>业绩记录</text> </view> </view> <!-- 右侧:金额与时间区域 --> <view class="item-right"> <!-- 业绩金额:增加货币符号 --> <view class="info-content">¥{{ item.amount || '0.00' }}</view> <!-- 结算状态标签:区分不同状态 --> <view class="settlement-tag" :class="getSettlementClass(item.settlementStatus)"> {{ getSettlementText(item.settlementStatus) }} </view> </view> <!-- 底部:时间与ID信息 --> <view class="item-footer"> <view class="item-time"> <uni-icons type="clock" size="22" color="#888" class="time-icon" /> {{ item.createTime || '暂无时间' }} </view> <view class="item-id"> 记录ID:<text class="id-text">{{ item.id || '无' }}</text> </view> </view> <!-- 分割线:优化样式 --> <view class="divider" v-if="index !== detailList.length - 1"></view> </view> <!-- 空状态:优化图标与文案 --> <view v-if="detailList.length === 0 && !isLoading" class="empty-state"> <uni-icons type="creditcard" size="70" color="#c0c4cc" class="empty-icon" /> <view class="empty-text">暂无业绩记录</view> <view class="empty-desc">您的分销业绩将在这里实时展示</view> </view> <!-- 加载中状态:新增加载动画 --> <view v-if="isLoading" class="loading-state"> <uni-icons type="spinner" size="36" color="#409eff" animation="spin" /> <view class="loading-text">加载中...</view> </view> </scroll-view> </view> </template> <script lang="ts" setup> import { getPageDistributionListByparentId } from '@/api/business/mobile/appApi' import { ref, computed } from 'vue'; import { onLoad, onReachBottom } from '@dcloudio/uni-app' // 状态管理 const detailList = ref<any[]>([]) // 业绩列表数据 const isLoading = ref(true) // 加载状态 const page = ref(1) // 当前页码 const limit = ref(20) // 每页条数 const targetUserId = ref('') // 目标用户ID const totalCount = ref(0) // 总记录数 const totalAmount = ref('0.00') // 累计业绩金额 const lastUpdateTime = ref('暂无') // 最近更新时间 /** * 获取业绩详情数据 * @param init 是否初始化(下拉刷新时重置) */ const getDetailList = (init: boolean = false) => { if (init) { page.value = 1 detailList.value = [] totalCount.value = 0 totalAmount.value = '0.00' lastUpdateTime.value = '暂无' isLoading.value = true } // 校验参数 if (!targetUserId.value) { console.error('缺少targetUserId参数') isLoading.value = false return } // 调用接口(参数顺序与接口匹配) getPageDistributionListByparentId( page.value, limit.value, targetUserId.value ).then(res => { const resData = res.data || {} const records = resData.records || [] // 更新列表数据 detailList.value = init ? records : detailList.value.concat(records) // 更新统计数据 totalCount.value = resData.total || 0 calculateTotalAmount() // 计算累计金额 updateLastUpdateTime(records) // 更新最近时间 // 处理分页:如果当前页小于总页数,页码+1(用于下一页加载) if (resData.current < resData.pages) { page.value++ } }).catch(err => { console.error('获取业绩详情失败', err) uni.showToast({ title: '加载失败,请重试', icon: 'none', duration: 2000 }) }).finally(() => { isLoading.value = false // 无论成功失败,都关闭加载状态 }) } /** * 计算累计业绩金额 */ const calculateTotalAmount = () => { const total = detailList.value.reduce((sum, item) => { return sum + Number(item.amount || 0) }, 0) totalAmount.value = total.toFixed(2) // 保留2位小数 } /** * 更新最近更新时间 * @param newRecords 新获取的记录 */ const updateLastUpdateTime = (newRecords: any[]) => { if (newRecords.length === 0) return // 找出最新的时间(按createTime降序) const latestRecord = [...detailList.value].sort((a, b) => { return new Date(b.createTime).getTime() - new Date(a.createTime).getTime() })[0] lastUpdateTime.value = latestRecord.createTime || '暂无' } /** * 业绩等级文本转换(0=一级分销,1=二级分销...) * @param level 等级数字 */ const getLevelText = (level: number) => { const levelMap = { 0: '一级分销', 1: '二级分销', 2: '三级分销', default: '普通分销' } return levelMap[level as keyof typeof levelMap] || levelMap.default } /** * 结算状态文本转换(0=待结算,1=已结算,2=已取消) * @param status 状态数字 */ const getSettlementText = (status: number) => { const statusMap = { 0: '待结算', 1: '已结算', 2: '已取消', default: '未知状态' } return statusMap[status as keyof typeof statusMap] || statusMap.default } /** * 结算状态样式类(不同状态不同颜色) * @param status 状态数字 */ const getSettlementClass = (status: number) => { const classMap = { 0: 'status-pending', // 待结算(蓝色) 1: 'status-completed', // 已结算(绿色) 2: 'status-canceled', // 已取消(灰色) default: 'status-default' } return classMap[status as keyof typeof classMap] || classMap.default } // 页面加载时获取参数并初始化数据 onLoad((options) => { if (options?.targetUserId) { targetUserId.value = options.targetUserId getDetailList(true) // 初始化加载 } else { console.error('未接收到targetUserId参数') uni.showToast({ title: '参数错误', icon: 'none', duration: 2000 }) } }) // 下拉加载更多(触底时触发) onReachBottom(() => { // 避免重复加载:当前不在加载中,且还有更多数据(通过totalCount判断) if (!isLoading.value && detailList.value.length < totalCount.value) { getDetailList() } }) </script> <style scoped> /* 页面主容器 */ .distribution-detail { min-height: 100vh; background-color: #f7f8fa; } /* 滚动列表容器:优化高度计算(适配不同设备) */ .detail-list { height: calc(100vh - 100rpx); padding: 20rpx 30rpx; box-sizing: border-box; } /* 业绩统计卡片:顶部核心数据展示 */ .stat-card { background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%); border-radius: 16rpx; padding: 30rpx; margin-bottom: 30rpx; color: #ffffff; display: flex; justify-content: space-between; box-shadow: 0 8rpx 20rpx rgba(64, 158, 255, 0.15); } .stat-item { display: flex; flex-direction: column; align-items: center; width: 33.33%; } .stat-label { font-size: 26rpx; opacity: 0.9; margin-bottom: 10rpx; } .stat-value { font-size: 36rpx; font-weight: 600; } /* 业绩列表项:卡片式设计 */ .detail-item { background-color: #ffffff; border-radius: 16rpx; padding: 30rpx; margin-bottom: 24rpx; box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05); transition: box-shadow 0.3s ease; } /* 列表项 hover 效果(增强交互) */ .detail-item:hover { box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.08); } /* 列表项左侧:用户信息区域 */ .item-left { margin-bottom: 20rpx; } /* 用户昵称标签 */ .user-tag { display: flex; align-items: center; color: #409eff; font-size: 26rpx; margin-bottom: 12rpx; } .tag-icon { margin-right: 8rpx; } /* 业绩标题 */ .info-title { font-size: 32rpx; color: #303133; font-weight: 500; } .title-divider { color: #c0c4cc; margin: 0 12rpx; } /* 列表项右侧:金额与状态区域 */ .item-right { display: flex; justify-content: space-between; align-items: center; margin-bottom: 24rpx; } /* 业绩金额 */ .info-content { font-size: 38rpx; color: #e63946; font-weight: 600; } /* 结算状态标签 */ .settlement-tag { padding: 6rpx 16rpx; border-radius: 20rpx; font-size: 24rpx; font-weight: 500; } /* 结算状态样式:待结算(蓝色) */ .status-pending { background-color: #e6f7ff; color: #1890ff; } /* 结算状态样式:已结算(绿色) */ .status-completed { background-color: #f0fff4; color: #52c41a; } /* 结算状态样式:已取消(灰色) */ .status-canceled { background-color: #f5f5f5; color: #999; } /* 列表项底部:时间与ID区域 */ .item-footer { display: flex; justify-content: space-between; align-items: center; font-size: 24rpx; color: #888; } /* 时间区域 */ .item-time { display: flex; align-items: center; } .time-icon { margin-right: 8rpx; } /* ID区域 */ .item-id { color: #999; } .id-text { color: #666; font-family: monospace; /* 等宽字体,ID更易读 */ } /* 分割线 */ .divider { height: 2rpx; background-color: #f5f7fa; margin-top: 24rpx; } /* 空状态 */ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding-top: 250rpx; gap: 24rpx; } .empty-icon { margin-bottom: 16rpx; } .empty-text { font-size: 34rpx; color: #606266; font-weight: 500; } .empty-desc { font-size: 26rpx; color: #909399; } /* 加载中状态 */ .loading-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 100rpx 0; gap: 20rpx; } .loading-text { font-size: 28rpx; color: #409eff; } </style> 为什么页面没有展示查询出来的值?
09-16
<script setup lang="ts"> import { computed, defineAsyncComponent, h, nextTick, onMounted, ref, render, watch } from 'vue' import { ElButton, ElMessage } from 'element-plus' import { useWorkflowStoreHook } from '@/stores/modules/workflow' import { cloneDeep } from '@/utils/general' import DefaultProps from './DefaultNodeProps' defineOptions({ name: 'process-tree', }) // 初始化核心依赖 const emit = defineEmits(['selectedNode']) // 响应式数据定义 const valid = ref(true) const _rootContainer = ref(null) // 组件根元素引用 const nodeRefs = ref({}) // 存储所有节点的 ref(替代原 this.$refs) // 计算属性 const nodeMap = computed(() => useWorkflowStoreHook().nodeMap) const dom = computed(() => useWorkflowStoreHook().design.process) // 节点类型与组件映射 const componentMap = { approval: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/ApprovalNode'),), cc: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/CcNode')), concurrent: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/ConcurrentNode'),), condition: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/ConditionNode'),), delay: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/DelayNode')), empty: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/EmptyNode')), inclusive: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/InclusiveNode'),), node: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/Node')), root: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/RootNode')), subprocess: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/SubprocessNode'),), task: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/TaskNode')), trigger: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/TriggerNode')), default: defineAsyncComponent(() => import('@/views/workflow/common/process/nodes/Node')), } // 节点类型判断方法 const isPrimaryNode = (node: any) => { return ( node && ['APPROVAL', 'CC', 'DELAY', 'ROOT', 'SUBPROCESS', 'TASK', 'TRIGGER'].includes(node.type) ) } const isBranchNode = (node: any) => { return node && ['CONCURRENT', 'CONDITION', 'INCLUSIVE'].includes(node.type) } const isEmptyNode = (node: any) => { return node && node.type === 'EMPTY' } const isConditionNode = (node: any) => { return node?.type === 'CONDITION' } const isBranchSubNode = (node: any) => { return node && ['CONCURRENT', 'CONDITION', 'INCLUSIVE'].includes(node.type) } const isInclusiveNode = (node: any) => { return node?.type === 'INCLUSIVE' } const isConcurrentNode = (node: any) => { return node?.type === 'CONCURRENT' } // 核心业务方法实现 /** * 生成随机节点 ID */ const getRandomId = () => { return `node_${new Date().getTime().toString().substring(5)}${Math.round(Math.random() * 9000 + 1000)}` } /** * 节点 ID 映射到 nodeMap */ const toMapping = (node: any) => { if (node && node.id) { nodeMap.value.set(node.id, node) } } /** * 插入分支遮挡线条 */ const insertCoverLine = (index: number, doms: any[], branchs: any[]) => { if (index === 0) { // 最左侧分支 doms.unshift(h('div', { class: 'line-top-left' }, [])) doms.unshift(h('div', { class: 'line-bot-left' }, [])) } else if (index === branchs.length - 1) { // 最右侧分支 doms.unshift(h('div', { class: 'line-top-right' }, [])) doms.unshift(h('div', { class: 'line-bot-right' }, [])) } } /** * 解码并插入节点 DOM */ const decodeAppendDom = (node: any, domList: any[], props = {}) => { props.config = node const compKey = node.type.toLowerCase() const TargetComp = componentMap[compKey as keyof typeof componentMap] if (!TargetComp) { console.warn(`未找到匹配的节点组件:${compKey}`) return } // 插入节点组件到 DOM 列表 domList.unshift( h(TargetComp, { props: props, key: node.id, ref: (el: any) => { if (el) nodeRefs.value[node.id] = el }, onInsertNode: (type: string) => insertNode(type, node), onDelNode: () => delNode(node), onSelected: () => selectNode(node), onCopy: () => copyBranch(node), onLeftMove: () => branchMove(node, -1), onRightMove: () => branchMove(node, 1), }), ) } /** * 递归生成流程树 DOM */ const buildDomTree = (node: any): any[] => { if (!node) return [] toMapping(node) // 1. 普通业务节点 if (isPrimaryNode(node)) { const childDoms = buildDomTree(node.children) decodeAppendDom(node, childDoms) return [h('div', { class: 'primary-node' }, childDoms)] } // 2. 分支节点 if (isBranchNode(node)) { let index = 0 const branchItems = node.branchs.map((branchNode: any) => { toMapping(branchNode) const childDoms = buildDomTree(branchNode.children) decodeAppendDom(branchNode, childDoms, { level: index + 1, size: node.branchs.length, }) insertCoverLine(index, childDoms, node.branchs) index++ return h('div', { class: 'branch-node-item' }, childDoms) }) // 插入「添加分支/条件」按钮 branchItems.unshift( h('div', { class: 'add-branch-btn' }, [ h( ElButton, { class: 'add-branch-btn-el', size: 'small', round: true, onClick: () => addBranchNode(node), }, () => `添加${isConditionNode(node) ? '条件' : '分支'}`, ), ]), ) const branchDom = [h('div', { class: 'branch-node' }, branchItems)] const afterChildDoms = buildDomTree(node.children) return [h('div', {}, [branchDom, afterChildDoms])] } // 3. 空节点 if (isEmptyNode(node)) { const childDoms = buildDomTree(node.children) decodeAppendDom(node, childDoms) return [h('div', { class: 'empty-node' }, childDoms)] } // 4. 末端节点 return [] } /** * 复制分支节点 */ const copyBranch = (node: any) => { const parentNode = nodeMap.value.get(node.parentId) if (!parentNode) return const newBranchNode = cloneDeep(node) newBranchNode.name = `${newBranchNode.name}-copy` forEachNode(parentNode, newBranchNode, (parent: any, currentNode: any) => { currentNode.id = getRandomId() currentNode.parentId = parent.id }) const insertIndex = parentNode.branchs.indexOf(node) parentNode.branchs.splice(insertIndex, 0, newBranchNode) nextTick(() => { renderProcessTree() }) } /** * 分支节点左右移动 */ const branchMove = (node: any, offset: number) => { const parentNode = nodeMap.value.get(node.parentId) if (!parentNode) return const currentIndex = parentNode.branchs.indexOf(node) const targetIndex = currentIndex + offset if (targetIndex < 0 || targetIndex >= parentNode.branchs.length) { ElMessage.info('已达边界,无法继续移动') return } [parentNode.branchs[currentIndex], parentNode.branchs[targetIndex]] = [ parentNode.branchs[targetIndex], parentNode.branchs[currentIndex], ] nextTick(() => { renderProcessTree() }) } /** * 选中节点 */ const selectNode = (node: any) => { useWorkflowStoreHook().selectedNode = node emit('selectedNode', node) } /** * 插入新节点 */ const insertNode = (type: string, parentNode: any) => { if (_rootContainer.value) { _rootContainer.value.click() } const afterNode = parentNode.children parentNode.children = { id: getRandomId(), parentId: parentNode.id, props: {}, type: type, } switch (type) { case 'APPROVAL': case 'SUBPROCESS': insertApprovalNode(parentNode) break case 'CC': insertCcNode(parentNode) break case 'CONCURRENT': insertConcurrentNode(parentNode) break case 'CONDITION': insertConditionNode(parentNode) break case 'DELAY': insertDelayNode(parentNode) break case 'INCLUSIVE': insertInclusiveNode(parentNode) break case 'TASK': insertTaskNode(parentNode) break case 'TRIGGER': insertTriggerNode(parentNode) break default: break } if (isBranchNode({ type: type })) { if (afterNode && afterNode.id) { afterNode.parentId = parentNode.children.children.id } parentNode.children.children.children = afterNode } else { if (afterNode && afterNode.id) { afterNode.parentId = parentNode.children.id } parentNode.children.children = afterNode } nextTick(() => { renderProcessTree() }) } /** * 节点插入的具体实现 */ const insertApprovalNode = (parentNode: any) => { parentNode.children.name = parentNode.children.type === 'APPROVAL' ? '审批人' : '子流程' parentNode.children.props = cloneDeep(DefaultProps.APPROVAL_PROPS) } const insertCcNode = (parentNode: any) => { parentNode.children.name = '抄送人' parentNode.children.props = cloneDeep(DefaultProps.CC_PROPS) } const insertConcurrentNode = (parentNode: any) => { parentNode.children.name = '并行分支' parentNode.children.children = { id: getRandomId(), parentId: parentNode.children.id, type: 'EMPTY', } parentNode.children.branchs = [ { id: getRandomId(), name: '分支1', parentId: parentNode.children.id, type: 'CONCURRENT', props: {}, children: {}, }, { id: getRandomId(), name: '分支2', parentId: parentNode.children.id, type: 'CONCURRENT', props: {}, children: {}, }, ] } const insertConditionNode = (parentNode: any) => { parentNode.children.name = '条件分支' parentNode.children.children = { id: getRandomId(), parentId: parentNode.children.id, type: 'EMPTY', } parentNode.children.branchs = [ { id: getRandomId(), parentId: parentNode.children.id, type: 'CONDITION', props: cloneDeep(DefaultProps.CONDITION_PROPS), name: '条件1', children: {}, }, { id: getRandomId(), parentId: parentNode.children.id, type: 'CONDITION', props: cloneDeep(DefaultProps.CONDITION_PROPS), name: '条件2', children: {}, }, ] } const insertDelayNode = (parentNode: any) => { parentNode.children.name = '延时处理' parentNode.children.props = cloneDeep(DefaultProps.DELAY_PROPS) } const insertInclusiveNode = (parentNode: any) => { parentNode.children.name = '包容分支' parentNode.children.children = { id: getRandomId(), parentId: parentNode.children.id, type: 'EMPTY', } parentNode.children.branchs = [ { id: getRandomId(), parentId: parentNode.children.id, type: 'INCLUSIVE', props: cloneDeep(DefaultProps.INCLUSIVE_PROPS), name: '包容条件1', children: {}, }, { id: getRandomId(), parentId: parentNode.children.id, type: 'INCLUSIVE', props: cloneDeep(DefaultProps.INCLUSIVE_PROPS), name: '包容条件2', children: {}, }, ] } const insertTaskNode = (parentNode: any) => { parentNode.children.name = '办理人' parentNode.children.props = cloneDeep(DefaultProps.TASK_PROPS) } const insertTriggerNode = (parentNode: any) => { parentNode.children.name = '触发器' parentNode.children.props = cloneDeep(DefaultProps.TRIGGER_PROPS) } /** * 获取分支末端节点 */ const getBranchEndNode = (conditionNode: any): any => { if (!conditionNode.children || !conditionNode.children.id) { return conditionNode } return getBranchEndNode(conditionNode.children) } /** * 添加分支节点 */ const addBranchNode = (node: any) => { if (node.branchs.length >= 8) { ElMessage.warning('最多只能添加 8 项😥') return } const newBranch = { id: getRandomId(), parentId: node.id, name: `${isConditionNode(node) ? '条件' : isInclusiveNode(node) ? '包容条件' : '分支'}${node.branchs.length + 1}`, props: isConditionNode(node) ? cloneDeep(DefaultProps.CONDITION_PROPS) : isInclusiveNode(node) ? cloneDeep(DefaultProps.INCLUSIVE_PROPS) : {}, type: isConditionNode(node) ? 'CONDITION' : isInclusiveNode(node) ? 'INCLUSIVE' : 'CONCURRENT', children: {}, } node.branchs.push(newBranch) nextTick(() => { renderProcessTree() }) } /** * 删除节点 */ const delNode = (node: any) => { console.log('删除节点', node) const parentNode = nodeMap.value.get(node.parentId) if (!parentNode) { ElMessage.warning('出现错误,找不到上级节点😥') return } if (isBranchNode(parentNode)) { parentNode.branchs.splice(parentNode.branchs.indexOf(node), 1) if (parentNode.branchs.length < 2) { const grandParentNode = nodeMap.value.get(parentNode.parentId) const remainingBranch = parentNode.branchs[0] if (remainingBranch?.children?.id) { grandParentNode.children = remainingBranch.children grandParentNode.children.parentId = grandParentNode.id const endNode = getBranchEndNode(remainingBranch) endNode.children = parentNode.children.children if (endNode.children && endNode.children.id) { endNode.children.parentId = endNode.id } } else { grandParentNode.children = parentNode.children.children if (grandParentNode.children && grandParentNode.children.id) { grandParentNode.children.parentId = grandParentNode.id } } } } else { if (node.children && node.children.id) { node.children.parentId = parentNode.id } parentNode.children = node.children } nextTick(() => { renderProcessTree() }) } /** * 流程校验入口 */ const validateProcess = () => { valid.value = true const err: any[] = [] validate(err, dom.value) return err } /** * 递归校验所有节点 */ const validate = (err: any[], node: any) => { if (isPrimaryNode(node)) { validateNode(err, node) validate(err, node.children) } else if (isBranchNode(node)) { node.branchs.forEach((branchNode: any) => { validateNode(err, branchNode) validate(err, branchNode.children) }) validate(err, node.children) } else if (isEmptyNode(node)) { validate(err, node.children) } } /** * 单个节点校验 */ const validateNode = (err: any[], node: any) => { const nodeComp = nodeRefs.value[node.id] if (nodeComp && typeof nodeComp.validate === 'function') { valid.value = nodeComp.validate(err) } } /** * 更新指定节点 DOM */ const nodeDomUpdate = (node: any) => { const nodeComp = nodeRefs.value[node.id] if (nodeComp && typeof nodeComp.$forceUpdate === 'function') { nodeComp.$forceUpdate() } } /** * 遍历节点树 */ const forEachNode = (parent: any, node: any, callback: (parent: any, node: any) => void) => { if (isBranchNode(node)) { callback(parent, node) forEachNode(node, node.children, callback) node.branchs.forEach((branchNode: any) => { callback(node, branchNode) forEachNode(branchNode, branchNode.children, callback) }) } else if (isPrimaryNode(node) || isEmptyNode(node) || isBranchSubNode(node)) { callback(parent, node) forEachNode(node, node.children, callback) } } /** * 渲染流程树入口 */ const renderProcessTree = () => { if (!_rootContainer.value) return console.log('渲染流程树') nodeMap.value.clear() // 确保有初始节点数据 if (!dom.value || Object.keys(dom.value).length === 0) { console.warn('没有流程数据可渲染') // 初始化一个默认的根节点 useWorkflowStoreHook().design.process = { id: getRandomId(), type: 'ROOT', name: '开始节点', children: {}, } return } const processTrees = buildDomTree(dom.value) // 添加流程结束标记 processTrees.push( h('div', { style: { 'text-align': 'center' } }, [ h('div', { class: 'process-end', innerHTML: '流程结束', }), ]), ) const rootDom = h('div', { class: '_root', ref: '_root' }, processTrees) // 清空容器并渲染新内容 _rootContainer.value.innerHTML = '' render(rootDom, _rootContainer.value) } // 监听流程数据变化 watch( dom, (newVal) => { if (newVal && Object.keys(newVal).length > 0) { nextTick(() => { renderProcessTree() }) } }, { deep: true }, ) // 组件挂载时初始化渲染 onMounted(() => { // 确保容器已挂载 nextTick(() => { if (dom.value && Object.keys(dom.value).length > 0) { renderProcessTree() } else { // 如果没有初始数据,创建一个默认的开始节点 useWorkflowStoreHook().design.process = { id: getRandomId(), type: 'ROOT', name: '开始节点', children: {}, } } }) }) // 暴露组件方法 defineExpose({ validateProcess, nodeDomUpdate, forEachNode, renderProcessTree, }) </script> <template> <!-- Vue3 中渲染函数生成的内容挂载到该容器 --> <div ref="_rootContainer" class="process-tree-container"></div> </template> <style lang="less" scoped> .process-tree-container { min-height: 400px; position: relative; } .loading-placeholder { display: flex; justify-content: center; align-items: center; height: 200px; color: #999; } /* 样式完全保留原逻辑,无需修改 */ ._root { margin: 0 auto; } .process-end { width: 60px; margin: 0 auto; margin-bottom: 20px; border-radius: 15px; padding: 5px 10px; font-size: small; color: #747474; background-color: #f2f2f2; box-shadow: 0 0 10px 0 #bcbcbc; } .primary-node { display: flex; align-items: center; flex-direction: column; } .branch-node { display: flex; justify-content: center; } .branch-node-item { position: relative; display: flex; background: #f5f6f6; flex-direction: column; align-items: center; border-top: 2px solid #cccccc; border-bottom: 2px solid #cccccc; &:before { content: ''; position: absolute; top: 0; left: calc(50% - 1px); margin: auto; width: 2px; height: 100%; background-color: #cacaca; } .line-top-left, .line-top-right, .line-bot-left, .line-bot-right { position: absolute; width: 50%; height: 4px; background-color: #f5f6f6; } .line-top-left { top: -2px; left: -1px; } .line-top-right { top: -2px; right: -1px; } .line-bot-left { bottom: -2px; left: -1px; } .line-bot-right { bottom: -2px; right: -1px; } } .add-branch-btn { position: absolute; width: 80px; .add-branch-btn-el { z-index: 999; position: absolute; top: -15px; } } .empty-node { display: flex; justify-content: center; flex-direction: column; align-items: center; } </style>
最新发布
09-21
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值