workflow-approval-dialog.vue 9.79 KB
<template>
  <el-dialog
    :visible.sync="visibleProxy"
    :show-close="false"
    width="88%"
    top="4vh"
    :close-on-click-modal="false"
    custom-class="workflow-approval-dialog"
    append-to-body
    @open="handleOpen"
  >
    <div class="dialog-inner">
      <div class="dialog-header">
        <div class="dialog-title-wrap">
          <div class="dialog-title">{{ flowTitle }}</div>
          <div class="dialog-subtitle">{{ flowSubtitle }}</div>
        </div>
        <span class="dialog-close" @click="visibleProxy = false"><i class="el-icon-close"></i></span>
      </div>

      <div v-loading="loading" class="dialog-body">
        <el-tabs v-model="activeTab">
          <el-tab-pane label="申请信息" name="form">
            <div v-if="formFields.length" class="form-grid">
              <div v-for="field in formFields" :key="field.key" class="form-field">
                <span class="form-field__label">{{ field.label }}</span>
                <span class="form-field__value">{{ field.value }}</span>
              </div>
            </div>
            <div v-else class="empty-tip">暂无表单详情</div>
          </el-tab-pane>
          <el-tab-pane label="流转记录" name="records">
            <div v-if="records.length" class="record-list">
              <div v-for="(rec, idx) in records" :key="idx" class="record-item">
                <div class="record-item__head">
                  <span class="record-item__node">{{ emptyText(rec.nodeName) }}</span>
                  <span class="record-item__time">{{ formatDateTime(rec.handleTime || rec.creatorTime) }}</span>
                </div>
                <div class="record-item__user">{{ emptyText(rec.userName) }} · {{ statusText(rec.handleStatus) }}</div>
                <div v-if="rec.handleOpinion" class="record-item__opinion">{{ rec.handleOpinion }}</div>
              </div>
            </div>
            <div v-else class="empty-tip">暂无流转记录</div>
          </el-tab-pane>
        </el-tabs>
      </div>

      <div class="dialog-footer">
        <el-button size="small" @click="visibleProxy = false">关闭</el-button>
        <el-button type="danger" size="small" :loading="rejecting" @click="openOpinionDialog('reject')">拒绝</el-button>
        <el-button type="primary" size="small" :loading="auditing" @click="openOpinionDialog('audit')">通过</el-button>
      </div>
    </div>

    <el-dialog
      :visible.sync="opinionVisible"
      :title="opinionType === 'audit' ? '审批通过' : '审批拒绝'"
      width="480px"
      append-to-body
      :close-on-click-modal="false"
    >
      <el-input
        v-model="opinion"
        type="textarea"
        :rows="4"
        placeholder="请输入审批意见(选填)"
      />
      <div slot="footer">
        <el-button size="small" @click="opinionVisible = false">取消</el-button>
        <el-button
          :type="opinionType === 'audit' ? 'primary' : 'danger'"
          size="small"
          :loading="auditing || rejecting"
          @click="submitApproval"
        >确定</el-button>
      </div>
    </el-dialog>
  </el-dialog>
</template>

<script>
import { getFlowBeforeInfo, auditFlow, rejectFlow } from '@/api/flowBefore'

const FIELD_LABELS = {
  khmc: '客户名称',
  sjh: '手机号',
  applicationNo: '申请编号',
  amount: '金额',
  reason: '事由',
  remark: '备注',
  bz: '备注',
  storeName: '门店',
  gsmdName: '归属门店',
  leaveType: '请假类型',
  startTime: '开始时间',
  endTime: '结束时间',
  flowTitle: '流程标题'
}

