You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

1749 lines
48 KiB

<template>
<div class="mod-config exp-apply-page">
<!-- 查询条件表单 -->
<el-form :inline="true" label-position="top" class="query-form">
<el-form-item label="申请单号">
<el-input v-model="queryHeaderData.applyNo" placeholder="请输入申请单号" clearable style="width: 150px"></el-input>
</el-form-item>
<el-form-item label="事业部">
<el-select v-model="queryHeaderData.buNo" placeholder="请选择" clearable style="width: 120px">
<el-option label="全部" value=""></el-option>
<el-option
v-for="i in buList"
:key="i.buNo"
:label="i.buDesc"
:value="i.buNo">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="试验类型">
<el-select v-model="queryHeaderData.experimentType" placeholder="请选择" clearable style="width: 120px">
<el-option label="全部" value=""></el-option>
<el-option label="High Risk" value="High Risk"></el-option>
<el-option label="Low Risk" value="Low Risk"></el-option>
</el-select>
</el-form-item>
<el-form-item label="试验名称">
<el-input v-model="queryHeaderData.title" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
</el-form-item>
<el-form-item label="项目编号">
<el-input v-model="queryHeaderData.projectNo" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
</el-form-item>
<el-form-item label="产品型号">
<el-input v-model="queryHeaderData.productType" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
</el-form-item>
<el-form-item label="状态">
<el-select v-model="queryHeaderData.status" placeholder="请选择" clearable style="width: 120px">
<el-option label="全部" value=""></el-option>
<el-option label="草稿" value="草稿"></el-option>
<el-option label="已下达" value="已下达"></el-option>
<el-option label="已批准" value="已批准"></el-option>
<el-option label="生产中" value="生产中"></el-option>
<el-option label="样品确认" value="样品确认"></el-option>
<el-option label="已完成" value="已完成"></el-option>
</el-select>
</el-form-item>
<el-form-item label="创建日期">
<el-date-picker
v-model="queryHeaderData.createStartDate"
type="date"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
placeholder="开始日期"
style="width: 140px">
</el-date-picker>
</el-form-item>
<el-form-item label="至">
<el-date-picker
v-model="queryHeaderData.createEndDate"
type="date"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
placeholder="结束日期"
style="width: 140px">
</el-date-picker>
</el-form-item>
<el-form-item label=" " style="margin-top: -11px">
<el-button @click="getDataList('Y')" type="primary" plain class="search-btn">查询</el-button>
<el-button @click="resetQuery()" plain class="reset-btn">重置</el-button>
<el-button @click="openCreateDialog()" type="success" plain class="add-btn">新增申请单</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table
ref="dataTable"
@row-click="handleRowClick"
highlight-current-row
:data="dataList"
v-loading="dataListLoading"
border
class="data-table"
style="width: 100%;"
:height="tableHeight">
<el-table-column
label="操作"
width="150"
align="center"
header-align="center">
<template slot-scope="scope">
<a
@click="editApply(scope.row)">修改</a>
<a
v-if="scope.row.status === '草稿' || scope.row.status === '已驳回'"
@click="submitApply(scope.row)">{{ scope.row.status === '已驳回' ? '重新下达' : '下达' }}</a>
<a
v-if="scope.row.status === '草稿'"
@click="deleteApply(scope.row)">删除</a>
<a
v-if="scope.row.status !== '草稿' && scope.row.status !== '已完成' && scope.row.status !== '已驳回'"
@click="withdrawApply(scope.row)">撤回</a>
<a
v-if="showSampleConfirmBtn(scope.row)"
@click="openSampleConfirm(scope.row)">样品确认</a>
</template>
</el-table-column>
<el-table-column
prop="buDesc"
label="事业部"
width="70"
align="center"
header-align="center">
</el-table-column>
<el-table-column
prop="applyNo"
label="申请单号"
width="100"
align="center"
header-align="center"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="status"
label="状态"
width="80"
align="center"
header-align="center">
</el-table-column>
<!-- <el-table-column
prop="currentStep"
label="当前步骤"
width="90"
align="center"
header-align="center">
</el-table-column>-->
<el-table-column
prop="experimentType"
label="试验类型"
width="100"
align="center" show-overflow-tooltip
header-align="center">
</el-table-column>
<el-table-column
prop="title"
label="试验名称"
min-width="120"
align="left"
header-align="center"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="projectNo"
label="项目编号"
width="100"
align="center"
header-align="center"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="productType"
label="产品型号"
min-width="220"
align="center"
header-align="center"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="expectedFinishDate"
label="期望完成日期"
width="120"
align="center"
header-align="center">
</el-table-column>
<el-table-column
prop="actualFinishDate"
label="最终完成日期"
width="120"
align="center"
header-align="center">
</el-table-column>
<el-table-column
prop="finalQuantity"
label="入库数量"
width="80"
align="center"
header-align="center">
</el-table-column>
<el-table-column
prop="creatorName"
label="创建人"
width="80"
align="center"
header-align="center">
</el-table-column>
<el-table-column
prop="createTime"
label="创建时间"
min-width="100"
align="center" show-overflow-tooltip
header-align="center">
</el-table-column>
</el-table>
<!-- 分页组件 -->
<el-pagination
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
:current-page="pageIndex"
:page-sizes="[20, 50, 100]"
:page-size="pageSize"
:total="totalPage"
layout="total, sizes, prev, pager, next, jumper"
style="margin-top: 10px; text-align: right;">
</el-pagination>
<!-- 下方Tab区域 -->
<el-tabs class="customer-tab"
v-model="activeName"
type="border-card"
style="margin-top: 10px; min-height: 280px;"
@tab-click="handleTabClick">
<!-- Tab 1: 项目详情 -->
<!-- <el-tab-pane :label="getProjectDetailLabel()" name="projectDetail">
<exp-project-detail
v-if="currentRow.applyNo"
ref="projectDetail"
:apply-no="currentRow.applyNo"
:status="currentRow.status"
:height="detailHeight"
@refresh="handleDetailRefresh">
</exp-project-detail>
<div v-else class="empty-tip">
请在上方表格中选择一条申请单记录
</div>
</el-tab-pane>-->
<el-tab-pane label="附件上传" name="attachment">
<erf-attachment-manager
v-if="currentRow.applyNo"
ref="attachmentManager"
:apply-no="currentRow.applyNo"
:disabled="false"
:height="detailHeight">
</erf-attachment-manager>
<div v-else class="empty-tip">
请在上方表格中选择一条申请单记录
</div>
</el-tab-pane>
<!-- Tab 2: 三方确认 -->
<el-tab-pane label="三方确认" v-if="currentRow.experimentType==='High Risk'" name="triConfirm">
<exp-tri-confirm
v-if="currentRow.applyNo&&currentRow.experimentType==='High Risk'"
ref="triConfirm"
:apply-no="currentRow.applyNo"
:experiment-type="currentRow.experimentType"
:height="detailHeight">
</exp-tri-confirm>
<div v-else class="empty-tip">
请在上方表格中选择一条申请单记录
</div>
</el-tab-pane>
<!-- Tab 3: 附件上传 -->
<!-- Tab 4: 审批状态和日志 -->
<el-tab-pane label="审批状态和日志" name="approvalStatus">
<div v-if="currentRow.applyNo" :style="{height: detailHeight + 'px', overflowY: 'auto', padding: '0px 10px'}">
<!-- 两栏布局:审批流程 | 审批日志 -->
<div class="two-column-layout">
<!-- 左栏:审批流程 -->
<div class="stages-column">
<div class="column-header">
<i class="el-icon-s-order"></i>
<span>审批流程</span>
<span class="progress-badge">{{ flowStatus.progressPercent || 0 }}%</span>
</div>
<div class="stages-list">
<div
v-for="(stage, index) in flowStatus.stages"
:key="index"
class="stage-item"
:class="'stage-' + stage.status">
<div class="stage-icon">
<i :class="getStageIcon(stage.status)"></i>
</div>
<div class="stage-content">
<div class="stage-name">{{ stage.nodeName }}</div>
<div class="stage-meta">
<el-tag :type="getStageTagType(stage.status)" size="mini" effect="plain">
{{ getStageStatusText(stage.status) }}
</el-tag>
<span v-if="stage.completeTime" class="stage-time">{{ stage.completeTime }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 右栏:审批日志(表格) -->
<div class="logs-column">
<div class="column-header">
<i class="el-icon-tickets"></i>
<span>审批日志</span>
<span class="logs-count">{{ flowStatus.approvalLogs ? flowStatus.approvalLogs.length : 0 }}条</span>
</div>
<div class="logs-table-wrapper">
<el-table
:data="flowStatus.approvalLogs"
size="small"
class="approval-logs-table"
style="width: 100%"
height="34vh">
<el-table-column prop="logTime" label="时间" width="150" align="center">
<template slot-scope="scope">
<span style="font-size: 12px">{{ formatDateTime(scope.row.logTime) }}</span>
</template>
</el-table-column>
<el-table-column prop="action" label="操作" width="90" align="center">
<template slot-scope="scope">
<el-tag :type="getActionTagType(scope.row.action)" size="mini">
{{ scope.row.action }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="nodeCode" label="节点" width="150" align="center">
<template slot-scope="scope">
<span style="font-size: 12px">{{ scope.row.nodeCode }}</span>
</template>
</el-table-column>
<el-table-column prop="operatorName" label="操作人" width="100" align="center">
<template slot-scope="scope">
<span style="font-size: 12px">{{ scope.row.operatorName }}</span>
</template>
</el-table-column>
<el-table-column prop="comment" label="备注" min-width="180" show-overflow-tooltip>
<template slot-scope="scope">
<span style="font-size: 12px; color: #606266">{{ scope.row.comment || '-' }}</span>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
</div>
<div v-else class="empty-tip">
<i class="el-icon-document" style="font-size: 40px; color: #C0C4CC; margin-bottom: 10px"></i>
<p style="font-size: 13px">请选择申请单查看审批状态</p>
</div>
</el-tab-pane>
</el-tabs>
<!-- 新增/编辑弹窗 -->
<el-dialog
:title="dialogTitle"
:visible.sync="dialogVisible"
width="950px"
:close-on-click-modal="false"
v-drag>
<exp-apply-form
v-if="dialogVisible"
ref="applyForm"
:apply-data="currentApply"
:readonly="dialogReadonly">
</exp-apply-form>
<el-footer style="height: 40px; margin-top: 40px; text-align: center">
<el-button type="primary" @click="saveApply" v-if="!dialogReadonly" :loading="saveLoading">
{{ saveLoading ? '保存中...' : '保存' }}
</el-button>
<el-button type="primary" @click="dialogVisible = false">关闭</el-button>
</el-footer>
</el-dialog>
<!-- 样品确认弹窗 -->
<el-dialog
title="样品确认"
:visible.sync="sampleConfirmVisible"
width="320px"
:close-on-click-modal="false">
<div style="margin-top: 1px;margin-left: 10px; color: #909399; font-size: 12px">
不输入或输入0表示样品报废,大于0表示正常入库
</div>
<el-form :model="sampleConfirmData" label-width="100px" size="small">
<el-form-item label="申请单号">
<el-tag type="primary">{{ sampleConfirmData.applyNo }}</el-tag>
</el-form-item>
<el-form-item label="样品数量" required>
<el-input
v-model="sampleConfirmData.sampleQuantity"
:min="0"
:precision="0"
placeholder="请输入样品数量"
style="width: 80%">
</el-input>
</el-form-item>
<el-form-item label="完成日期" required>
<el-date-picker
v-model="sampleConfirmData.finalFinishDate"
type="date"
format="yyyy-MM-dd"
value-format="yyyy-MM-dd"
placeholder="请选择最终完成日期"
style="width: 80%">
</el-date-picker>
</el-form-item>
<el-form-item label="样品状态">
<el-tag :type="getSampleStatusTagType()">
{{ getSampleStatusText() }}
</el-tag>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button type="primary" @click="confirmSampleSubmit" :loading="sampleConfirmLoading">
{{ sampleConfirmLoading ? '确认中...' : '确认' }}
</el-button>
<el-button @click="sampleConfirmVisible = false">关闭</el-button>
</div>
</el-dialog>
<!-- 下达确认弹窗 - 选择审批人 -->
<el-dialog
title="确认审批人信息"
:visible.sync="submitDialogVisible"
width="500px"
:close-on-click-modal="false">
<el-form :model="submitData" label-width="80px" size="small">
<!-- 技术经理 -->
<el-form-item label="技术经理">
<el-tag type="success" size="medium">
{{ submitData.techManagerName }}
</el-tag>
<span style="margin-left: 10px; color: #909399; font-size: 12px">
(根据发起人角色自动分配)
</span>
</el-form-item>
<!-- 生产经理 -->
<el-form-item label="生产经理" required>
<el-select class="manager-select"
v-model="submitData.prodManagerIds"
multiple
placeholder="请选择生产经理"
style="width: 100%">
<el-option
v-for="manager in prodManagerList"
:key="manager.userId"
:label="manager.userDisplay || manager.username"
:value="manager.userId">
</el-option>
</el-select>
<div style="margin-top: 1px; color: #909399; font-size: 12px">
可多选,所有选中的生产经理都必须审批通过后才会流转到计划员排产
</div>
</el-form-item>
<!-- 质量经理 -->
<el-form-item label="质量经理" required>
<el-select class="manager-select"
v-model="submitData.qualityManagerIds"
multiple
placeholder="请选择质量经理"
style="width: 100%">
<el-option
v-for="manager in qualityManagerList"
:key="manager.userId"
:label="manager.userDisplay || manager.username"
:value="manager.userId">
</el-option>
</el-select>
<div style="margin-top: 1px; color: #909399; font-size: 12px">
可多选,所有选中的质量经理都必须审批通过后才会流转到计划员排产
</div>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="submitDialogVisible = false">取消</el-button>
<el-button type="primary" @click="confirmSubmit" :loading="submitLoading">
{{ submitLoading ? '下达中...' : '确认下达' }}
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import { searchExpApplyList, submitExpApply, deleteExpApply, withdrawExpApply, getSubmitApprovers, getFlowStatus, getTriConfirmList, confirmSample } from '@/api/erf/erf'
import { getBuList } from '@/api/factory/site'
import ExpApplyForm from './components/expApplyForm.vue'
import ExpProjectDetail from './components/expProjectDetail.vue'
import ExpTriConfirm from './components/expTriConfirm.vue'
import ErfAttachmentManager from './components/erfAttachmentManager.vue'
export default {
name: 'ExpApplyList',
components: {
ExpApplyForm,
ExpProjectDetail,
ExpTriConfirm,
ErfAttachmentManager
},
data() {
return {
buList: [],
// 查询条件
queryHeaderData: {
applyNo: '',
buNo: '',
experimentType: '',
status: '',
createStartDate: '',
createEndDate: '',
page: 1,
limit: 20
},
// 数据列表
dataList: [],
// 分页参数
pageIndex: 1,
pageSize: 20,
totalPage: 0,
dataListLoading: false,
// 弹窗相关
dialogVisible: false,
dialogTitle: '新增申请单',
dialogReadonly: false,
currentApply: {},
saveLoading: false,
// 当前选中行
currentRow: {},
// Tab页签
activeName: 'attachment',
// 表格高度
tableHeight: (window.innerHeight - 260)/2,
detailHeight: '35vh',
// 下达确认弹窗
submitDialogVisible: false,
submitLoading: false,
submitData: {
applyNo: '',
buNo: '',
techManagerId: null,
techManagerName: '',
prodManagerIds: [],
qualityManagerIds: []
},
prodManagerList: [], // 生产经理候选列表
qualityManagerList: [], // 质量经理候选列表
// 流程状态数据
flowStatus: {
applyNo: '',
currentStatus: '',
currentStep: '',
progressPercent: 0,
stages: [],
approvalLogs: []
},
// 样品确认弹窗
sampleConfirmVisible: false,
sampleConfirmLoading: false,
sampleConfirmData: {
applyNo: '',
sampleQuantity: null,
finalFinishDate: ''
}
}
},
activated() {
this.loadBuList()
this.getDataList()
},
methods: {
/**
* 加载事业部列表
*/
loadBuList() {
const tempData = { site: this.$store.state.user.site }
getBuList(tempData).then(({data}) => {
if (data.code === 0) {
this.buList = data.row1
}
})
},
/**
* 获取申请单列表
*/
getDataList(flag) {
if (flag === 'Y') {
this.pageIndex = 1
}
this.queryHeaderData.page = this.pageIndex
this.queryHeaderData.limit = this.pageSize
this.dataListLoading = true
searchExpApplyList(this.queryHeaderData).then(({data}) => {
this.dataListLoading = false
if (data && data.code === 0) {
this.dataList = data.page.list || []
this.totalPage = data.page.totalCount || 0
// 如果有数据,处理行选中
if (this.dataList.length > 0) {
// 尝试找到之前选中的行
if (this.currentRow.applyNo) {
const selectedRow = this.dataList.find(item => item.applyNo === this.currentRow.applyNo)
if (selectedRow) {
// 如果找到之前选中的行,重新高亮
this.$nextTick(() => {
this.handleRowClick(selectedRow)
})
} else {
// 如果没找到,选中第一行
this.handleRowClick(this.dataList[0])
}
} else {
// 如果之前没有选中任何行,默认选中第一行
this.handleRowClick(this.dataList[0])
}
} else {
// 没有数据,清空当前行
this.currentRow = {}
}
} else {
this.dataList = []
this.totalPage = 0
this.currentRow = {}
this.$message.error(data.msg || '查询失败')
}
}).catch(error => {
this.dataListLoading = false
this.$message.error('查询异常')
})
},
/**
* 重置查询条件
*/
resetQuery() {
this.queryHeaderData = {
applyNo: '',
buNo: '',
experimentType: '',
status: '',
createStartDate: '',
createEndDate: '',
page: 1,
limit: 20
}
this.getDataList('Y')
},
/**
* 打开新增对话框
*/
openCreateDialog() {
this.dialogTitle = '新增申请单'
this.dialogReadonly = false
this.currentApply = {}
this.dialogVisible = true
},
/**
* 查看详情
*/
viewDetail(row) {
this.dialogTitle = '查看申请单'
this.dialogReadonly = true
this.currentApply = { ...row }
this.dialogVisible = true
},
/**
* 编辑申请单
*/
editApply(row) {
this.dialogTitle = '修改申请单'
this.dialogReadonly = false
this.currentApply = { ...row }
this.dialogVisible = true
},
/**
* 保存申请单
*/
saveApply() {
const formData = this.$refs.applyForm.getFormData()
if (!formData) {
return
}
this.saveLoading = true
this.$refs.applyForm.save().then(() => {
this.saveLoading = false
this.$message.success('保存成功')
this.dialogVisible = false
this.getDataList()
}).catch(error => {
this.saveLoading = false
})
},
/**
* 下达申请单 - 打开审批人确认弹窗
*/
submitApply(row) {
// ✅ 如果是High Risk,先验证工序
if (row.experimentType === 'High Risk') {
console.log('🔍 检测到High Risk申请单,开始验证工序...')
// 通过API查询工序列表进行验证(不依赖子组件是否已加载)
this.validateHighRiskProcessByApi(row.applyNo).then(isValid => {
if (!isValid) {
console.log('❌ High Risk验证失败,终止下达')
this.$alert('High Risk申请单必须至少有一条完整的工序(包含车间、质量、技术三个负责人)才能下达', '操作提示', {
confirmButtonText: '确定',
type: 'warning'
})
return
}
console.log('✅ High Risk验证通过,继续下达流程')
// 验证通过后继续下达流程
this.proceedSubmit(row)
}).catch(error => {
console.error('❌ 验证工序异常:', error)
this.$message.error('验证工序失败,请重试')
})
} else {
// 非High Risk直接下达
console.log('📝 Low Risk申请单,直接下达')
this.proceedSubmit(row)
}
},
/**
* 通过API验证High Risk工序
*
* @param {String} applyNo 申请单号
* @return {Promise<Boolean>} 验证结果
*/
validateHighRiskProcessByApi(applyNo) {
return new Promise((resolve, reject) => {
// 调用API查询工序列表
getTriConfirmList({ applyNo: applyNo }).then(({data}) => {
if (data && data.code === 0) {
const processList = data.list || []
console.log('📊 查询到工序数量:', processList.length)
// 检查是否至少有一条完整的工序
const completeProcesses = processList.filter(process => {
const isComplete = process.prodApproverName &&
process.qaApproverName &&
process.techApproverName
if (isComplete) {
console.log('✅ 完整工序:', process.processStep, {
车间: process.prodApproverName,
质量: process.qaApproverName,
技术: process.techApproverName
})
}
return isComplete
})
console.log(`📈 完整工序数量: ${completeProcesses.length}/${processList.length}`)
// 至少需要一条完整的工序
resolve(completeProcesses.length > 0)
} else {
reject(new Error(data.msg || '查询工序列表失败'))
}
}).catch(error => {
reject(error)
})
})
},
/**
* 执行下达流程
*/
proceedSubmit(row) {
// 一次性获取所有审批人信息
getSubmitApprovers({
userId: this.$store.state.user.id,
buNo: row.buNo
}).then(({data}) => {
if (data && data.code === 0) {
// 设置生产经理和质量经理列表
this.prodManagerList = data.prodManagers || []
this.qualityManagerList = data.qualityManagers || []
// 根据事业部设置默认生产经理(使用user_display模糊匹配)
const defaultProdManagerIds = []
if (row.buDesc === 'RFID') {
// RFID默认Tony
const tony = this.prodManagerList.find(m =>
m.userDisplay && m.userDisplay.toLowerCase().includes('tony')
)
if (tony) defaultProdManagerIds.push(tony.userId)
} else if (row.buDesc === 'RF') {
// RF默认Charles
const charles = this.prodManagerList.find(m =>
m.userDisplay && m.userDisplay.toLowerCase().includes('charles')
)
if (charles) defaultProdManagerIds.push(charles.userId)
}
// 根据事业部设置默认质量经理(使用user_display模糊匹配)
const defaultQualityManagerIds = []
if (row.buDesc === 'RFID') {
// RFID默认Victor
const victor = this.qualityManagerList.find(m =>
m.userDisplay && m.userDisplay.toLowerCase().includes('victor')
)
if (victor) defaultQualityManagerIds.push(victor.userId)
} else if (row.buDesc === 'RF') {
// RF默认尹君
const yin = this.qualityManagerList.find(m =>
m.userDisplay && (m.userDisplay.includes('尹君') || m.userDisplay.toLowerCase().includes('yin'))
)
if (yin) defaultQualityManagerIds.push(yin.userId)
}
// 设置表单数据
this.submitData = {
applyNo: row.applyNo,
buNo: row.buNo,
techManagerId: data.techManager.managerId,
techManagerName: data.techManager.managerName,
prodManagerIds: defaultProdManagerIds,
qualityManagerIds: defaultQualityManagerIds
}
// 显示弹窗
this.submitDialogVisible = true
} else {
this.$message.error(data.msg || '获取审批人信息失败')
}
}).catch(() => {
this.$message.error('获取审批人信息异常')
})
},
/**
* 确认下达
*/
confirmSubmit() {
// 验证必填项
if (!this.submitData.techManagerId) {
this.$message.warning('技术经理未分配')
return
}
if (!this.submitData.prodManagerIds || this.submitData.prodManagerIds.length === 0) {
this.$message.warning('请至少选择一位生产经理')
return
}
if (!this.submitData.qualityManagerIds || this.submitData.qualityManagerIds.length === 0) {
this.$message.warning('请至少选择一位质量经理')
return
}
this.submitLoading = true
// 调用下达API
submitExpApply({
applyNo: this.submitData.applyNo,
techManagerId: this.submitData.techManagerId,
prodManagerIds: this.submitData.prodManagerIds,
qualityManagerIds: this.submitData.qualityManagerIds
}).then(({data}) => {
this.submitLoading = false
if (data && data.code === 0) {
this.$message.success('下达成功,已通知相关审批人')
this.submitDialogVisible = false
this.getDataList()
// 触发全局审批通知检查(通知审批人)
this.triggerApprovalNotification()
} else {
this.$message.error(data.msg || '下达失败')
}
}).catch(() => {
this.submitLoading = false
})
},
/**
* 触发全局审批通知检查
* 在申请单下达成功后调用,立即通知审批人
*/
triggerApprovalNotification() {
try {
// 通过 $root 访问 App.vue 的方法
if (this.$root && this.$root.$children && this.$root.$children[0]) {
const app = this.$root.$children[0]
if (app.checkApprovalNotifications) {
// 延迟1秒后检查,确保后端数据已更新
setTimeout(() => {
//app.checkApprovalNotifications()
console.log('[审批通知] 已触发手动检查')
}, 1000)
}
}
} catch (error) {
console.log('[审批通知] 触发通知检查失败:', error.message)
}
},
/**
* 删除申请单
*/
deleteApply(row) {
this.$confirm('确定删除该申请单?', '操作提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
deleteExpApply({ applyNo: row.applyNo }).then(({data}) => {
if (data && data.code === 0) {
this.$message.success('删除成功')
this.getDataList()
} else {
this.$message.error(data.msg || '删除失败')
}
})
})
},
/**
* 撤回申请单
*/
withdrawApply(row) {
this.$confirm('确定撤回该申请单?撤回后将变为草稿状态', '操作提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
withdrawExpApply({
applyNo: row.applyNo,
currentUserId: this.$store.state.user.id
}).then(({data}) => {
if (data && data.code === 0) {
this.$message.success('撤回成功')
this.getDataList()
} else {
this.$message.error(data.msg || '撤回失败')
}
})
})
},
/**
* 获取状态类型
*/
getStatusType(status) {
const types = {
'草稿': 'info',
'已下达': 'warning',
'已批准': 'success',
'生产中': '',
'已完成': 'success',
'已取消': 'danger',
'已驳回': 'danger'
}
return types[status] || 'info'
},
/**
* 获取状态文本(直接返回中文状态)
*/
getStatusText(status) {
return status || ''
},
/**
* 分页大小改变
*/
sizeChangeHandle(val) {
this.pageSize = val
this.pageIndex = 1
this.getDataList()
},
/**
* 当前页改变
*/
currentChangeHandle(val) {
this.pageIndex = val
this.getDataList()
},
/**
* 行点击事件
*/
handleRowClick(row) {
this.currentRow = JSON.parse(JSON.stringify(row))
// 设置表格当前行高亮
this.$nextTick(() => {
this.$refs.dataTable.setCurrentRow(row)
})
// 根据当前tab刷新对应的数据
if (this.activeName === 'approvalStatus') {
// 刷新审批状态和日志
this.loadFlowStatus()
} else if (this.activeName === 'triConfirm') {
// 刷新三方确认数据
this.$nextTick(() => {
if (this.$refs.triConfirm && this.$refs.triConfirm.loadProcessList) {
console.log('🔄 行切换,刷新三方确认数据')
this.$refs.triConfirm.loadProcessList()
}
})
} else {
// 否则自动切换到项目详情tab
this.activeName = 'attachment'
}
},
/**
* Tab切换事件
*/
handleTabClick(tab) {
// Tab切换时可以做一些特殊处理
console.log('当前Tab:', tab.name)
// 切换到审批状态tab时加载流程状态
if (tab.name === 'approvalStatus' && this.currentRow.applyNo) {
this.loadFlowStatus()
}
// 切换到三方确认tab时刷新数据
if (tab.name === 'triConfirm' && this.currentRow.applyNo) {
this.$nextTick(() => {
if (this.$refs.triConfirm && this.$refs.triConfirm.loadProcessList) {
console.log('🔄 Tab切换,刷新三方确认数据')
this.$refs.triConfirm.loadProcessList()
}
})
}
},
/**
* 项目详情刷新事件
*/
handleDetailRefresh() {
// 刷新当前行数据
this.getDataList()
},
/**
* 获取项目详情Tab标签
*/
getProjectDetailLabel() {
if (this.currentRow.applyNo) {
return `项目详情 [${this.currentRow.applyNo}]`
}
return '项目详情'
},
/**
* 加载流程状态
*/
loadFlowStatus() {
if (!this.currentRow.applyNo) {
return
}
getFlowStatus({ applyNo: this.currentRow.applyNo }).then(({data}) => {
if (data && data.code === 0) {
this.flowStatus = data.data || {
stages: [],
approvalLogs: []
}
}
})
},
/**
* 获取阶段颜色
*/
getStageColor(status) {
const colors = {
'pending': '#C0C4CC',
'current': '#409EFF',
'completed': '#67C23A',
'rejected': '#F56C6C'
}
return colors[status] || '#C0C4CC'
},
/**
* 获取阶段图标
*/
getStageIcon(status) {
const icons = {
'pending': 'el-icon-time',
'current': 'el-icon-loading',
'completed': 'el-icon-success',
'rejected': 'el-icon-error'
}
return icons[status] || 'el-icon-time'
},
/**
* 获取阶段标签类型
*/
getStageTagType(status) {
const types = {
'pending': 'info',
'current': '',
'completed': 'success',
'rejected': 'danger'
}
return types[status] || 'info'
},
/**
* 获取阶段状态文本
*/
getStageStatusText(status) {
const texts = {
'pending': '未开始',
'current': '进行中',
'completed': '已完成',
'rejected': '已驳回'
}
return texts[status] || '未知'
},
/**
* 获取操作类型标签
*/
getActionTagType(action) {
const types = {
'下达': 'primary',
'批准': 'success',
'确认': 'success',
'驳回': 'danger',
'撤回': 'warning',
'提醒': 'info',
'已排产': 'success',
'录入最终结果': 'success'
}
return types[action] || 'info'
},
/**
* 格式化日期时间
*/
formatDateTime(dateTime) {
if (!dateTime) {
return ''
}
if (typeof dateTime === 'string') {
return dateTime
}
const date = new Date(dateTime)
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
const seconds = String(date.getSeconds()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
},
/**
* 判断是否显示样品确认按钮
* Low Risk: 计划员排产完成后(currentStep="已完成"且status="生产中")显示
* High Risk: 三方确认阶段(currentStep="三方确认"且status="生产中")显示
*/
showSampleConfirmBtn(row) {
if (row.experimentType === 'Low Risk') {
return row.currentStep === '样品确认'
} else if (row.experimentType === 'High Risk') {
return row.currentStep === '样品确认'
}
return false
},
/**
* 打开样品确认对话框
*/
openSampleConfirm(row) {
this.sampleConfirmData = {
applyNo: row.applyNo,
sampleQuantity: null,
finalFinishDate: ''
}
this.sampleConfirmVisible = true
},
/**
* 获取样品状态标签类型
*/
getSampleStatusTagType() {
if (!this.sampleConfirmData.sampleQuantity || this.sampleConfirmData.sampleQuantity === 0) {
return 'danger'
}
return 'success'
},
/**
* 获取样品状态文本
*/
getSampleStatusText() {
if (!this.sampleConfirmData.sampleQuantity || this.sampleConfirmData.sampleQuantity === 0) {
return '报废'
}
return '正常入库'
},
/**
* 提交样品确认
*/
confirmSampleSubmit() {
// 验证最终完成日期必填
if (!this.sampleConfirmData.finalFinishDate) {
this.$message.warning('请选择最终完成日期')
return
}
// 确定样品状态
const finalStatus = (!this.sampleConfirmData.sampleQuantity || this.sampleConfirmData.sampleQuantity === 0)
? '报废'
: '正常入库'
const finalQuantity = this.sampleConfirmData.sampleQuantity || 0
this.sampleConfirmLoading = true
confirmSample({
applyNo: this.sampleConfirmData.applyNo,
finalQuantity: finalQuantity,
finalStatus: finalStatus,
actualFinishDate: this.sampleConfirmData.finalFinishDate
}).then(({data}) => {
this.sampleConfirmLoading = false
if (data && data.code === 0) {
this.$message.success('样品确认成功')
this.sampleConfirmVisible = false
this.getDataList()
} else {
this.$message.error(data.msg || '样品确认失败')
}
}).catch(() => {
this.sampleConfirmLoading = false
this.$message.error('样品确认异常')
})
}
}
}
</script>
<style scoped>
.mod-config {
}
.el-form {
margin-bottom: 10px;
}
.dialog-footer {
text-align: center;
}
.empty-tip {
padding: 40px;
text-align: center;
color: #999;
font-size: 14px;
}
/* ==================== 页面专属样式 - 不影响全局 ==================== */
/* 查询表单样式 */
.exp-apply-page .query-form {
background-color: #FFFFFF;
padding: 15px 15px 5px 15px;
border-radius: 4px;
}
.exp-apply-page .query-form >>> .el-form-item__label {
color: #333333;
font-size: 13px;
padding-bottom: 5px;
}
.exp-apply-page .query-form >>> .el-input__inner {
height: 32px;
line-height: 32px;
border-radius: 4px;
border: 1px solid #DCDFE6;
font-size: 13px;
}
.exp-apply-page .query-form >>> .el-input__inner::placeholder {
color: #C0C4CC;
font-size: 13px;
}
.exp-apply-page .query-form >>> .el-select .el-input__inner {
border-radius: 4px;
}
.exp-apply-page .query-form >>> .el-date-editor .el-input__inner {
border-radius: 4px;
}
/* 按钮样式 - 扁平化风格 */
.exp-apply-page >>> .el-button {
height: 32px;
padding: 0 15px;
font-size: 13px;
border-radius: 4px;
}
/* 查询按钮 - 蓝色扁平 */
.exp-apply-page .search-btn {
background-color: #ECF5FF;
border-color: #B3D8FF;
color: #409EFF;
}
.exp-apply-page .search-btn:hover {
background-color: #409EFF;
border-color: #409EFF;
color: #FFFFFF;
}
/* 重置按钮 - 灰色扁平 */
.exp-apply-page .reset-btn {
background-color: #F5F7FA;
border-color: #D3D4D6;
color: #606266;
}
.exp-apply-page .reset-btn:hover {
background-color: #909399;
border-color: #909399;
color: #FFFFFF;
}
/* 新增按钮 - 绿色扁平 */
.exp-apply-page .add-btn {
background-color: #F0F9FF;
border-color: #C0E6C7;
color: #67C23A;
}
.exp-apply-page .add-btn:hover {
background-color: #67C23A;
border-color: #67C23A;
color: #FFFFFF;
}
/* 数据表格样式 */
.exp-apply-page .data-table {
background-color: #FFFFFF;
border-radius: 4px;
}
.exp-apply-page .data-table >>> .el-table__header-wrapper th {
background-color: #F5F7FA !important;
color: #333333;
font-weight: 600;
font-size: 16px;
border-color: #EBEEF5;
padding: 10px 0;
height: auto;
}
/* 固定列表头也使用相同背景色 */
.exp-apply-page .data-table >>> .el-table__fixed-header-wrapper th {
background-color: #F5F7FA !important;
color: #333333;
font-weight: 600;
font-size: 16px;
border-color: #EBEEF5;
padding: 12px 0;
height: auto;
}
.exp-apply-page .data-table >>> .el-table__header-wrapper .cell {
text-align: center;
padding: 0 10px;
//line-height: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px !important;
}
.exp-apply-page .data-table >>> .el-table__fixed-header-wrapper .cell {
text-align: center;
padding: 0 10px;
//line-height: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.exp-apply-page .data-table >>> .el-table__body-wrapper td {
border-color: #EBEEF5;
padding: 12px 0;
font-size: 16px;
color: #606266;
height: auto;
}
/* 固定列数据行也使用相同样式 */
.exp-apply-page .data-table >>> .el-table__fixed-body-wrapper td {
border-color: #EBEEF5;
padding: 12px 0;
font-size: 16px;
color: #606266;
height: auto;
}
.exp-apply-page .data-table >>> .el-table__body-wrapper .cell {
padding: 0 10px;
//line-height: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px !important;
}
.exp-apply-page .data-table >>> .el-table__fixed-body-wrapper .cell {
padding: 0 10px;
//line-height: 2;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 操作列链接样式 - 统一蓝色样式 */
.exp-apply-page .data-table >>> .el-table__body-wrapper a {
text-decoration: none;
cursor: pointer;
font-size: 13px;
color: #409EFF !important;
white-space: nowrap;
}
.exp-apply-page .data-table >>> .el-table__body-wrapper a:hover {
color: #66B1FF !important;
text-decoration: underline;
}
.exp-apply-page .data-table >>> .el-table__body tr:hover > td {
background-color: #F5F7FA !important;
}
.exp-apply-page .data-table >>> .el-table__body tr.current-row > td {
background-color: #ECF5FF !important;
}
/* Tab样式优化 - 两栏布局 */
/* 两栏布局容器 */
.two-column-layout {
display: flex;
gap: 10px;
height: 100%;
}
/* 统一的列头样式 */
.column-header {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: #606266;
padding: 8px 12px;
background: #f5f7fa;
border-bottom: 2px solid #409EFF;
}
.column-header i {
color: #409EFF;
font-size: 14px;
}
.progress-badge {
margin-left: auto;
font-size: 14px;
color: #409EFF;
font-weight: bold;
}
/* ==================== 左栏:审批流程 ==================== */
.stages-column {
flex: 0 0 380px;
background: white;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.stages-list {
padding: 8px;
overflow-y: auto;
flex: 1;
}
.stage-item {
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px;
margin-bottom: 6px;
border-radius: 6px;
background: #fafafa;
transition: all 0.2s;
}
.stage-item:last-child {
margin-bottom: 0;
}
.stage-item:hover {
background: #f0f2f5;
transform: translateX(2px);
}
.stage-icon {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
margin-top: 2px;
}
.stage-pending .stage-icon {
background: #e8e8e8;
color: #909399;
}
.stage-current .stage-icon {
background: #409EFF;
color: white;
animation: pulse-stage 2s infinite;
}
@keyframes pulse-stage {
0%, 100% {
box-shadow: 0 0 0 3px rgba(64, 158, 255, 0.3);
}
50% {
box-shadow: 0 0 0 5px rgba(64, 158, 255, 0.1);
}
}
.stage-completed .stage-icon {
background: #67C23A;
color: white;
}
.stage-rejected .stage-icon {
background: #F56C6C;
color: white;
}
.stage-content {
flex: 1;
min-width: 0;
}
.stage-name {
font-size: 13px;
font-weight: 600;
color: #303133;
margin-bottom: 5px;
}
.stage-meta {
display: flex;
align-items: center;
gap: 6px;
}
.stage-time {
font-size: 11px;
color: #909399;
}
.stage-current {
background: #ecf5ff !important;
border: 1px solid #b3d8ff;
}
.stage-completed {
background: #f0f9ff !important;
}
.stage-rejected {
background: #fef0f0 !important;
border: 1px solid #fbc4c4;
}
/* ==================== 右栏:审批日志(表格) ==================== */
.logs-column {
flex: 1;
background: white;
border: 1px solid #e4e7ed;
border-radius: 6px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.logs-column .column-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-shrink: 0;
}
.logs-count {
font-size: 11px;
color: #909399;
font-weight: normal;
margin-left: auto;
}
.logs-table-wrapper {
flex: 1;
overflow: hidden;
}
/* ==================== 审批日志表格专用样式 ==================== */
/* 表头样式 - 深灰色背景,白色文字 */
.approval-logs-table >>> .el-table__header-wrapper th,
.approval-logs-table >>> .el-table__header-wrapper .el-table__cell {
background-color: #F5F7FA !important;
color: #606266 !important;
font-size: 12px;
font-weight: 600;
}
/* 数据行样式 - 更高的行高防止遮挡 */
.approval-logs-table >>> .el-table__body-wrapper .el-table__row {
height: 40px !important;
}
.approval-logs-table >>> .el-table__body-wrapper td,
.approval-logs-table >>> .el-table__body-wrapper .el-table__cell {
height: 40px !important;
padding: 0 !important;
}
/* 关键修复:cell容器要有足够的padding和overflow可见 */
.approval-logs-table >>> .el-table__body-wrapper .cell {
padding: 1px 12px !important;
line-height: 24px !important;
overflow: visible !important;
height: auto !important;
}
/* 标签样式 - 确保不被遮挡 */
.approval-logs-table >>> .el-tag {
vertical-align: middle !important;
display: inline-block !important;
margin: 2px 0 !important;
}
/* 悬停效果 */
.approval-logs-table >>> .el-table__body tr:hover > td {
background-color: #f5f7fa !important;
}
/* 空状态 */
.empty-tip {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 50px 20px;
color: #909399;
}
.empty-tip p {
margin: 0;
}
/deep/ .manager-select .el-input__inner {
height: 30px !important;
line-height: 30px !important;
}
</style>