2905 lines
92 KiB
Vue
2905 lines
92 KiB
Vue
<template>
|
||
<transition name="report-detail-fade">
|
||
<div v-if="visible" class="report-detail">
|
||
<div class="detail-panel">
|
||
<div class="detail-header">
|
||
<div class="header-left">
|
||
<el-button
|
||
type="text"
|
||
icon="el-icon-arrow-left"
|
||
class="back-button"
|
||
@click="handleBack"
|
||
>
|
||
</el-button>
|
||
<div class="header-info">
|
||
<div class="title">
|
||
报告详情
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<!-- 草稿状态:显示发起和修改按钮 -->
|
||
<template v-if="reportDetail.status === 'DRAFT' && !isEditMode">
|
||
<el-button
|
||
v-if="canResend"
|
||
@click="handleResend"
|
||
>
|
||
直送
|
||
</el-button>
|
||
<el-button @click="handleLaunch">
|
||
发起
|
||
</el-button>
|
||
<el-button v-if="hasOperationPermission()" type="primary" @click="handleEdit">
|
||
修改
|
||
</el-button>
|
||
</template>
|
||
<!-- 已拒绝状态:显示重新发起和修改按钮 -->
|
||
<template v-if="reportDetail.status === 'REJECTED' && !isEditMode">
|
||
<el-button
|
||
:loading="resending"
|
||
@click="handleResend"
|
||
>
|
||
直送
|
||
</el-button>
|
||
<el-button @click="handleResendApproval">
|
||
发起
|
||
</el-button>
|
||
<el-button v-if="hasOperationPermission()" type="primary" @click="handleEdit">
|
||
修改
|
||
</el-button>
|
||
</template>
|
||
<!-- 编辑模式:显示保存和返回按钮 -->
|
||
<template v-if="isEditMode">
|
||
<el-button @click="handleCancelEdit">
|
||
返回
|
||
</el-button>
|
||
<el-button type="primary" :loading="saving" @click="handleSave">
|
||
保存
|
||
</el-button>
|
||
</template>
|
||
<!-- 审批中状态:仅从待办页面进入时显示退回、直送和提交/重新发起按钮 -->
|
||
<template v-if="reportDetail.status === 'APPROVING' && !isEditMode && fromTodo">
|
||
<el-button
|
||
@click="handleReturn"
|
||
>
|
||
退回
|
||
</el-button>
|
||
<el-button
|
||
v-if="canResend"
|
||
:loading="resending"
|
||
@click="handleResend"
|
||
>
|
||
直送
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
@click="handleSubmit"
|
||
>
|
||
提交
|
||
</el-button>
|
||
</template>
|
||
</div>
|
||
</div>
|
||
<div class="detail-content">
|
||
<!-- 报告详情信息 -->
|
||
<section class="block-section">
|
||
<div class="section-title">
|
||
基础信息
|
||
</div>
|
||
<div class="info-grid">
|
||
<!-- 流程主题单独一行 -->
|
||
<div class="info-item-full">
|
||
<span class="label">流程主题</span>
|
||
<div v-if="!isEditMode && !isEditingProcessTitle" class="value process-title-value-wrapper">
|
||
{{ showTopDetail.processTitle || detail.processTitle || reportDetail.processTitle || '--' }}
|
||
<i
|
||
class="el-icon-edit process-title-edit-icon"
|
||
@click="handleEditProcessTitle"
|
||
></i>
|
||
</div>
|
||
<div v-else-if="!isEditMode && isEditingProcessTitle" class="process-title-input-wrapper">
|
||
<el-input
|
||
v-model="editingProcessTitle"
|
||
placeholder="请输入流程主题"
|
||
maxlength="100"
|
||
show-word-limit
|
||
class="process-title-input"
|
||
>
|
||
<template slot="suffix">
|
||
<i class="el-icon-check process-title-icon" @click="handleSaveProcessTitle"></i>
|
||
<i class="el-icon-close process-title-icon" @click="handleCancelProcessTitle"></i>
|
||
</template>
|
||
</el-input>
|
||
</div>
|
||
<el-input
|
||
v-else
|
||
v-model="editFormData.processTitle"
|
||
maxlength="100"
|
||
show-word-limit
|
||
placeholder="请输入流程主题"
|
||
/>
|
||
</div>
|
||
<!-- 其他信息一行两个 -->
|
||
<div class="info-row">
|
||
<div class="info-item">
|
||
<span class="label">创建人</span>
|
||
<span class="value">{{ showTopDetail.createBy || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">创建时间</span>
|
||
<span class="value">{{ showTopDetail.createTime || '--' }}</span>
|
||
</div>
|
||
</div>
|
||
<div class="info-row">
|
||
<div class="info-item">
|
||
<span class="label">当前环节</span>
|
||
<span class="value">{{ showTopDetail.currentNode || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">当前处理人</span>
|
||
<span class="value">{{ showTopDetail.currentProcessor || '--' }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 报告详情信息 -->
|
||
<section class="block-section">
|
||
<div class="section-title">
|
||
报告信息
|
||
</div>
|
||
<!-- 只读模式 -->
|
||
<template v-if="!isEditMode">
|
||
<div class="info-grid">
|
||
<div class="info-item">
|
||
<span class="label">标题名称</span>
|
||
<span class="value">{{ reportDetail.title || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">厂家名称(全称)</span>
|
||
<span class="value">{{ reportDetail.manufacturerName || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">产品型号(全称)</span>
|
||
<span class="value">{{ reportDetail.modelName || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">厂家简称</span>
|
||
<span class="value">{{ reportDetail.manufacturerAbbreviation || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">产品型号简称</span>
|
||
<span class="value">{{ reportDetail.modelAbbreviation || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">产品编号</span>
|
||
<span class="value">{{ reportDetail.productNumber || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">领域/模块</span>
|
||
<span class="value">{{ reportDetail.module || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">板块</span>
|
||
<span class="value">{{ reportDetail.plate || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">平台</span>
|
||
<span class="value">{{ reportDetail.platform || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">系列</span>
|
||
<span class="value">{{ reportDetail.series || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">细分市场</span>
|
||
<span class="value">{{ reportDetail.marketSegment || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">项目名称</span>
|
||
<span class="value">{{ reportDetail.projectName || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">项目编号</span>
|
||
<span class="value">{{ reportDetail.projectNumber || '--' }}</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="label">文件</span>
|
||
<span class="value">
|
||
<a v-if="reportDetail.filePath" href="javascript:void(0)" class="el-link el-link--primary" @click.stop="handleDownload(reportDetail)">{{ reportDetail.fileName }}</a>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div v-if="reportDetail.explanation" class="description-block">
|
||
<span class="label">说明:</span>
|
||
<span class="value">{{ reportDetail.explanation }}</span>
|
||
</div>
|
||
</template>
|
||
<!-- 编辑模式 -->
|
||
<template v-else>
|
||
<el-form ref="editForm" :model="editFormData" label-width="150px">
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="标题名称" prop="title">
|
||
<el-input v-model="editFormData.title" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="厂家名称(全称)" prop="manufacturerName">
|
||
<el-input v-model="editFormData.manufacturerName" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="产品型号(全称)" prop="modelName">
|
||
<el-input v-model="editFormData.modelName" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="厂家简称" prop="manufacturerAbbreviation">
|
||
<el-input v-model="editFormData.manufacturerAbbreviation" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="产品型号简称" prop="modelAbbreviation">
|
||
<el-input v-model="editFormData.modelAbbreviation" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="产品编号" prop="productNumber">
|
||
<el-input v-model="editFormData.productNumber" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="领域/模块" prop="module">
|
||
<el-input v-model="editFormData.module" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="板块" prop="plate">
|
||
<el-input v-model="editFormData.plate" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="平台" prop="platform">
|
||
<el-input v-model="editFormData.platform" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="系列" prop="series">
|
||
<el-input v-model="editFormData.series" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="细分市场" prop="marketSegment">
|
||
<el-input v-model="editFormData.marketSegment" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
<el-col :span="12">
|
||
<el-form-item label="项目名称" prop="projectName">
|
||
<el-input v-model="editFormData.projectName" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-row :gutter="20">
|
||
<el-col :span="12">
|
||
<el-form-item label="项目编号" prop="projectNumber">
|
||
<el-input v-model="editFormData.projectNumber" placeholder="请输入" />
|
||
</el-form-item>
|
||
</el-col>
|
||
</el-row>
|
||
<el-form-item label="附件上传" prop="attachments">
|
||
<el-upload
|
||
ref="upload"
|
||
:auto-upload="false"
|
||
:on-change="handleFileChange"
|
||
:on-remove="handleFileRemove"
|
||
:file-list="fileList"
|
||
:limit="1"
|
||
accept=".doc,.pdf,.xls,.xlsx,.ppt,.pptx"
|
||
>
|
||
<el-button type="primary" icon="el-icon-upload">
|
||
选择文件
|
||
</el-button>
|
||
<div slot="tip" class="el-upload__tip">
|
||
支持扩展名: .doc .pdf .xls .xlsx .ppt .pptx,仅支持上传一个文件
|
||
</div>
|
||
</el-upload>
|
||
</el-form-item>
|
||
<el-form-item label="说明" prop="explanation">
|
||
<el-input
|
||
v-model="editFormData.explanation"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请输入"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
</template>
|
||
</section>
|
||
|
||
<!-- 审批环节(草稿状态下显示) -->
|
||
<section v-if="reportDetail.status === 'DRAFT' || reportDetail.status === 'REJECTED'" class="block-section">
|
||
<div class="section-title">
|
||
审批环节
|
||
</div>
|
||
<div class="approval-workflow">
|
||
<div class="workflow-table">
|
||
<table class="workflow-table-content">
|
||
<thead>
|
||
<tr>
|
||
<th class="step-name-col">
|
||
环节名称
|
||
</th>
|
||
<th class="handler-col">
|
||
处理人
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="(step, index) in approvalWorkflowSteps" :key="index" class="workflow-row">
|
||
<td class="step-name-cell">
|
||
<span class="step-name">
|
||
{{ step.name }}
|
||
<span v-if="step.required" class="required-mark">*</span>
|
||
</span>
|
||
</td>
|
||
<td class="handler-cell">
|
||
<!-- 只读模式:显示已选择的人员 -->
|
||
<template v-if="!isEditMode">
|
||
<div v-if="step.handlers && step.handlers.length > 0" class="handler-display">
|
||
<span
|
||
v-for="(handlerId, idx) in step.handlers"
|
||
:key="idx"
|
||
class="handler-tag"
|
||
>
|
||
{{ getHandlerName(handlerId, step.options) }}
|
||
</span>
|
||
</div>
|
||
<span v-else class="handler-empty">--</span>
|
||
</template>
|
||
<!-- 编辑模式:可编辑的下拉框 -->
|
||
<el-select
|
||
v-else
|
||
v-model="step.handlers"
|
||
multiple
|
||
filterable
|
||
remote
|
||
placeholder="请选择"
|
||
:remote-method="(query) => handleRemoteSearch(query, index)"
|
||
:loading="step.loading"
|
||
class="handler-select"
|
||
@change="(value) => handleHandlerChange(index, value)"
|
||
>
|
||
<el-option
|
||
v-for="person in step.options"
|
||
:key="person.userNumber"
|
||
:value="person.userId"
|
||
:label="person.username"
|
||
>
|
||
<span class="username-text">{{ person.username }}</span>
|
||
<span class="user-id-text">{{ person.userNumber }}</span>
|
||
</el-option>
|
||
</el-select>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 审批流程 -->
|
||
<section v-if="reportDetail.status && reportDetail.status !== 'DRAFT' && detail.flowId && !isEditMode" class="block-section">
|
||
<div class="section-title">
|
||
审批流程
|
||
</div>
|
||
<div v-if="timelineNodes && timelineNodes.length > 0" class="approval-timeline">
|
||
<el-timeline>
|
||
<el-timeline-item
|
||
v-for="(node, index) in timelineNodes"
|
||
:key="index"
|
||
:color="getTimelineColor(node.result || node.status, index, timelineNodes.length)"
|
||
>
|
||
<div class="timeline-item-content">
|
||
<div class="node-title">
|
||
{{ node.title }}
|
||
</div>
|
||
<div class="node-content">
|
||
<div class="node-info">
|
||
<div class="node-user">
|
||
{{ node.userName || '--' }}
|
||
<span class="node-time">
|
||
{{ formatTime(node.operateTime) }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="node-status" :class="getStatusClass(node.result || node.status)">
|
||
{{ setStatusText(node.result || node.status) }}
|
||
</div>
|
||
<div v-if="node.opinion" class="node-opinion">
|
||
审批意见:{{ node.opinion }}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</el-timeline-item>
|
||
</el-timeline>
|
||
</div>
|
||
<div v-else class="empty-progress">
|
||
<span class="empty-text">暂无审批流程数据</span>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 审批弹窗 -->
|
||
<el-dialog
|
||
title="提交"
|
||
:visible.sync="approvalDialogVisible"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
append-to-body
|
||
@close="handleApprovalDialogClose"
|
||
>
|
||
<el-form
|
||
ref="approvalForm"
|
||
:model="approvalFormData"
|
||
:rules="approvalFormRules"
|
||
label-width="100px"
|
||
>
|
||
<el-form-item label="提交意见:">
|
||
<el-input
|
||
v-model="approvalFormData.opinion"
|
||
type="textarea"
|
||
:rows="4"
|
||
placeholder="请输入提交意见(可选)"
|
||
/>
|
||
</el-form-item>
|
||
</el-form>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button @click="handleApprovalDialogClose">
|
||
取消
|
||
</el-button>
|
||
<el-button type="primary" :loading="approvalSubmitting" @click="handleApprovalSubmit">
|
||
确定
|
||
</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
|
||
<!-- 退回节点选择弹窗 -->
|
||
<el-dialog
|
||
title="选择退回节点"
|
||
:visible.sync="returnNodeDialogVisible"
|
||
width="500px"
|
||
:close-on-click-modal="false"
|
||
append-to-body
|
||
@close="handleReturnNodeDialogClose"
|
||
>
|
||
<div class="return-node-dialog-content">
|
||
<el-radio-group
|
||
v-model="selectedReturnNode"
|
||
@change="handleReturnNodeChange"
|
||
>
|
||
<el-radio
|
||
v-for="node in returnableNodes"
|
||
:key="node"
|
||
:label="node"
|
||
>
|
||
</el-radio>
|
||
</el-radio-group>
|
||
<!-- 当选择"编制"节点时,显示意见输入框 -->
|
||
<div v-if="selectedReturnNode === '编制'" class="return-opinion-section">
|
||
<div class="opinion-label">
|
||
退回意见:
|
||
</div>
|
||
<el-input
|
||
v-model="returnOpinion"
|
||
type="textarea"
|
||
:rows="3"
|
||
placeholder="请输入退回意见"
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div slot="footer" class="dialog-footer">
|
||
<el-button @click="handleReturnNodeDialogClose">
|
||
取消
|
||
</el-button>
|
||
<el-button
|
||
type="primary"
|
||
:loading="returnSubmitting"
|
||
:disabled="!selectedReturnNode"
|
||
@click="handleReturnSubmit"
|
||
>
|
||
确定
|
||
</el-button>
|
||
</div>
|
||
</el-dialog>
|
||
</div>
|
||
</transition>
|
||
</template>
|
||
|
||
<script>
|
||
import api from '@/api'
|
||
import { getUserId } from '@/utils'
|
||
export default {
|
||
name: 'ReportDetail',
|
||
props: {
|
||
detail: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
visible: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
fromTodo: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
canResend: {
|
||
type: Boolean,
|
||
default: false
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
reportDetail: {},
|
||
showTopDetail: {
|
||
processTitle: '',
|
||
createBy: '',
|
||
createTime: '',
|
||
currentNode: '',
|
||
currentProcessor: ''
|
||
},
|
||
progress: [], // 保留用于兼容
|
||
flowNodes: [], // 审批流程节点数据
|
||
progressDetail: {}, // 审批流程详情数据
|
||
approvalDialogVisible: false,
|
||
approvalFormData: {
|
||
result: '',
|
||
opinion: ''
|
||
},
|
||
approvalSubmitting: false,
|
||
resending: false, // 直送提交loading状态
|
||
approvalFormRules: {
|
||
// 意见改为非必填
|
||
},
|
||
// 退回节点选择相关
|
||
returnNodeDialogVisible: false,
|
||
returnableNodes: [],
|
||
selectedReturnNode: '',
|
||
returnSubmitting: false,
|
||
returnOpinion: '拒绝', // 退回意见(仅在选择编制节点时使用)
|
||
isEditMode: false, // 编辑模式
|
||
editFormData: {
|
||
title: '',
|
||
manufacturerName: '',
|
||
modelName: '',
|
||
manufacturerAbbreviation: '',
|
||
modelAbbreviation: '',
|
||
productNumber: '',
|
||
module: '',
|
||
plate: '',
|
||
platform: '',
|
||
series: '',
|
||
marketSegment: '',
|
||
projectName: '',
|
||
projectNumber: '',
|
||
explanation: '',
|
||
fileUrl: '',
|
||
fileName: '',
|
||
processTitle: ''
|
||
},
|
||
originalReportDetail: {}, // 保存原始数据,用于取消编辑时恢复
|
||
selectedFile: null, // 选中的文件
|
||
fileList: [], // 文件列表
|
||
uploading: false, // 上传中状态
|
||
saving: false, // 保存中状态
|
||
isEditingProcessTitle: false, // 是否正在编辑流程主题
|
||
editingProcessTitle: '', // 正在编辑的流程主题值
|
||
approvalFlowIdFromUsers: '', // 从审批人员接口获取的流程ID
|
||
approvalCanSend: false, // 审批人员接口返回的是否可继续发起标识
|
||
// 保存时对比快照(不依赖watch/change)
|
||
originalEditSnapshot: null, // 进入编辑态并完成回填后的快照
|
||
approvalWorkflowSteps: [
|
||
{
|
||
name: '校对',
|
||
required: true,
|
||
handlers: [],
|
||
options: [],
|
||
loading: false
|
||
},
|
||
{
|
||
name: '审核',
|
||
required: true,
|
||
handlers: [],
|
||
options: [],
|
||
loading: false
|
||
},
|
||
{
|
||
name: '批准',
|
||
required: true,
|
||
handlers: [],
|
||
options: [],
|
||
loading: false
|
||
}
|
||
],
|
||
launching: false // 发起中状态
|
||
}
|
||
},
|
||
computed: {
|
||
// 发起审批节点
|
||
initiateNode() {
|
||
if (!this.reportDetail || !this.reportDetail.createBy) {
|
||
return null
|
||
}
|
||
return {
|
||
userName: this.reportDetail.createBy,
|
||
createTime: this.reportDetail.createTime,
|
||
status: 'INITIATE',
|
||
result: 'INITIATE'
|
||
}
|
||
},
|
||
// 合并所有节点为时间线数据
|
||
timelineNodes() {
|
||
const nodes = []
|
||
|
||
// 添加发起审批节点
|
||
if (this.initiateNode) {
|
||
}
|
||
|
||
// 使用 flowNodes 数据构建审批流程节点(按照返回的顺序直接渲染,不排序)
|
||
if (this.flowNodes && this.flowNodes.length > 0) {
|
||
// 查找是否有 REJECT 节点
|
||
const rejectIndex = this.flowNodes.findIndex((node) => node.result === 'REJECT')
|
||
|
||
let processedNodes = this.flowNodes
|
||
if (rejectIndex !== -1) {
|
||
// 如果有 REJECT 节点,删除该节点后面的所有节点
|
||
// processedNodes = this.flowNodes.slice(0, rejectIndex + 1)
|
||
} else {
|
||
// 如果没有 REJECT,将第一个 result 是"待审批"的节点改为 APPROVING
|
||
const stayIndex = processedNodes.findIndex((node) => node.result === '待审批')
|
||
if (stayIndex !== -1) {
|
||
processedNodes = processedNodes.map((node, index) => {
|
||
if (index === stayIndex) {
|
||
return {
|
||
...node,
|
||
result: 'APPROVING'
|
||
}
|
||
}
|
||
return node
|
||
})
|
||
}
|
||
}
|
||
|
||
// 转换为时间线节点格式
|
||
processedNodes.forEach((node) => {
|
||
let result = 'APPROVING'
|
||
let status = 'APPROVING'
|
||
|
||
if (node.result) {
|
||
if (node.result === 'REJECT') {
|
||
result = 'REJECT'
|
||
status = 'REJECT'
|
||
} else if (node.result === 'MSG') {
|
||
result = 'MSG'
|
||
status = 'MSG'
|
||
} else if (node.result === 'RETURN') {
|
||
result = 'PASS'
|
||
status = 'PASS'
|
||
} else if (node.result === 'APPROVING') {
|
||
result = 'APPROVING'
|
||
status = 'APPROVING'
|
||
} else if (node.result === 'PASS') {
|
||
result = 'PASS'
|
||
status = 'PASS'
|
||
} else if (node.result === '待审批') {
|
||
result = '待审批'
|
||
status = 'STAY'
|
||
}
|
||
}
|
||
|
||
// 处理审批意见:如果有 opinions 数组,取第一个;否则为空
|
||
const opinion = node.opinions && Array.isArray(node.opinions) && node.opinions.length > 0
|
||
? node.opinions[0]
|
||
: null
|
||
|
||
// 处理用户名:优先使用 todoUserName(已处理人),如果没有则使用 undoUserName(待处理人)
|
||
const userName = node.todoUserName || node.undoUserName || '--'
|
||
|
||
nodes.push({
|
||
title: node.node || '审批节点',
|
||
userName: userName,
|
||
operateTime: node.approvalTime || '',
|
||
result: result,
|
||
status: status,
|
||
opinion: opinion
|
||
})
|
||
})
|
||
}
|
||
|
||
return nodes
|
||
},
|
||
// 当前环节
|
||
currentNode() {
|
||
// 如果是草稿状态,返回"草稿"
|
||
if (this.reportDetail.status === 'DRAFT') {
|
||
return '草稿'
|
||
}
|
||
if (this.flowNodes && this.flowNodes.length > 0) {
|
||
// 找到第一个"待审批"或"审批中"的节点
|
||
const approvingNode = this.flowNodes.find((node) =>
|
||
node.result === '待审批' || node.result === 'APPROVING'
|
||
)
|
||
if (approvingNode) {
|
||
return approvingNode.node || '--'
|
||
}
|
||
// 如果都已完成,返回最后一个节点
|
||
const lastNode = this.flowNodes[this.flowNodes.length - 1]
|
||
return lastNode ? (lastNode.node || '--') : '--'
|
||
}
|
||
return '--'
|
||
},
|
||
// 当前处理人
|
||
currentProcessor() {
|
||
if (this.flowNodes && this.flowNodes.length > 0) {
|
||
// 找到第一个"待审批"或"审批中"的节点
|
||
const approvingNode = this.flowNodes.find((node) =>
|
||
node.result === '待审批' || node.result === 'APPROVING'
|
||
)
|
||
if (approvingNode) {
|
||
// 优先使用待处理人,如果没有则使用已处理人
|
||
return approvingNode.undoUserName || approvingNode.todoUserName || '--'
|
||
}
|
||
}
|
||
return '--'
|
||
}
|
||
},
|
||
watch: {
|
||
detail: {
|
||
handler(newVal) {
|
||
if (newVal && newVal.reportId) {
|
||
// 如果有 reportId,调用接口获取详情
|
||
this.loadReportDetailFromApi(newVal.reportId, newVal.flowId)
|
||
if (newVal.flowId) {
|
||
this.getApprovalSpet(newVal.flowId)
|
||
}
|
||
}
|
||
},
|
||
immediate: true,
|
||
deep: true
|
||
}
|
||
},
|
||
mounted() {
|
||
// this.getReportDetailProcess()
|
||
// 加载用户权限
|
||
this.loadUserSelfRole()
|
||
},
|
||
methods: {
|
||
// 加载用户自身角色权限
|
||
loadUserSelfRole() {
|
||
const request = {
|
||
...api.GET_USER_SELF_ROLE,
|
||
params: {
|
||
userOrDepID: getUserId()
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
if (resp && resp.data) {
|
||
// 将权限数据存储到 localStorage
|
||
try {
|
||
localStorage.setItem('user_self_role', JSON.stringify(resp.data))
|
||
} catch (err) {
|
||
console.error('存储权限数据到 localStorage 失败:', err)
|
||
}
|
||
}
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('获取用户自身角色失败:', err)
|
||
})
|
||
},
|
||
// 检查是否是管理员(isAdmin === 1)
|
||
isAdmin() {
|
||
try {
|
||
const permissionDataStr = localStorage.getItem('user_self_role')
|
||
if (!permissionDataStr) {
|
||
return false
|
||
}
|
||
const permissionData = JSON.parse(permissionDataStr)
|
||
if (!Array.isArray(permissionData)) {
|
||
return false
|
||
}
|
||
// 检查是否有 isAdmin === 1 的记录
|
||
return permissionData.some((perm) => perm.isAdmin === 1 || perm.isAdmin === '1')
|
||
} catch (err) {
|
||
console.error('检查管理员权限失败:', err)
|
||
return false
|
||
}
|
||
},
|
||
// 检查是否有全局编辑权限(permissionPath 是"对标报告"且 parameterPer 是"编辑")
|
||
hasGlobalEditPermission() {
|
||
try {
|
||
const permissionDataStr = localStorage.getItem('user_self_role')
|
||
if (!permissionDataStr) {
|
||
return false
|
||
}
|
||
const permissionData = JSON.parse(permissionDataStr)
|
||
if (!Array.isArray(permissionData)) {
|
||
return false
|
||
}
|
||
// 检查是否有 permissionPath 为 "对标报告" 且 parameterPer 为 "编辑" 的权限
|
||
return permissionData.some((perm) => {
|
||
const permissionPath = perm.permissionPath || ''
|
||
const parameterPer = perm.parameterPer || ''
|
||
// 检查 permissionPath 是否等于 "对标报告"(不包含其他路径)
|
||
return permissionPath === '对标报告' && parameterPer === '编辑'
|
||
})
|
||
} catch (err) {
|
||
console.error('检查全局编辑权限失败:', err)
|
||
return false
|
||
}
|
||
},
|
||
// 检查是否有操作权限(管理员或全局编辑权限)
|
||
hasOperationPermission() {
|
||
// 如果是管理员,拥有全部权限
|
||
if (this.isAdmin()) {
|
||
return true
|
||
}
|
||
// 如果有全局编辑权限,拥有全部权限
|
||
if (this.hasGlobalEditPermission()) {
|
||
return true
|
||
}
|
||
return false
|
||
},
|
||
setStatusText(result) {
|
||
const statusMap = {
|
||
'PASS': '通过',
|
||
'REJECT': '驳回',
|
||
'REFUSE': '驳回',
|
||
'APPROVE': '批准',
|
||
'APPROVING': '审批中',
|
||
'PENDING': '待处理',
|
||
'WAITING': '待处理',
|
||
'INITIATE': '发起审批',
|
||
'MSG': '通过'
|
||
}
|
||
return statusMap[result] || result || '待处理'
|
||
},
|
||
formatTime(time) {
|
||
if (!time) return '--'
|
||
// 如果是时间戳,转换为日期格式
|
||
if (typeof time === 'number') {
|
||
const date = new Date(time)
|
||
return date.toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
})
|
||
}
|
||
// 如果已经是字符串格式,直接返回
|
||
return time
|
||
},
|
||
getTimelineColor(result, stepIndex, totalSteps) {
|
||
// 根据审批结果设置颜色
|
||
if (result === 'PASS' || result === 'APPROVE') {
|
||
return '#67C23A' // 绿色 - 通过
|
||
} else if (result === 'REJECT' || result === 'REFUSE') {
|
||
return '#F56C6C' // 红色 - 驳回
|
||
} else if (result === 'APPROVING' || result === 'PENDING' || result === 'WAITING') {
|
||
return '#E6A23C' // 橙色 - 审批中
|
||
} else if (result === 'INITIATE' || result === 'MSG') {
|
||
return '#409EFF' // 蓝色 - 发起审批/MSG
|
||
}
|
||
// 默认颜色
|
||
return '#409EFF' // 默认蓝色
|
||
},
|
||
getStatusClass(result) {
|
||
const classMap = {
|
||
'PASS': 'status-pass',
|
||
'APPROVE': 'status-approve',
|
||
'REJECT': 'status-reject',
|
||
'REFUSE': 'status-reject',
|
||
'APPROVING': 'status-approving',
|
||
'PENDING': 'status-pending',
|
||
'WAITING': 'status-pending',
|
||
'INITIATE': 'status-initiate',
|
||
'MSG': 'status-msg'
|
||
}
|
||
return classMap[result] || 'status-default'
|
||
},
|
||
loadReportDetail() {
|
||
// 如果已经有详情数据,直接使用
|
||
if (this.detail && Object.keys(this.detail).length > 0) {
|
||
if (this.detail.reportId) {
|
||
// 如果有 reportId,调用接口获取详情
|
||
this.loadReportDetailFromApi(this.detail.reportId)
|
||
} else {
|
||
// 如果已经有完整的详情数据,直接使用
|
||
this.reportDetail = this.detail
|
||
// 如果flowId存在且状态不是草稿,加载审批流程
|
||
if (this.detail.flowId && this.reportDetail.status && this.reportDetail.status !== 'DRAFT') {
|
||
this.getApprovalSpet(this.detail.flowId)
|
||
}
|
||
}
|
||
}
|
||
},
|
||
loadReportDetailFromApi(reportId, flowId) {
|
||
console.log(reportId, flowId, 'reportId, flowId')
|
||
|
||
if (!reportId) {
|
||
return
|
||
}
|
||
const request = {
|
||
...api.GET_REPORT_DETAIL,
|
||
params: {
|
||
reportId: reportId
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
this.reportDetail = resp.data || resp
|
||
// 保存原始数据(用于编辑模式取消时恢复)
|
||
this.originalReportDetail = JSON.parse(JSON.stringify(this.reportDetail))
|
||
// 如果是草稿状态,获取审批人员(默认只读显示)
|
||
if (this.reportDetail.status === 'DRAFT') {
|
||
const currentFlowId = flowId || this.detail.flowId || this.reportDetail.flowId
|
||
if (currentFlowId) {
|
||
// 重置审批环节
|
||
this.approvalWorkflowSteps = [
|
||
{
|
||
name: '校对',
|
||
required: true,
|
||
handlers: [],
|
||
options: [],
|
||
loading: false
|
||
},
|
||
{
|
||
name: '审核',
|
||
required: true,
|
||
handlers: [],
|
||
options: [],
|
||
loading: false
|
||
},
|
||
{
|
||
name: '批准',
|
||
required: true,
|
||
handlers: [],
|
||
options: [],
|
||
loading: false
|
||
}
|
||
]
|
||
// 获取审批人员
|
||
this.loadApprovalUsers(currentFlowId)
|
||
}
|
||
} else if (flowId && this.reportDetail.status && this.reportDetail.status !== 'DRAFT') {
|
||
// 如果flowId存在且状态不是草稿,加载审批流程
|
||
this.getApprovalSpet(flowId)
|
||
}
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('获取报告详情失败:', err)
|
||
this.$message.error('获取报告详情失败,请重试')
|
||
})
|
||
},
|
||
getApprovalSpet(flowId) {
|
||
if (!flowId) {
|
||
return
|
||
}
|
||
const reportId = this.reportDetail.reportId || this.reportDetail.id || this.detail.reportId || this.detail.id
|
||
if (!reportId) {
|
||
return
|
||
}
|
||
const request = {
|
||
...api.GET_REPORT_DETAIL_PROCESS,
|
||
params: {
|
||
flowId: flowId// 报告ID作为templateId参数
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
console.log('审批流程数据:', resp)
|
||
const data = resp.data || resp
|
||
// 保存流程详情
|
||
this.progressDetail = {
|
||
explanation: data.explanation || '',
|
||
flowStatus: data.flowStatus || '',
|
||
progressTitle: data.progressTitle || '',
|
||
templateVersion: data.templateVersion || '',
|
||
contentName: data.contentName || ''
|
||
}
|
||
this.reportDetail.processTitle = data.progressTitle
|
||
// 如果 flowStatus 是 REJECTED,强制设置当前环节为"草稿",当前处理人为创建人
|
||
const flowStatus = data.flowStatus || ''
|
||
const createBy = data.createBy || ''
|
||
if (flowStatus) {
|
||
// 将报告状态与流程状态保持一致
|
||
this.$set(this.reportDetail, 'status', flowStatus)
|
||
}
|
||
this.showTopDetail = {
|
||
processTitle: data.progressTitle,
|
||
createBy: createBy,
|
||
createTime: data.createTime || '',
|
||
currentNode: flowStatus === 'REJECTED' ? '编制' : (data.currentNode || ''),
|
||
currentProcessor: flowStatus === 'REJECTED' ? createBy : (data.processor || '')
|
||
}
|
||
// 保存流程节点
|
||
this.flowNodes = data.flowNodes || []
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('获取审批流程数据失败:', err)
|
||
this.flowNodes = []
|
||
this.progressDetail = {}
|
||
})
|
||
},
|
||
handleBack() {
|
||
this.$emit('close')
|
||
},
|
||
handleDownload(reportDetail) {
|
||
const link = document.createElement('a')
|
||
link.href = reportDetail.filePath
|
||
link.style.display = 'none'
|
||
document.body.appendChild(link)
|
||
link.click()
|
||
document.body.removeChild(link)
|
||
},
|
||
// 处理退回按钮点击
|
||
handleReturn() {
|
||
if (!this.detail || !this.detail.flowId) {
|
||
this.$message.error('缺少审批信息')
|
||
return
|
||
}
|
||
|
||
const request = {
|
||
...api.GET_CAN_RETURN_NODE,
|
||
params: {
|
||
flowId: this.detail.flowId
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
console.log(resp)
|
||
// 在列表最前面添加"编制"节点作为默认选项
|
||
const nodes = Array.isArray(resp.data) ? resp.data : []
|
||
this.returnableNodes = ['编制', ...nodes]
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('获取退回节点失败:', err)
|
||
// 即使获取失败,也添加"编制"节点
|
||
this.returnableNodes = ['编制']
|
||
})
|
||
this.selectedReturnNode = ''
|
||
this.returnOpinion = '拒绝'
|
||
this.returnNodeDialogVisible = true
|
||
},
|
||
// 关闭退回节点选择弹窗
|
||
handleReturnNodeDialogClose() {
|
||
this.returnNodeDialogVisible = false
|
||
this.selectedReturnNode = ''
|
||
this.returnOpinion = '拒绝'
|
||
},
|
||
// 处理退回节点选择变化
|
||
handleReturnNodeChange() {
|
||
// 如果不是"编制"节点,清空意见
|
||
if (this.selectedReturnNode !== '编制') {
|
||
this.returnOpinion = ''
|
||
} else {
|
||
// 如果选择"编制"节点,设置默认值为"拒绝"
|
||
if (!this.returnOpinion) {
|
||
this.returnOpinion = '拒绝'
|
||
}
|
||
}
|
||
},
|
||
// 提交退回操作
|
||
handleReturnSubmit() {
|
||
if (!this.selectedReturnNode) {
|
||
this.$message.warning('请选择要退回的节点')
|
||
return
|
||
}
|
||
|
||
if (!this.detail || !this.detail.flowId) {
|
||
this.$message.error('缺少审批信息')
|
||
return
|
||
}
|
||
|
||
this.returnSubmitting = true
|
||
|
||
// 如果选择的是"编制"节点,调用拒绝接口
|
||
if (this.selectedReturnNode === '编制') {
|
||
const submitData = {
|
||
flowId: this.detail.flowId,
|
||
userId: getUserId(),
|
||
result: 'REJECT',
|
||
opinion: this.returnOpinion || '' // 意见非必填,可以为空
|
||
}
|
||
|
||
const request = {
|
||
...api.START_REPORT_APPROVAL,
|
||
params: submitData,
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
this.handleReturnNodeDialogClose()
|
||
// 重新加载审批流程数据
|
||
if (this.detail.flowId) {
|
||
this.getApprovalSpet(this.detail.flowId)
|
||
}
|
||
// 通知父组件刷新数据
|
||
this.$message({
|
||
message: '拒绝成功',
|
||
type: 'success'
|
||
})
|
||
this.$emit('refresh')
|
||
this.returnSubmitting = false
|
||
// 执行返回按钮的点击
|
||
this.handleBack()
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('拒绝失败:', err)
|
||
this.$message({
|
||
message: err.message || '拒绝失败,请重试',
|
||
type: 'error'
|
||
})
|
||
this.returnSubmitting = false
|
||
})
|
||
.finally(() => {
|
||
this.returnSubmitting = false
|
||
})
|
||
} else {
|
||
// 其他节点,调用退回接口
|
||
// 获取流程发起人
|
||
const createUserId = this.reportDetail.createdById || this.detail.createdById || ''
|
||
if (!createUserId) {
|
||
this.$message.error('无法获取流程发起人信息')
|
||
this.returnSubmitting = false
|
||
return
|
||
}
|
||
|
||
// 获取当前处理人
|
||
const processUserId = getUserId()
|
||
|
||
// 构建提交数据
|
||
const submitData = {
|
||
flowId: this.detail.flowId,
|
||
nodeName: this.selectedReturnNode,
|
||
createUserId: createUserId,
|
||
processUserId: processUserId
|
||
}
|
||
|
||
const request = {
|
||
...api.RETURN_REPORT_TODO,
|
||
params: submitData,
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
this.handleReturnNodeDialogClose()
|
||
// 重新加载审批流程数据
|
||
if (this.detail.flowId) {
|
||
this.getApprovalSpet(this.detail.flowId)
|
||
}
|
||
// 通知父组件刷新数据
|
||
this.$message({
|
||
message: '退回成功',
|
||
type: 'success'
|
||
})
|
||
this.$emit('refresh')
|
||
this.returnSubmitting = false
|
||
// 执行返回按钮的点击
|
||
this.handleBack()
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('退回失败:', err)
|
||
this.$message({
|
||
message: err.message || '退回失败,请重试',
|
||
type: 'error'
|
||
})
|
||
this.returnSubmitting = false
|
||
})
|
||
.finally(() => {
|
||
this.returnSubmitting = false
|
||
})
|
||
}
|
||
},
|
||
// 处理直送按钮点击
|
||
handleResend() {
|
||
const reportId = this.reportDetail.reportId || this.reportDetail.id || this.detail.reportId || this.detail.id
|
||
if (!reportId) {
|
||
this.$message.error('缺少报告ID,无法执行直送操作')
|
||
return
|
||
}
|
||
|
||
this.$confirm('确定要执行直送操作吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(() => {
|
||
this.resending = true
|
||
const request = {
|
||
...api.RESEND_REPORT_APPROVAL,
|
||
params: {
|
||
reportId: reportId,
|
||
userId: getUserId()
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
// 重新加载审批流程数据
|
||
if (this.detail.flowId) {
|
||
this.getApprovalSpet(this.detail.flowId)
|
||
}
|
||
// 通知父组件刷新数据
|
||
this.$message({
|
||
message: '直送成功',
|
||
type: 'success'
|
||
})
|
||
this.$emit('refresh')
|
||
this.resending = false
|
||
// 执行返回按钮的点击
|
||
this.handleBack()
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('直送失败:', err)
|
||
this.$message({
|
||
message: err.message || '直送失败,请重试',
|
||
type: 'error'
|
||
})
|
||
this.resending = false
|
||
})
|
||
.finally(() => {
|
||
this.resending = false
|
||
})
|
||
})
|
||
.catch(() => {
|
||
// 取消操作
|
||
})
|
||
},
|
||
// 处理提交按钮点击
|
||
handleSubmit() {
|
||
this.approvalFormData = {
|
||
result: 'PASS',
|
||
opinion: '同意' // 默认值为"同意"
|
||
}
|
||
this.approvalDialogVisible = true
|
||
// 清除表单验证
|
||
this.$nextTick(() => {
|
||
if (this.$refs.approvalForm) {
|
||
this.$refs.approvalForm.clearValidate()
|
||
}
|
||
})
|
||
},
|
||
handleApprovalDialogClose() {
|
||
this.approvalDialogVisible = false
|
||
this.approvalFormData = {
|
||
result: '',
|
||
opinion: ''
|
||
}
|
||
// 清除表单验证
|
||
if (this.$refs.approvalForm) {
|
||
this.$refs.approvalForm.clearValidate()
|
||
}
|
||
},
|
||
handleApprovalSubmit() {
|
||
// 表单验证
|
||
this.$refs.approvalForm.validate((valid) => {
|
||
if (!valid) {
|
||
return false
|
||
}
|
||
|
||
if (!this.detail || !this.detail.flowId) {
|
||
this.$message.error('缺少审批信息')
|
||
return
|
||
}
|
||
|
||
// 构建提交数据
|
||
const submitData = {
|
||
flowId: this.detail.flowId,
|
||
userId: getUserId(),
|
||
result: this.approvalFormData.result, // 'PASS'
|
||
opinion: this.approvalFormData.opinion || ''
|
||
}
|
||
|
||
this.approvalSubmitting = true
|
||
const request = {
|
||
...api.START_REPORT_APPROVAL,
|
||
params: submitData,
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
this.handleApprovalDialogClose()
|
||
// 重新加载审批流程数据
|
||
if (this.detail.flowId) {
|
||
this.getApprovalSpet(this.detail.flowId)
|
||
}
|
||
// 通知父组件刷新数据
|
||
this.$message({
|
||
message: '审批提交成功',
|
||
type: 'success'
|
||
})
|
||
this.$emit('refresh')
|
||
this.approvalSubmitting = false
|
||
// 执行返回按钮的点击
|
||
this.handleBack()
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('审批提交失败:', err)
|
||
this.$message({
|
||
message: '审批提交失败,请重试',
|
||
type: 'error'
|
||
})
|
||
this.approvalSubmitting = false
|
||
})
|
||
.finally(() => {
|
||
this.approvalSubmitting = false
|
||
})
|
||
})
|
||
},
|
||
// 对steps数组进行去重,优先保留有approvalTime的记录
|
||
deduplicateSteps(steps) {
|
||
if (!Array.isArray(steps) || steps.length === 0) {
|
||
return []
|
||
}
|
||
|
||
// 使用Map来存储已见过的唯一标识和对应的记录
|
||
const seen = new Map()
|
||
const result = []
|
||
|
||
steps.forEach(step => {
|
||
// 生成唯一标识:优先使用id,如果没有id则使用多个字段组合
|
||
let uniqueKey = null
|
||
|
||
if (step.id) {
|
||
// 如果有id,使用id作为唯一标识
|
||
uniqueKey = `id_${step.id}`
|
||
} else {
|
||
// 如果没有id,使用多个字段组合作为唯一标识
|
||
const userName = step.userName || step.operator || step.username || step.handlerName || step.handler || ''
|
||
const approvalTime = step.approvalTime || step.handleTime || step.createTime || ''
|
||
const stepResult = step.result || step.status || ''
|
||
uniqueKey = `combo_${userName}_${approvalTime}_${stepResult}`
|
||
}
|
||
|
||
if (!uniqueKey) {
|
||
return // 跳过无法生成唯一标识的记录
|
||
}
|
||
|
||
// 检查是否已存在相同唯一标识的记录
|
||
if (seen.has(uniqueKey)) {
|
||
// 如果已存在,比较两条记录,优先保留有approvalTime的记录
|
||
const existingIndex = seen.get(uniqueKey)
|
||
const existingStep = result[existingIndex]
|
||
|
||
const currentHasTime = !!(step.approvalTime || step.handleTime || step.createTime)
|
||
const existingHasTime = !!(existingStep.approvalTime || existingStep.handleTime || existingStep.createTime)
|
||
|
||
// 如果当前记录有approvalTime而已存在的没有,则替换
|
||
if (currentHasTime && !existingHasTime) {
|
||
result[existingIndex] = step
|
||
}
|
||
// 如果两条都有或都没有,保留已存在的(第一条)
|
||
} else {
|
||
// 如果这个唯一标识还没有出现过,添加到结果中
|
||
seen.set(uniqueKey, result.length)
|
||
result.push(step)
|
||
}
|
||
})
|
||
|
||
// 对所有记录的approvalTime进行格式化
|
||
return result.map(step => {
|
||
const formattedStep = { ...step }
|
||
if (formattedStep.approvalTime) {
|
||
formattedStep.approvalTime = this.formatTime(formattedStep.approvalTime)
|
||
}
|
||
if (formattedStep.handleTime) {
|
||
formattedStep.handleTime = this.formatTime(formattedStep.handleTime)
|
||
}
|
||
if (formattedStep.createTime) {
|
||
formattedStep.createTime = this.formatTime(formattedStep.createTime)
|
||
}
|
||
return formattedStep
|
||
})
|
||
},
|
||
// 发起功能:直接调用发起接口(按照report.vue的方式)
|
||
handleLaunch() {
|
||
const reportId = this.reportDetail.reportId || this.reportDetail.id || this.detail.reportId || this.detail.id
|
||
const flowId = this.approvalFlowIdFromUsers || this.detail.flowId || this.reportDetail.flowId
|
||
const canSend = this.canResend
|
||
|
||
if (!reportId) {
|
||
this.$message.error('缺少报告ID,无法发起审批')
|
||
return
|
||
}
|
||
|
||
if (!flowId) {
|
||
this.$message.error('缺少流程ID,无法发起审批')
|
||
return
|
||
}
|
||
|
||
this.$confirm('确定发起审批吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(() => {
|
||
let request = null
|
||
if (canSend) {
|
||
// 可继续发起时,调用继续审批接口,使用审批人员接口返回的 flowId
|
||
const continueFlowId = this.approvalFlowIdFromUsers || flowId
|
||
if (!continueFlowId) {
|
||
this.$message.error('缺少流程ID,无法继续发起审批')
|
||
return
|
||
}
|
||
request = {
|
||
...api.CONTINUE_REPORT_APPROVAL,
|
||
params: {
|
||
flowId: continueFlowId,
|
||
userId: getUserId()
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
} else {
|
||
const submitData = {
|
||
reportId: reportId,
|
||
flowId: flowId,
|
||
userId: getUserId()
|
||
}
|
||
request = {
|
||
...api.ADD_REPORT,
|
||
params: submitData,
|
||
disableSuccessMsg: true
|
||
}
|
||
}
|
||
|
||
this.launching = true
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
this.launching = false
|
||
// 重新加载详情
|
||
this.loadReportDetailFromApi(reportId, flowId)
|
||
this.$message({
|
||
type: 'success',
|
||
message: '发起成功'
|
||
})
|
||
// 触发刷新事件
|
||
this.$emit('refresh')
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('发起失败:', err)
|
||
this.launching = false
|
||
this.$message({
|
||
type: 'error',
|
||
message: '发起失败,请重试'
|
||
})
|
||
})
|
||
})
|
||
.catch(() => {
|
||
this.$message({
|
||
type: 'info',
|
||
message: '取消发起审批'
|
||
})
|
||
})
|
||
},
|
||
handleResatrtLaunch() {
|
||
const reportId = this.reportDetail.reportId || this.reportDetail.id || this.detail.reportId || this.detail.id
|
||
|
||
if (!reportId) {
|
||
this.$message.error('缺少报告ID,无法发起审批')
|
||
return
|
||
}
|
||
this.$confirm('确定重新发起吗?', '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(() => {
|
||
// 按照保存时的格式转换 nodeList
|
||
const nodeList = this.approvalWorkflowSteps.map((step, index) => ({
|
||
nodeName: step.name,
|
||
assignees: step.handlers,
|
||
nodeOrder: index + 1
|
||
}))
|
||
const submitData = {
|
||
reportId: reportId,
|
||
...this.editFormData,
|
||
flowId: '',
|
||
title: this.editingProcessTitle || '',
|
||
processTitle: this.editFormData.processTitle || '',
|
||
explanation: '',
|
||
nodeList: nodeList,
|
||
userId: getUserId()
|
||
}
|
||
const request = {
|
||
...api.RESTART_REPORT_APPROVAL,
|
||
params: submitData,
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.launching = true
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
this.launching = false
|
||
// 重新加载详情
|
||
const currentFlowId = this.detail.flowId || this.reportDetail.flowId
|
||
this.loadReportDetailFromApi(reportId, currentFlowId)
|
||
this.$message({
|
||
type: 'success',
|
||
message: '发起成功'
|
||
})
|
||
// 触发刷新事件
|
||
this.$emit('refresh')
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('发起失败:', err)
|
||
this.launching = false
|
||
this.$message({
|
||
type: 'error',
|
||
message: '发起失败,请重试'
|
||
})
|
||
})
|
||
})
|
||
.catch(() => {
|
||
this.$message({
|
||
type: 'info',
|
||
message: '取消发起审批'
|
||
})
|
||
})
|
||
},
|
||
// 加载审批人(用于草稿状态显示和编辑模式)
|
||
loadApprovalUsers(flowId) {
|
||
if (!flowId) {
|
||
return Promise.resolve()
|
||
}
|
||
const request = {
|
||
...api.GET_REPORT_APPROVAL_USERS,
|
||
params: {
|
||
flowId: flowId
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
return this.$request(request)
|
||
.asyncThen((resp) => {
|
||
console.log('获取审批流程信息:', resp)
|
||
// 记录可继续发起的标识及流程ID
|
||
const meta = this.parseApprovalUsersMeta(resp)
|
||
this.approvalCanSend = !!meta.canSend
|
||
this.approvalFlowIdFromUsers = meta.flowId || flowId || ''
|
||
// 同步流程主题(接口返回为准)
|
||
if (meta.title) {
|
||
this.reportDetail.processTitle = meta.title
|
||
this.showTopDetail.processTitle = meta.title
|
||
// 编辑态下也同步到表单,避免保存后显示旧值
|
||
if (this.isEditMode) {
|
||
this.editFormData.processTitle = meta.title
|
||
}
|
||
}
|
||
this.fillApprovalUsersData(resp, this.approvalWorkflowSteps)
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('获取审批流程信息失败:', err)
|
||
throw err
|
||
})
|
||
},
|
||
// 提取审批人员接口返回的流程ID/流程主题与可发起标识
|
||
parseApprovalUsersMeta(resp) {
|
||
const meta = { flowId: '', canSend: false, title: '' }
|
||
const candidate = resp?.data ?? resp
|
||
const pickFrom = (obj) => {
|
||
if (!obj || Array.isArray(obj)) return
|
||
meta.flowId = meta.flowId || obj.flowId || obj.id || obj.flowid || ''
|
||
meta.canSend = meta.canSend || obj.canSend || obj.canResend || false
|
||
meta.title = meta.title || obj.progressTitle || obj.processTitle || obj.title || ''
|
||
}
|
||
if (candidate && !Array.isArray(candidate)) {
|
||
pickFrom(candidate)
|
||
if (candidate.data && !Array.isArray(candidate.data)) {
|
||
pickFrom(candidate.data)
|
||
}
|
||
}
|
||
return meta
|
||
},
|
||
// 填充审批人数据(按照report.vue的逻辑)
|
||
fillApprovalUsersData(resp, workflowSteps) {
|
||
// 提取节点列表
|
||
let nodeList = []
|
||
let data = null
|
||
if (Array.isArray(resp.data)) {
|
||
data = resp.data
|
||
} else if (Array.isArray(resp.data?.data)) {
|
||
data = resp.data.data
|
||
} else if (Array.isArray(resp.data?.result)) {
|
||
data = resp.data.result
|
||
} else if (Array.isArray(resp)) {
|
||
data = resp
|
||
} else if (resp.data && !Array.isArray(resp.data)) {
|
||
data = resp.data
|
||
}
|
||
|
||
if (Array.isArray(data)) {
|
||
nodeList = data
|
||
} else if (data && Array.isArray(data.data)) {
|
||
nodeList = data.data
|
||
} else if (data && Array.isArray(data.result)) {
|
||
nodeList = data.result
|
||
} else if (data && Array.isArray(data.nodeList)) {
|
||
nodeList = data.nodeList
|
||
} else if (data && Array.isArray(data.flowNodes)) {
|
||
nodeList = data.flowNodes
|
||
}
|
||
|
||
// 遍历节点列表,根据 nodeName 匹配到对应的审批环节并填充人员信息
|
||
if (nodeList && nodeList.length > 0) {
|
||
nodeList.forEach((nodeItem) => {
|
||
const nodeName = nodeItem.nodeName || nodeItem.node || ''
|
||
if (!nodeName) {
|
||
return
|
||
}
|
||
|
||
// 找到对应的审批环节
|
||
const step = workflowSteps.find(s => s.name === nodeName)
|
||
if (!step) {
|
||
return
|
||
}
|
||
|
||
// 获取 assignees 数组
|
||
const assignees = nodeItem.assignees || []
|
||
if (!Array.isArray(assignees) || assignees.length === 0) {
|
||
return
|
||
}
|
||
|
||
// 从 assignees 中提取人员信息(将 id 统一转换为 userId)
|
||
const handlerInfos = assignees.map(assignee => {
|
||
const userId = assignee.userId || assignee.id || ''
|
||
return {
|
||
userId: userId,
|
||
username: assignee.username || '',
|
||
userNumber: assignee.userNumber || assignee.account || ''
|
||
}
|
||
}).filter(item => item.userId)
|
||
|
||
// 如果有处理人信息,设置到 handlers 中(handlers 存储 userId 数组)
|
||
if (handlerInfos.length > 0) {
|
||
step.handlers = handlerInfos.map(h => h.userId).filter(Boolean)
|
||
// 设置 options,确保已选择的人员信息在选项中(用于下拉框显示)
|
||
step.options = handlerInfos.map(h => ({
|
||
userId: h.userId,
|
||
username: h.username || '',
|
||
userNumber: h.userNumber || ''
|
||
})).filter(opt => opt.userId)
|
||
}
|
||
})
|
||
}
|
||
},
|
||
// 获取处理人名称(用于只读显示)
|
||
getHandlerName(handlerId, options) {
|
||
const handler = options.find(opt => opt.userId === handlerId)
|
||
return handler ? handler.username : handlerId
|
||
},
|
||
// 远程搜索人员(用于编辑模式)
|
||
handleRemoteSearch(query, stepIndex) {
|
||
const step = this.approvalWorkflowSteps[stepIndex]
|
||
if (!query || query.trim() === '') {
|
||
// 空查询时,只保留已选择的人员在选项中
|
||
if (step.handlers && step.handlers.length > 0) {
|
||
const selectedOptions = step.options.filter((opt) => step.handlers.includes(opt.userId))
|
||
step.options = selectedOptions
|
||
} else {
|
||
step.options = []
|
||
}
|
||
return
|
||
}
|
||
|
||
step.loading = true
|
||
const trimmedQuery = query.trim()
|
||
const request = {
|
||
...api.GET_ALLUSERS,
|
||
params: {
|
||
name: trimmedQuery
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
const respData = resp && resp.data
|
||
let result = []
|
||
if (Array.isArray(respData)) {
|
||
result = respData
|
||
} else if (respData && Array.isArray(respData.result)) {
|
||
result = respData.result
|
||
} else if (respData && respData.data && Array.isArray(respData.data)) {
|
||
result = respData.data
|
||
}
|
||
// 转换为选项格式
|
||
const users = result
|
||
.map((user) => ({
|
||
userId: user.userId || user.id,
|
||
username: user.username || user.name,
|
||
userNumber: user.userNumber || user.userId || user.id
|
||
}))
|
||
.filter((user) => user.userId && user.userNumber)
|
||
const existingIds = new Set(step.handlers || [])
|
||
const filteredUsers = users.filter((user) => !existingIds.has(user.userId))
|
||
const selectedOptions = (step.options || []).filter(
|
||
(opt) => opt.userId && step.handlers && step.handlers.includes(opt.userId)
|
||
)
|
||
const optionsMap = new Map()
|
||
selectedOptions.forEach((opt) => {
|
||
if (opt.userNumber) {
|
||
optionsMap.set(opt.userNumber, opt)
|
||
}
|
||
})
|
||
filteredUsers.forEach((user) => {
|
||
if (user.userNumber && !optionsMap.has(user.userNumber)) {
|
||
optionsMap.set(user.userNumber, user)
|
||
}
|
||
})
|
||
step.options = Array.from(optionsMap.values())
|
||
step.loading = false
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('搜索人员失败:', err)
|
||
if (step.handlers && step.handlers.length > 0) {
|
||
const selectedOptions = step.options.filter((opt) => step.handlers.includes(opt.userId))
|
||
step.options = selectedOptions
|
||
} else {
|
||
step.options = []
|
||
}
|
||
step.loading = false
|
||
})
|
||
},
|
||
// 处理人员选择变化(用于编辑模式)
|
||
handleHandlerChange(stepIndex, selectedIds) {
|
||
// 提交时通过快照对比判断是否变化
|
||
},
|
||
// 处理重新发起按钮点击(已拒绝状态)
|
||
handleResendApproval() {
|
||
// 直接调用重新发起接口(
|
||
this.handleResatrtLaunch()
|
||
},
|
||
handleProcessTitleChange() {
|
||
// 提交时通过快照对比判断是否变化
|
||
},
|
||
// 加载审批人(用于发起弹窗,草稿和已拒绝状态)
|
||
loadLaunchApprovalUsers(flowId) {
|
||
if (!flowId) {
|
||
console.error('缺少流程ID,无法获取审批人员信息')
|
||
return
|
||
}
|
||
const request = {
|
||
...api.GET_REPORT_APPROVAL_USERS,
|
||
params: {
|
||
flowId: flowId
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
console.log('获取审批流程信息:', resp)
|
||
// 使用 fillResendApprovalData 填充数据(它会处理流程主题和审批人)
|
||
this.fillResendApprovalData(resp)
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('获取审批流程信息失败:', err)
|
||
})
|
||
},
|
||
// 修改功能:进入编辑模式
|
||
async handleEdit() {
|
||
// 保存原始数据
|
||
this.originalReportDetail = JSON.parse(JSON.stringify(this.reportDetail))
|
||
// 填充表单数据
|
||
this.editFormData = {
|
||
title: this.reportDetail.title || '',
|
||
manufacturerName: this.reportDetail.manufacturerName || '',
|
||
modelName: this.reportDetail.modelName || '',
|
||
manufacturerAbbreviation: this.reportDetail.manufacturerAbbreviation || '',
|
||
modelAbbreviation: this.reportDetail.modelAbbreviation || '',
|
||
productNumber: this.reportDetail.productNumber || '',
|
||
module: this.reportDetail.module || '',
|
||
plate: this.reportDetail.plate || '',
|
||
platform: this.reportDetail.platform || '',
|
||
series: this.reportDetail.series || '',
|
||
marketSegment: this.reportDetail.marketSegment || '',
|
||
projectName: this.reportDetail.projectName || '',
|
||
projectNumber: this.reportDetail.projectNumber || '',
|
||
explanation: this.reportDetail.explanation || '',
|
||
fileUrl: this.reportDetail.fileUrl || this.reportDetail.filePath || '',
|
||
fileName: this.reportDetail.fileName || '',
|
||
processTitle: this.reportDetail.processTitle || this.showTopDetail.processTitle || ''
|
||
}
|
||
// 如果有文件路径,显示在文件列表中
|
||
if (this.reportDetail.fileUrl || this.reportDetail.filePath) {
|
||
this.fileList = [
|
||
{
|
||
name: this.reportDetail.fileName || '文件',
|
||
url: this.reportDetail.fileUrl || this.reportDetail.filePath
|
||
}
|
||
]
|
||
} else {
|
||
this.fileList = []
|
||
}
|
||
this.selectedFile = null
|
||
this.isEditMode = true
|
||
// 如果是草稿状态,确保审批人员/流程主题已回填后再记录快照(避免回填触发“误判修改”)
|
||
if (this.reportDetail.status === 'DRAFT') {
|
||
const flowId = this.detail.flowId || this.reportDetail.flowId
|
||
if (flowId) {
|
||
try {
|
||
await this.loadApprovalUsers(flowId)
|
||
} catch (e) {
|
||
// ignore
|
||
}
|
||
}
|
||
}
|
||
this.originalEditSnapshot = this.buildEditSnapshot()
|
||
},
|
||
// 取消编辑:恢复只读状态
|
||
handleCancelEdit() {
|
||
// 恢复原始数据
|
||
this.reportDetail = JSON.parse(JSON.stringify(this.originalReportDetail))
|
||
// 清空表单数据
|
||
this.editFormData = {
|
||
title: '',
|
||
manufacturerName: '',
|
||
modelName: '',
|
||
manufacturerAbbreviation: '',
|
||
modelAbbreviation: '',
|
||
productNumber: '',
|
||
module: '',
|
||
plate: '',
|
||
platform: '',
|
||
series: '',
|
||
marketSegment: '',
|
||
projectName: '',
|
||
projectNumber: '',
|
||
explanation: '',
|
||
fileUrl: '',
|
||
fileName: '',
|
||
processTitle: ''
|
||
}
|
||
this.fileList = []
|
||
this.selectedFile = null
|
||
this.originalEditSnapshot = null
|
||
this.isEditMode = false
|
||
// 清除表单验证
|
||
if (this.$refs.editForm) {
|
||
this.$refs.editForm.clearValidate()
|
||
}
|
||
},
|
||
// 文件变化
|
||
handleFileChange(file, fileList) {
|
||
const allowedTypes = ['.doc', '.pdf', '.xls', '.xlsx', '.ppt', '.pptx']
|
||
const fileName = file.name.toLowerCase()
|
||
const isValidType = allowedTypes.some((type) => fileName.endsWith(type))
|
||
|
||
if (!isValidType) {
|
||
this.$message.error('不支持的文件类型,仅支持 .doc .pdf .xls .xlsx .ppt .pptx')
|
||
this.$refs.upload.handleRemove(file)
|
||
return false
|
||
}
|
||
|
||
this.selectedFile = file && file.raw ? file.raw : null
|
||
this.fileList = fileList.slice(-1)
|
||
},
|
||
// 文件移除
|
||
handleFileRemove(file, fileList) {
|
||
this.selectedFile = null
|
||
this.fileList = []
|
||
},
|
||
// 文件上传
|
||
handleFileUpload() {
|
||
if (this.uploading || !this.selectedFile) {
|
||
if (!this.selectedFile) {
|
||
return Promise.resolve(null)
|
||
}
|
||
return Promise.resolve(null)
|
||
}
|
||
|
||
const file = this.selectedFile
|
||
const formData = new FormData()
|
||
formData.append('file', file)
|
||
formData.append('createBy', getUserId())
|
||
|
||
const request = {
|
||
...api.TEST_FILE_UPLOAD,
|
||
params: formData,
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.uploading = true
|
||
const loadingInstance = this.$loading({
|
||
lock: true,
|
||
text: '正在上传文件,请稍候...',
|
||
spinner: 'el-icon-loading',
|
||
background: 'rgba(0, 0, 0, 0.7)'
|
||
})
|
||
return this.$upload(request, null, false)
|
||
.then((resp) => {
|
||
console.log('文件上传成功:', resp)
|
||
loadingInstance.close()
|
||
let fileUrl = ''
|
||
if (resp.data && resp) {
|
||
const url = resp.data
|
||
fileUrl = url
|
||
this.editFormData.fileUrl = fileUrl
|
||
}
|
||
return fileUrl
|
||
})
|
||
.catch((err) => {
|
||
console.error('文件上传失败:', err)
|
||
loadingInstance.close()
|
||
this.$message.error('文件上传失败,请重试')
|
||
throw err
|
||
})
|
||
.finally(() => {
|
||
if (loadingInstance) {
|
||
loadingInstance.close()
|
||
}
|
||
this.uploading = false
|
||
})
|
||
},
|
||
// 保存功能
|
||
handleSave() {
|
||
// 表单验证
|
||
if (this.$refs.editForm) {
|
||
this.$refs.editForm.validate((valid) => {
|
||
if (!valid) {
|
||
return false
|
||
}
|
||
this.submitEdit()
|
||
})
|
||
} else {
|
||
this.submitEdit()
|
||
}
|
||
},
|
||
buildEditSnapshot() {
|
||
const normalize = (v) => (v == null ? '' : v)
|
||
const normalizeSteps = (steps) =>
|
||
(steps || []).map((s) => ({
|
||
name: s.name,
|
||
handlers: (s.handlers || []).filter(Boolean).slice().sort()
|
||
}))
|
||
// 仅用于对比的字段集合
|
||
return {
|
||
form: {
|
||
title: normalize(this.editFormData?.title),
|
||
manufacturerName: normalize(this.editFormData?.manufacturerName),
|
||
modelName: normalize(this.editFormData?.modelName),
|
||
manufacturerAbbreviation: normalize(this.editFormData?.manufacturerAbbreviation),
|
||
modelAbbreviation: normalize(this.editFormData?.modelAbbreviation),
|
||
productNumber: normalize(this.editFormData?.productNumber),
|
||
module: normalize(this.editFormData?.module),
|
||
plate: normalize(this.editFormData?.plate),
|
||
platform: normalize(this.editFormData?.platform),
|
||
series: normalize(this.editFormData?.series),
|
||
marketSegment: normalize(this.editFormData?.marketSegment),
|
||
projectName: normalize(this.editFormData?.projectName),
|
||
projectNumber: normalize(this.editFormData?.projectNumber),
|
||
explanation: normalize(this.editFormData?.explanation),
|
||
fileUrl: normalize(this.editFormData?.fileUrl),
|
||
fileName: normalize(this.editFormData?.fileName)
|
||
},
|
||
processTitle: (this.editFormData?.processTitle || '').trim(),
|
||
steps: normalizeSteps(this.approvalWorkflowSteps)
|
||
}
|
||
},
|
||
diffEditSnapshot(original, current) {
|
||
const isSame = (a, b) => JSON.stringify(a) === JSON.stringify(b)
|
||
const baseChanged =
|
||
!original ||
|
||
!isSame(original.form, current.form) ||
|
||
!!this.selectedFile // 新选中文件一定算基础变更
|
||
const approvalChanged =
|
||
!original ||
|
||
original.processTitle !== current.processTitle ||
|
||
!isSame(original.steps, current.steps)
|
||
return { baseChanged, approvalChanged }
|
||
},
|
||
hasAnyEditChange() {
|
||
const current = this.buildEditSnapshot()
|
||
const { baseChanged, approvalChanged } = this.diffEditSnapshot(this.originalEditSnapshot, current)
|
||
return baseChanged || approvalChanged
|
||
},
|
||
// 提交编辑
|
||
submitEdit() {
|
||
const submitData = async () => {
|
||
try {
|
||
// 没有任何修改时,不调用接口
|
||
if (!this.hasAnyEditChange()) {
|
||
this.isEditMode = false
|
||
this.$message({
|
||
type: 'info',
|
||
message: '未修改,无需保存'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 如果有新文件,先上传文件获取文件路径
|
||
if (this.selectedFile) {
|
||
const fileUrl = await this.handleFileUpload()
|
||
this.editFormData.fileUrl = fileUrl || ''
|
||
this.editFormData.fileName = this.selectedFile.name
|
||
}
|
||
|
||
const reportId = this.reportDetail.reportId || this.reportDetail.id
|
||
if (!reportId) {
|
||
this.$message.error('缺少报告ID')
|
||
return
|
||
}
|
||
|
||
const includeApprovalInSave = !this.canResend
|
||
const shouldUpdateApprovalProcess = this.canResend
|
||
|
||
const { processTitle, ...restForm } = this.editFormData
|
||
const params = {
|
||
reportId: reportId,
|
||
flowId: this.detail.flowId,
|
||
...restForm,
|
||
createBy: getUserId(),
|
||
userId: getUserId()
|
||
}
|
||
|
||
if (includeApprovalInSave) {
|
||
params.processTitle = processTitle || ''
|
||
params.nodeList = this.approvalWorkflowSteps.map((step, index) => ({
|
||
nodeName: step.name,
|
||
assignees: step.handlers,
|
||
nodeOrder: index + 1
|
||
}))
|
||
}
|
||
|
||
this.saving = true
|
||
// 提交时对比快照判断是否有变化(不依赖watch/change)
|
||
const currentSnap = this.buildEditSnapshot()
|
||
const { baseChanged, approvalChanged } = this.diffEditSnapshot(this.originalEditSnapshot, currentSnap)
|
||
// 当 canResend 为 true 时(shouldUpdateApprovalProcess 为 true),审批环节和流程主题的修改应该走 updateApprovalProcess
|
||
// 当 canResend 为 false 时(includeApprovalInSave 为 true),审批环节和流程主题的修改应该包含在 EDIT_REPORT 中
|
||
const needUpdateApprovalProcess =
|
||
shouldUpdateApprovalProcess && approvalChanged
|
||
// 判断是否需要调用 EDIT_REPORT:
|
||
// 1. 如果有基础变化(文件或表单字段),需要调用 EDIT_REPORT
|
||
// 2. 如果 includeApprovalInSave 为 true 且有审批变化,需要调用 EDIT_REPORT(将审批数据包含在保存中)
|
||
// 3. 如果 shouldUpdateApprovalProcess 为 true 且只有审批变化(没有基础变化),不应该调用 EDIT_REPORT
|
||
const shouldOnlyUpdateApprovalProcess = shouldUpdateApprovalProcess && approvalChanged && !baseChanged
|
||
const needUpdateReportBase =
|
||
!shouldOnlyUpdateApprovalProcess && (
|
||
baseChanged ||
|
||
(includeApprovalInSave && approvalChanged)
|
||
)
|
||
|
||
// 如果是草稿状态(includeApprovalInSave 为 true)且流程主题被修改,先调用 EDIT_APPROVAL_PROCESS_TITLE
|
||
if (includeApprovalInSave && approvalChanged && this.detail.flowId) {
|
||
const originalProcessTitle = (this.originalEditSnapshot?.processTitle || '').trim()
|
||
const currentProcessTitle = (currentSnap.processTitle || '').trim()
|
||
if (originalProcessTitle !== currentProcessTitle) {
|
||
const updateTitleRequest = {
|
||
...api.EDIT_APPROVAL_PROCESS_TITLE,
|
||
params: {
|
||
flowId: this.detail.flowId,
|
||
title: currentProcessTitle
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
// 先调用更新流程主题接口,成功后再调用编辑报告接口
|
||
await this.$request(updateTitleRequest)
|
||
.asyncThen(() => {
|
||
console.log('流程主题更新成功')
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('更新流程主题失败:', err)
|
||
this.$message.error('更新流程主题失败,请重试')
|
||
throw err
|
||
})
|
||
}
|
||
}
|
||
|
||
if (needUpdateReportBase) {
|
||
console.log('needUpdateReportBase', needUpdateReportBase)
|
||
const request = {
|
||
...api.EDIT_REPORT,
|
||
params: params,
|
||
disableSuccessMsg: true
|
||
}
|
||
await this.$request(request).asyncThen(() => {})
|
||
}
|
||
|
||
if (needUpdateApprovalProcess) {
|
||
console.log('needUpdateApprovalProcess', needUpdateApprovalProcess)
|
||
await this.updateApprovalProcess()
|
||
}
|
||
|
||
this.isEditMode = false
|
||
// 重新加载详情 + 草稿时刷新审批环节/流程主题(对齐 handleResatrtLaunch 的“重新获取”效果)
|
||
const currentFlowId = this.detail.flowId || this.reportDetail.flowId
|
||
this.loadReportDetailFromApi(reportId, currentFlowId)
|
||
if (this.reportDetail.status === 'DRAFT' && currentFlowId) {
|
||
// 重新获取审批环节与流程主题
|
||
this.loadApprovalUsers(currentFlowId)
|
||
}
|
||
// 更新快照(保存后以当前值为基准)
|
||
this.originalEditSnapshot = this.buildEditSnapshot()
|
||
this.$message({
|
||
type: 'success',
|
||
message: '保存成功'
|
||
})
|
||
// 触发刷新事件(保存后不返回上一级)
|
||
this.$emit('refresh', { keepOpen: true })
|
||
} catch (err) {
|
||
console.error('保存失败:', err)
|
||
this.$message.error(err?.message || '保存失败,请重试')
|
||
} finally {
|
||
this.saving = false
|
||
}
|
||
}
|
||
|
||
submitData()
|
||
},
|
||
// 更新审批环节(直送模式下单独调用流程修改接口)
|
||
updateApprovalProcess() {
|
||
const flowId = this.detail.flowId || this.reportDetail.flowId
|
||
if (!flowId) {
|
||
this.$message.error('缺少流程ID,无法保存审批环节')
|
||
return Promise.reject(new Error('缺少流程ID'))
|
||
}
|
||
|
||
const nodeList = this.approvalWorkflowSteps.map((step, index) => ({
|
||
nodeName: step.name,
|
||
assignees: step.handlers,
|
||
nodeOrder: index + 1
|
||
}))
|
||
|
||
const request = {
|
||
...api.EDIT_APPROVAL_PROCESS,
|
||
params: {
|
||
nodeList: nodeList,
|
||
flowId: flowId,
|
||
title: this.editFormData.processTitle || '',
|
||
userId: getUserId()
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
return this.$request(request)
|
||
.asyncThen(() => {})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('更新审批环节失败:', err)
|
||
this.$message.error(err?.message || '更新审批环节失败,请重试')
|
||
throw err
|
||
})
|
||
},
|
||
// 开始编辑流程主题
|
||
handleEditProcessTitle() {
|
||
this.isEditingProcessTitle = true
|
||
this.editingProcessTitle = this.showTopDetail.processTitle || this.detail.processTitle || this.reportDetail.processTitle || ''
|
||
},
|
||
// 取消编辑流程主题
|
||
handleCancelProcessTitle() {
|
||
this.isEditingProcessTitle = false
|
||
this.editingProcessTitle = ''
|
||
},
|
||
// 保存流程主题
|
||
handleSaveProcessTitle() {
|
||
if (!this.detail || !this.detail.flowId) {
|
||
this.$message.error('缺少流程ID,无法保存')
|
||
return
|
||
}
|
||
|
||
const newTitle = (this.editingProcessTitle || '').trim()
|
||
if (!newTitle) {
|
||
this.$message.warning('流程主题不能为空')
|
||
return
|
||
}
|
||
|
||
this.$confirm(`确认修改主题为${newTitle}吗?`, '提示', {
|
||
confirmButtonText: '确定',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
})
|
||
.then(() => {
|
||
const request = {
|
||
...api.EDIT_APPROVAL_PROCESS_TITLE,
|
||
params: {
|
||
flowId: this.detail.flowId,
|
||
title: newTitle
|
||
},
|
||
disableSuccessMsg: true
|
||
}
|
||
|
||
this.$request(request)
|
||
.asyncThen((resp) => {
|
||
this.showTopDetail.processTitle = newTitle
|
||
this.isEditingProcessTitle = false
|
||
this.editingProcessTitle = ''
|
||
this.$message({
|
||
message: '修改成功',
|
||
type: 'success'
|
||
})
|
||
// 重新加载详情
|
||
if (this.detail.flowId) {
|
||
this.getApprovalSpet(this.detail.flowId)
|
||
}
|
||
})
|
||
.asyncErrorCatch((err) => {
|
||
console.error('修改流程主题失败:', err)
|
||
this.$message({
|
||
message: err.message || '修改失败,请重试',
|
||
type: 'error'
|
||
})
|
||
})
|
||
})
|
||
.catch(() => {
|
||
// 取消操作
|
||
})
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.report-detail {
|
||
width: 100%;
|
||
height: 100%;
|
||
background: #fff;
|
||
display: flex;
|
||
justify-content: flex-start;
|
||
align-items: flex-start;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
overflow-y: auto;
|
||
|
||
.detail-panel {
|
||
width: 100%;
|
||
// max-width: 1200px;
|
||
background: #ffffff;
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.detail-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24px 32px 16px 32px;
|
||
border-bottom: 1px solid #ebeef5;
|
||
|
||
.header-left {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.back-button {
|
||
display: flex;
|
||
align-items: center;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
|
||
::v-deep .el-icon-arrow-left {
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.header-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
|
||
.title {
|
||
font-size: 20px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
}
|
||
|
||
.subtitle {
|
||
font-size: 14px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
|
||
.detail-content {
|
||
padding: 24px 32px 40px 32px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
overflow: auto;
|
||
gap: 24px;
|
||
|
||
&.has-footer {
|
||
/* 发起模式下为底部按钮预留空间 */
|
||
padding-bottom: 100px;
|
||
}
|
||
}
|
||
|
||
.block-section {
|
||
border-radius: 8px;
|
||
padding: 24px 24px 16px 24px;
|
||
|
||
.section-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 16px;
|
||
}
|
||
}
|
||
|
||
.info-grid {
|
||
// 基础样式
|
||
.label {
|
||
font-size: 16px;
|
||
color: #909399;
|
||
}
|
||
|
||
.value {
|
||
font-size: 16px;
|
||
color: #303133;
|
||
font-weight: 500;
|
||
}
|
||
|
||
// Flex 布局样式(用于基础信息部分:.info-item-full 和 .info-row)
|
||
.info-item-full {
|
||
display: flex;
|
||
gap: 16px 24px;
|
||
margin-bottom: 16px;
|
||
.label {
|
||
min-width: 100px;
|
||
}
|
||
.value {
|
||
flex: 1;
|
||
}
|
||
|
||
.process-title-value-wrapper {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
|
||
.process-title-edit-icon {
|
||
cursor: pointer;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
transition: color 0.3s;
|
||
flex-shrink: 0;
|
||
|
||
&:hover {
|
||
color: #409eff;
|
||
}
|
||
}
|
||
}
|
||
|
||
.process-title-input-wrapper {
|
||
flex: 1;
|
||
|
||
.process-title-input {
|
||
::v-deep .el-input__suffix {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding-right: 8px;
|
||
|
||
.process-title-icon {
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
transition: color 0.3s;
|
||
|
||
&.el-icon-check {
|
||
color: #67c23a;
|
||
|
||
&:hover {
|
||
color: #529b2e;
|
||
}
|
||
}
|
||
|
||
&.el-icon-close {
|
||
color: #909399;
|
||
|
||
&:hover {
|
||
color: #606266;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-row {
|
||
display: flex;
|
||
gap: 16px 24px;
|
||
margin-bottom: 16px;
|
||
|
||
.info-item {
|
||
flex: 1;
|
||
display: flex;
|
||
gap: 16px 24px;
|
||
.label {
|
||
min-width: 100px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 网格布局样式(用于报告信息部分:当 .info-grid 的第一个直接子元素是 .info-item 时)
|
||
// 通过检查第一个子元素是否为 .info-item(而不是 .info-item-full 或 .info-row)来判断
|
||
// 由于 CSS 限制,使用 :has() 选择器,如果不支持则通过 JavaScript 动态添加类名
|
||
.info-grid:not(:has(.info-item-full)):not(:has(.info-row)) {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
|
||
> .info-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
border-right: 1px solid #ebeef5;
|
||
border-bottom: 1px solid #ebeef5;
|
||
|
||
&:nth-child(2n) {
|
||
border-right: none;
|
||
}
|
||
|
||
&:nth-last-child(-n + 2) {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.label {
|
||
font-size: 16px;
|
||
color: #303133;
|
||
background: #f5f7fa;
|
||
padding: 12px 16px;
|
||
min-width: 140px;
|
||
width: 140px;
|
||
text-align: right;
|
||
border-right: 1px solid #ebeef5;
|
||
font-weight: 500;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.value {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
padding: 12px 16px;
|
||
flex: 1;
|
||
word-break: break-word;
|
||
}
|
||
}
|
||
}
|
||
|
||
.description-block {
|
||
display: flex;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
margin-top: 16px;
|
||
|
||
.label {
|
||
color: #909399;
|
||
}
|
||
|
||
.value {
|
||
flex: 1;
|
||
white-space: pre-line;
|
||
word-break: break-word;
|
||
}
|
||
}
|
||
|
||
.approval-timeline {
|
||
padding: 20px 0;
|
||
|
||
::v-deep .el-timeline {
|
||
padding-left: 0;
|
||
|
||
.el-timeline-item {
|
||
padding-bottom: 24px;
|
||
|
||
&:last-child {
|
||
padding-bottom: 0;
|
||
}
|
||
|
||
.el-timeline-item__node {
|
||
width: 12px;
|
||
height: 12px;
|
||
left: 0;
|
||
top: 4px;
|
||
border: 2px solid #fff;
|
||
box-shadow: 0 0 0 2px #409eff;
|
||
}
|
||
|
||
.el-timeline-item__wrapper {
|
||
top: 0;
|
||
padding-left: 28px;
|
||
|
||
.el-timeline-item__content {
|
||
padding-top: 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.timeline-item-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
|
||
.node-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.node-content {
|
||
background: #f4f7fc;
|
||
border-radius: 4px;
|
||
padding: 12px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.node-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
font-size: 13px;
|
||
color: #606266;
|
||
}
|
||
|
||
.node-user {
|
||
color: #303133;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.node-time {
|
||
color: #909399;
|
||
font-size: 12px;
|
||
margin-left: 8px;
|
||
}
|
||
|
||
.node-status {
|
||
display: inline-block;
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
border-radius: 2px;
|
||
width: fit-content;
|
||
|
||
&.status-initiate,
|
||
&.status-pass,
|
||
&.status-approve {
|
||
color: #67c23a;
|
||
background: transparent;
|
||
}
|
||
|
||
&.status-msg {
|
||
color: #409eff;
|
||
background: transparent;
|
||
}
|
||
|
||
&.status-reject {
|
||
color: #f56c6c;
|
||
background: transparent;
|
||
}
|
||
|
||
&.status-approving,
|
||
&.status-pending {
|
||
color: #e6a23c;
|
||
background: transparent;
|
||
}
|
||
|
||
&.status-default {
|
||
color: #909399;
|
||
background: transparent;
|
||
}
|
||
}
|
||
|
||
.node-opinion {
|
||
font-size: 13px;
|
||
color: #000;
|
||
line-height: 1.6;
|
||
}
|
||
}
|
||
|
||
.empty-progress {
|
||
text-align: center;
|
||
padding: 40px 20px;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
|
||
.empty-text {
|
||
display: inline-block;
|
||
}
|
||
}
|
||
}
|
||
|
||
.report-detail-fade-enter-active,
|
||
.report-detail-fade-leave-active {
|
||
transition: opacity 0.2s ease;
|
||
}
|
||
|
||
.report-detail-fade-enter,
|
||
.report-detail-fade-leave-to {
|
||
opacity: 0;
|
||
}
|
||
|
||
::v-deep .el-timeline-item__tail {
|
||
left: 7px;
|
||
top: 3px;
|
||
}
|
||
|
||
.dialog-body-wrapper {
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
padding-right: 10px;
|
||
}
|
||
|
||
.approval-workflow {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 16px;
|
||
margin-top: 20px;
|
||
|
||
.workflow-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.workflow-table {
|
||
width: 100%;
|
||
|
||
.workflow-table-content {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
border: 1px solid #ebeef5;
|
||
|
||
thead {
|
||
background: #f5f7fa;
|
||
|
||
th {
|
||
padding: 12px 16px;
|
||
text-align: left;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #303133;
|
||
border-bottom: 1px solid #ebeef5;
|
||
|
||
&.step-name-col {
|
||
width: 120px;
|
||
}
|
||
|
||
&.handler-col {
|
||
width: auto;
|
||
}
|
||
}
|
||
}
|
||
|
||
tbody {
|
||
.workflow-row {
|
||
&:not(:last-child) {
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.step-name-cell {
|
||
padding: 12px 16px;
|
||
vertical-align: middle;
|
||
|
||
.step-name {
|
||
font-size: 14px;
|
||
color: #303133;
|
||
|
||
.required-mark {
|
||
color: #f56c6c;
|
||
margin-left: 4px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.handler-cell {
|
||
padding: 12px 16px;
|
||
vertical-align: middle;
|
||
|
||
.handler-select {
|
||
width: 100%;
|
||
min-width: 200px;
|
||
|
||
::v-deep .el-input__inner {
|
||
height: 32px;
|
||
line-height: 32px;
|
||
}
|
||
|
||
::v-deep .el-tag {
|
||
margin: 2px 4px 2px 0;
|
||
}
|
||
}
|
||
|
||
.handler-display {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
|
||
.handler-tag {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
background: #f0f2f5;
|
||
border: 1px solid #e4e7ed;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
}
|
||
}
|
||
|
||
.handler-empty {
|
||
color: #909399;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.el-select-dropdown__item {
|
||
display: flex !important;
|
||
justify-content: space-between !important;
|
||
padding-right: 20px !important;
|
||
}
|
||
|
||
.username-text {
|
||
color: #303133 !important;
|
||
display: inline-block;
|
||
text-align: right !important;
|
||
}
|
||
|
||
.user-id-text {
|
||
color: #909399 !important;
|
||
display: inline-block;
|
||
margin-left: 20px;
|
||
text-align: right !important;
|
||
}
|
||
|
||
::v-deep .el-upload-dragger {
|
||
width: 100%;
|
||
height: 180px;
|
||
}
|
||
|
||
::v-deep .el-form-item {
|
||
margin-bottom: 18px;
|
||
}
|
||
|
||
.launch-section {
|
||
margin-top: 24px;
|
||
}
|
||
|
||
.launch-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.page-footer {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
align-items: center;
|
||
padding: 16px 32px;
|
||
background: #fff;
|
||
border-top: 1px solid #ebeef5;
|
||
flex-shrink: 0;
|
||
position: relative;
|
||
z-index: 10;
|
||
|
||
.footer-buttons {
|
||
display: flex;
|
||
gap: 12px;
|
||
}
|
||
}
|
||
|
||
.required-mark {
|
||
color: #f56c6c;
|
||
margin-left: 4px;
|
||
}
|
||
|
||
.return-node-dialog-content {
|
||
padding: 10px 0;
|
||
|
||
.empty-hint {
|
||
text-align: center;
|
||
padding: 40px 0;
|
||
color: #909399;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.return-node-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
margin-bottom: 16px;
|
||
|
||
.return-node-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 12px;
|
||
border: 1px solid #ebeef5;
|
||
border-radius: 4px;
|
||
transition: all 0.3s;
|
||
|
||
&:hover {
|
||
border-color: #409eff;
|
||
background: #ecf5ff;
|
||
}
|
||
|
||
.node-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin-left: 8px;
|
||
|
||
.node-title {
|
||
font-size: 14px;
|
||
font-weight: 500;
|
||
color: #303133;
|
||
}
|
||
|
||
.node-user {
|
||
font-size: 12px;
|
||
color: #909399;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.return-opinion-section {
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #ebeef5;
|
||
|
||
.opinion-label {
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
color: #606266;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
}
|
||
::v-deep .el-button {
|
||
line-height: 0.85 !important;
|
||
}
|
||
</style>
|