2026-02-24 08:50:46 +08:00

2905 lines
92 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>