| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323 |
- <template>
- <view class="flow-container">
- <uv-empty v-if="loading" mode="list" text="加载中..." icon-size="80"></uv-empty>
- <uv-empty
- v-else-if="!list.length"
- mode="data"
- text="暂无审批记录"
- icon-size="80"
- ></uv-empty>
- <view v-else class="flow-timeline">
- <view
- v-for="(node, index) in list"
- :key="node.nodeId || index"
- class="node-wrapper"
- >
- <!-- 左侧时间轴 -->
- <view class="timeline-side">
- <view
- class="dot-outer"
- :class="{ 'is-current': currentNode === node.nodeId }"
- >
- <view class="dot-inner">
- <text class="idx">{{ index + 1 }}</text>
- </view>
- </view>
- <view
- v-if="index !== list.length - 1"
- class="line-path"
- ></view>
- </view>
- <!-- 右侧内容卡片 -->
- <view
- class="node-card"
- :class="{ 'active-node': currentNode === node.nodeId }"
- >
- <view class="node-header">
- <view class="title-section">
- <text class="node-title">{{ node.nodeTitle || '流程节点' }}</text>
- <view class="method-tag" v-if="node.type !== 'start' && node.nodeType !== 'start'">
- <uv-icon name="info-circle" size="22" color="#909399"></uv-icon>
- <text>{{ getMethodName(node) }}</text>
- </view>
- </view>
- </view>
- <!-- 审批人列表 -->
- <view class="approvers-area">
- <view
- v-for="(item, idx) in node.items"
- :key="idx"
- class="approver-box"
- >
- <view class="box-top">
- <view class="user-info">
- <view class="avatar-stub">{{ (item.userName || '?').substring(0, 1) }}</view>
- <text class="user-name">{{ item.userName || '--' }}</text>
- </view>
- <text class="status-badge" :class="getStatusClass(item)">
- {{ getStatusLabel(item) }}
- </text>
- </view>
- <view class="box-content">
- <view class="info-item" v-if="getDate(item)">
- <uv-icon name="clock" size="24" color="#C0C4CC"></uv-icon>
- <text class="info-val">{{ formatDate(getDate(item),'YYYY-MM-DD HH:mm') }}</text>
- </view>
-
- <view class="opinion-bubble" v-if="getOpinion(item)">
- <text class="opinion-text">{{ getOpinion(item) }}</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- </view>
- </view>
- </view>
- </template>
- <script setup lang="ts">
- /**
- * 统一审批记录组件
- * 兼容两种数据结构(Execution 和 Flow API)
- */
- import { flowApprovalResultOptions, flowApprovalModelOptions } from '@/constants';
- import { formatDate } from '@/utils/date';
- const props = defineProps({
- list: { type: Array as () => any[], default: () => [] },
- currentNode: { type: String, default: '' },
- loading: { type: Boolean, default: false }
- });
- const getMethodName = (node: any) => {
- const model = node.actType || node.nodeModel;
- if (!model) return '--';
- const found = flowApprovalModelOptions.find((opt: any) => opt.dictValue === model);
- return found ? found.dictLabel : '--';
- };
- const getStatusLabel = (item: any) => {
- const val = item.approveResult || item.approvalResult;
- if (!val) return '处理中';
- const opt = flowApprovalResultOptions.find((i: any) => i.dictValue === val);
- return opt ? opt.dictLabel : val;
- };
- const getStatusClass = (item: any) => {
- const val = item.approveResult || item.approvalResult;
- if (!val) return 'is-primary';
- const opt = flowApprovalResultOptions.find(o => o.dictValue === val);
- return opt ? `is-${opt.type}` : 'is-primary';
- };
- const getDate = (item: any) => item.approveDate || item.approvalDate;
- const getOpinion = (item: any) => item.approveOpinion || item.approvalDesc;
- </script>
- <style lang="scss" scoped>
- .flow-container {
- padding: 20rpx 0;
- }
- .flow-timeline {
- display: flex;
- flex-direction: column;
- }
- .node-wrapper {
- display: flex;
- gap: 20rpx;
- }
- /* 时间轴样式 */
- .timeline-side {
- width: 60rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- flex-shrink: 0;
- .dot-outer {
- width: 44rpx;
- height: 44rpx;
- border-radius: 50%;
- background-color: #fff;
- border: 4rpx solid #E4E7ED;
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 2;
- transition: all 0.3s ease;
- &.is-current {
- border-color: #1c9bfd;
- box-shadow: 0 0 12rpx rgba(28, 155, 253, 0.4);
- .dot-inner { background-color: #1c9bfd; .idx { color: #fff; } }
- }
- }
- .dot-inner {
- width: 30rpx;
- height: 30rpx;
- border-radius: 50%;
- background-color: #F2F6FC;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: all 0.3s ease;
- .idx {
- font-size: 18rpx;
- color: #909399;
- font-weight: bold;
- }
- }
- .line-path {
- width: 3rpx;
- flex: 1;
- background-color: #EBEEF5;
- margin: 10rpx 0;
- }
- }
- /* 节点卡片样式 */
- .node-card {
- flex: 1;
- background-color: #F8F9FA;
- border: 1rpx solid #EBEEF5;
- border-radius: 20rpx;
- padding: 28rpx;
- margin-bottom: 30rpx;
- box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
- position: relative;
- transition: transform 0.2s ease;
- &.active-node {
- border: 1rpx solid rgba(28, 155, 253, 0.3);
- background: linear-gradient(180deg, #f0f7ff 0%, #ffffff 100%);
- box-shadow: 0 8rpx 24rpx rgba(28, 155, 253, 0.12);
- }
- .node-header {
- margin-bottom: 24rpx;
-
- .title-section {
- display: flex;
- align-items: center;
- justify-content: space-between;
-
- .node-title {
- font-size: 30rpx;
- font-weight: 600;
- color: #303133;
- }
-
- .method-tag {
- display: flex;
- align-items: center;
- gap: 6rpx;
- font-size: 22rpx;
- color: #909399;
- background-color: #F2F6FC;
- padding: 4rpx 14rpx;
- border-radius: 100rpx;
- }
- }
- }
- }
- /* 审批人样式 */
- .approvers-area {
- display: flex;
- flex-direction: column;
- gap: 20rpx;
- }
- .approver-box {
- background-color: #fff;
- border: 1rpx solid #ebeef5;
- border-radius: 16rpx;
- padding: 20rpx;
- box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.02);
-
- .box-top {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 16rpx;
-
- .user-info {
- display: flex;
- align-items: center;
- gap: 16rpx;
-
- .avatar-stub {
- width: 48rpx;
- height: 48rpx;
- background: linear-gradient(135deg, #1c9bfd 0%, #15a982 100%);
- color: #fff;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 24rpx;
- font-weight: bold;
- }
-
- .user-name {
- font-size: 28rpx;
- font-weight: 500;
- color: #303133;
- }
- }
- }
- }
- /* 状态徽章 */
- .status-badge {
- font-size: 22rpx;
- padding: 4rpx 16rpx;
- border-radius: 100rpx;
- font-weight: 500;
- &.is-success { background-color: #f0f9eb; color: #67c23a; }
- &.is-error { background-color: #fef0f0; color: #f56c6c; }
- &.is-warning { background-color: #fdf6ec; color: #e6a23c; }
- &.is-primary { background-color: #ecf5ff; color: #409eff; }
- }
- .box-content {
- .info-item {
- display: flex;
- align-items: center;
- gap: 10rpx;
- margin-bottom: 12rpx;
-
- .info-val {
- font-size: 24rpx;
- color: #909399;
- }
- }
-
- .opinion-bubble {
- background-color: #F8F9FA;
- padding: 16rpx 20rpx;
- border-radius: 12rpx;
- position: relative;
- border: 1rpx solid #EBEEF5;
-
- .opinion-text {
- font-size: 26rpx;
- color: #606266;
- line-height: 1.6;
- }
- }
- }
- </style>
|