export default {
  name: 'WorkflowApprovalDialog',
  props: {
    visible: { type: Boolean, default: false },
    payload: {
      type: Object,
      default: () => ({})
    }
  },
  data() {
    return {
      loading: false,
      activeTab: 'form',
      flowTitle: '流程审批',
      flowSubtitle: '',
      formData: {},
      formFields: [],
      records: [],
      enCode: '',
      opinionVisible: false,
      opinionType: 'audit',
      opinion: '',
      auditing: false,
      rejecting: false
    }
  },
  computed: {
    visibleProxy: {
      get() { return this.visible },
      set(v) { this.$emit('update:visible', v) }
    },
    operatorId() {
      return this.payload.operatorId || ''
    },
    processId() {
      return this.payload.processId || ''
    },
    taskNodeId() {
      return this.payload.taskNodeId || ''
    }
  },
  methods: {
    emptyText(val) {
      if (val === undefined || val === null || String(val).trim() === '') return '无'
      return val
    },
    formatDateTime(val) {
      if (!val) return '无'
      const d = new Date(val)
      if (isNaN(d.getTime())) return String(val)
      const y = d.getFullYear()
      const m = String(d.getMonth() + 1).padStart(2, '0')
      const day = String(d.getDate()).padStart(2, '0')
      const h = String(d.getHours()).padStart(2, '0')
      const min = String(d.getMinutes()).padStart(2, '0')
      return `${y}-${m}-${day} ${h}:${min}`
    },
    statusText(status) {
      const map = { 0: '待处理', 1: '已通过', 2: '已拒绝', 3: '已撤回' }
      return map[Number(status)] || '处理中'
    },
    async handleOpen() {
      if (!this.processId || !this.operatorId) {
        this.$message.warning('审批参数不完整')
        this.visibleProxy = false
        return
      }
      this.loading = true
      this.activeTab = 'form'
      try {
        const res = await getFlowBeforeInfo(this.processId, { taskNodeId: this.taskNodeId })
        const data = res.data || {}
        const task = data.flowTaskInfo || {}
        this.flowTitle = task.fullName || this.payload.title || '流程审批'
        this.flowSubtitle = [
          task.flowName || this.payload.flowName || '无',
          task.userName ? `${task.userName} 发起` : ''
        ].filter(Boolean).join(' · ')
        this.enCode = this.payload.flowCode || task.enCode || ''
        this.records = data.flowTaskOperatorRecordList || []
        let parsed = {}
        try {
          parsed = data.flowFormInfo ? JSON.parse(data.flowFormInfo) : (data.flowFormInfo || {})
        } catch (e) {
          parsed = typeof data.flowFormInfo === 'object' ? data.flowFormInfo : {}
        }
        if (!parsed || typeof parsed !== 'object') parsed = {}
        this.formData = parsed
        this.formFields = this.buildFormFields(parsed)
      } catch (e) {
        this.$message.error('加载审批详情失败')
        this.visibleProxy = false
      } finally {
        this.loading = false
      }
    },
    buildFormFields(obj) {
      const skipKeys = ['id', 'flowId', 'status', 'formOperates', 'fileList']
      const fields = []
      Object.keys(obj || {}).forEach(key => {
        if (skipKeys.includes(key)) return
        const val = obj[key]
        if (val === null || val === undefined || val === '') return
        if (typeof val === 'object') return
        fields.push({
          key,
          label: FIELD_LABELS[key] || key,
          value: this.emptyText(val)
        })
      })
      return fields.slice(0, 24)
    },
    openOpinionDialog(type) {
      this.opinionType = type
      this.opinion = ''
      this.opinionVisible = true
    },
    async submitApproval() {
      const payload = {
        handleOpinion: this.opinion || '',
        formData: this.formData,
        enCode: this.enCode
      }
      const isAudit = this.opinionType === 'audit'
      if (isAudit) this.auditing = true
      else this.rejecting = true
      try {
        const api = isAudit ? auditFlow : rejectFlow
        await api(this.operatorId, payload)
        this.$message.success(isAudit ? '审批已通过' : '已拒绝')
        this.opinionVisible = false
        this.$emit('success')
        this.visibleProxy = false
      } catch (e) {
        // 错误由拦截器处理
      } finally {
        this.auditing = false
        this.rejecting = false
      }
    }
  }
}
</script>

<style lang="scss" scoped>
::v-deep .workflow-approval-dialog {
  max-width: 1200px;
  border-radius: 16px;
  padding: 0;
  .el-dialog__header { display: none; }
  .el-dialog__body { padding: 0; }
}
.dialog-inner { display: flex; flex-direction: column; max-height: 88vh; }
.dialog-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 20px 12px;
  border-bottom: 1px solid #f1f5f9;
  background: rgba(224, 242, 254, 0.5);
}
.dialog-title { font-size: 17px; font-weight: 600; color: #0f172a; }
.dialog-subtitle { font-size: 12px; color: #64748b; margin-top: 2px; }
.dialog-close {
  cursor: pointer;
  width: 28px;
  height: 28px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 999px;
  color: #64748b;
  &:hover { background: rgba(0, 0, 0, 0.06); }
}
.dialog-body { flex: 1; min-height: 0; overflow: auto; padding: 12px 20px; }
.dialog-footer {
  display: flex;
  justify-content: flex-start;
  gap: 8px;
  padding: 12px 20px 16px;
  border-top: 1px solid #f1f5f9;
}
.form-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 10px 16px;
}
.form-field {
  display: flex;
  gap: 8px;
  font-size: 13px;
  line-height: 1.5;
  white-space: nowrap;
  overflow: hidden;
}
.form-field__label { color: #64748b; flex-shrink: 0; }
.form-field__value { color: #0f172a; overflow: hidden; text-overflow: ellipsis; }
.record-list { display: flex; flex-direction: column; gap: 10px; }
.record-item {
  padding: 10px 12px;
  border-radius: 10px;
  background: #f8fafc;
  border: 1px solid #e2e8f0;
}
.record-item__head {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  font-size: 13px;
  font-weight: 600;
  color: #0f172a;
}
.record-item__time { color: #64748b; font-weight: 400; font-size: 12px; }
.record-item__user { margin-top: 4px; font-size: 12px; color: #475569; }
.record-item__opinion { margin-top: 6px; font-size: 12px; color: #334155; }
.empty-tip { padding: 24px 0; text-align: center; color: #94a3b8; font-size: 13px; }
</style>