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.
 
 
 
 
 

1016 lines
23 KiB

<template>
<div class="tri-confirm-container">
<!-- 页面标题和统计 -->
<div class="page-header">
<div class="header-left">
<h2 class="page-title">
<i class="el-icon-s-check"></i> 三方确认
</h2>
<p class="page-subtitle">High Risk 生产质量技术三方确认管理</p>
</div>
<div class="header-right">
<div class="stat-cards">
<div class="stat-card stat-total">
<div class="stat-icon"><i class="el-icon-document"></i></div>
<div class="stat-content">
<div class="stat-value">{{ dataList.length }}</div>
<div class="stat-label">待确认工序</div>
</div>
</div>
<div class="stat-card stat-can-confirm">
<div class="stat-icon"><i class="el-icon-success"></i></div>
<div class="stat-content">
<div class="stat-value">{{ canConfirmCount }}</div>
<div class="stat-label">可立即确认</div>
</div>
</div>
<div class="stat-card stat-waiting">
<div class="stat-icon"><i class="el-icon-time"></i></div>
<div class="stat-content">
<div class="stat-value">{{ waitingCount }}</div>
<div class="stat-label">等待上道工序</div>
</div>
</div>
</div>
</div>
</div>
<!-- 查询条件表单 -->
<div class="search-section">
<el-collapse class="no-arrow" v-model="searchExpanded">
<el-collapse-item name="1">
<template slot="title">
<i class="el-icon-search"></i>
<span style="margin-left: 8px; font-weight: 500;">筛选条件</span>
</template>
<el-form :inline="true" label-position="top" class="search-form">
<el-form-item label="申请单号">
<el-input v-model="queryData.applyNo" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
</el-form-item>
<el-form-item label="试验类型">
<el-select v-model="queryData.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="queryData.title" placeholder="支持模糊查询" clearable style="width: 150px"></el-input>
</el-form-item>
<el-form-item label=" ">
<el-button @click="handleQuery" type="primary" icon="el-icon-search">查询</el-button>
<el-button @click="resetQuery" type="default" icon="el-icon-refresh-left">重置</el-button>
</el-form-item>
</el-form>
</el-collapse-item>
</el-collapse>
</div>
<!-- 卡片列表 -->
<div class="cards-container" v-loading="dataListLoading">
<transition-group name="card-list" tag="div" class="cards-grid">
<div
v-for="item in filteredDataList"
:key="item.applyNo + '-' + item.processStep"
class="confirm-card"
:class="{'high-risk': item.experimentType === 'High Risk', 'disabled': !item.canConfirm}">
<!-- 卡片头部 -->
<div class="card-header">
<div class="card-title-area">
<el-tag
:type="item.experimentType === 'High Risk' ? 'danger' : 'success'"
size="middle"
effect="dark"
class="risk-tag">
<i :class="item.experimentType === 'High Risk' ? 'el-icon-warning' : 'el-icon-success'"></i>
{{ item.experimentType }}
</el-tag>
<span class="apply-no">{{ item.applyNo }}</span>
</div>
<el-tag
:type="getRoleTagType(item.currentRole)"
size="middle"
effect="plain"
class="role-tag">
{{ getRoleName(item.currentRole) }}
</el-tag>
</div>
<!-- 卡片主体内容 -->
<div class="card-body">
<h3 class="experiment-title">
<i class="el-icon-document"></i>
{{ item.title }}
</h3>
<div class="process-info">
<div class="process-badge">
<i class="el-icon-s-operation"></i>
<span class="process-label">工序 {{ item.processSeq }}</span>
</div>
<div class="process-name">{{ item.processStep }}</div>
</div>
<div class="card-details">
<div class="detail-row">
<span class="detail-label">
<i class="el-icon-box"></i>
项目编号
</span>
<span class="detail-value">{{ item.projectNo || '-' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">
<i class="el-icon-goods"></i>
产品型号
</span>
<span class="detail-value" :title="item.productType">{{ item.productType || '-' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">
<i class="el-icon-user"></i>
申请人
</span>
<span class="detail-value">{{ item.creatorName }}</span>
</div>
</div>
<!-- 三方确认状态 -->
<div class="tri-status">
<div class="status-item" :class="{'confirmed': item.prodConfirmed}">
<i :class="item.prodConfirmed ? 'el-icon-circle-check' : 'el-icon-remove-outline'"></i>
<span>生产</span>
</div>
<div class="status-item" :class="{'confirmed': item.qaConfirmed}">
<i :class="item.qaConfirmed ? 'el-icon-circle-check' : 'el-icon-remove-outline'"></i>
<span>质量</span>
</div>
<div class="status-item" :class="{'confirmed': item.techConfirmed}">
<i :class="item.techConfirmed ? 'el-icon-circle-check' : 'el-icon-remove-outline'"></i>
<span>技术</span>
</div>
</div>
</div>
<!-- 卡片底部 -->
<div class="card-footer">
<div class="status-tip" v-if="!item.canConfirm">
<i class="el-icon-time"></i>
等待第{{ item.processSeq - 1 }}道工序完成
</div>
<div class="status-tip ready" v-else>
<i class="el-icon-success"></i>
可以确认
</div>
<div class="action-buttons">
<el-button
type="success"
size="small"
plain
icon="el-icon-check"
class="confirm-btn"
:disabled="!item.canConfirm"
@click="openConfirmDialog(item)">
确认
</el-button>
</div>
</div>
</div>
</transition-group>
<!-- 空状态 -->
<div v-if="!dataListLoading && filteredDataList.length === 0" class="empty-state">
<i class="el-icon-document-checked empty-icon"></i>
<p class="empty-text">暂无待确认工序</p>
<p class="empty-subtext">您已完成所有三方确认任务</p>
</div>
</div>
<!-- 确认弹窗 -->
<el-dialog
:title="'三方确认 - ' + getRoleName(confirmData.roleType)"
:visible.sync="confirmDialogVisible"
width="700px"
:close-on-click-modal="false"
v-drag>
<!-- 申请单和工序信息 -->
<div class="confirm-info">
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="申请单号">{{ confirmData.applyNo }}</el-descriptions-item>
<el-descriptions-item label="试验类型">
<el-tag :type="confirmData.experimentType === 'High Risk' ? 'danger' : 'success'" size="small">
{{ confirmData.experimentType }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="试验名称" :span="2">{{ confirmData.title }}</el-descriptions-item>
<el-descriptions-item label="工序顺序">第 {{ confirmData.processSeq }} 道</el-descriptions-item>
<el-descriptions-item label="工序名称">{{ confirmData.processStep }}</el-descriptions-item>
</el-descriptions>
</div>
<!-- 确认表单 -->
<el-divider content-position="left">确认信息</el-divider>
<el-form :model="confirmData" label-position="top" style="margin-left: 5px; margin-top: -5px;">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="确认角色">
<el-tag :type="getRoleTagType(confirmData.roleType)" size="medium">
{{ getRoleName(confirmData.roleType) }}
</el-tag>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="样品是否OK" required>
<el-radio-group v-model="confirmData.sampleOk">
<el-radio :label="true">OK</el-radio>
<el-radio :label="false">NG</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="确认意见" required>
<el-input
v-model="confirmData.comment"
type="textarea"
:rows="3"
placeholder="请输入确认意见"
maxlength="500"
show-word-limit>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
<el-footer style="height: 40px; margin-top: 60px; text-align: center">
<el-button type="success" @click="doConfirm" :loading="confirmLoading" icon="el-icon-check">
{{ confirmLoading ? '确认中...' : '确认提交' }}
</el-button>
<el-button type="primary" @click="confirmDialogVisible = false" :disabled="confirmLoading">关闭</el-button>
</el-footer>
</el-dialog>
</div>
</template>
<script>
import { getPendingTriConfirmList, confirmTriApproval } from '@/api/erf/erf'
export default {
name: 'TriConfirm',
data() {
return {
searchExpanded: ['0'], // 搜索条件默认展开
// 查询条件
queryData: {
applyNo: '',
experimentType: '',
title: ''
},
// 数据列表
dataList: [],
filteredDataList: [],
dataListLoading: false,
// 确认弹窗
confirmDialogVisible: false,
confirmData: {
applyNo: '',
processStep: '',
processSeq: null,
roleType: '',
sampleOk: true,
comment: '',
approverUserId: this.$store.state.user.id,
approverName: this.$store.state.user.name,
// 用于展示的信息
experimentType: '',
title: ''
},
confirmLoading: false
}
},
computed: {
/**
* 计算可立即确认的数量
*/
canConfirmCount() {
return this.filteredDataList.filter(item => item.canConfirm).length
},
/**
* 计算等待上道工序的数量
*/
waitingCount() {
return this.filteredDataList.filter(item => !item.canConfirm).length
}
},
activated() {
this.getDataList()
},
methods: {
/**
* 获取待确认列表
*/
getDataList() {
this.dataListLoading = true
const params = {
currentUserId: this.$store.state.user.id,
site: this.$store.state.user.site
}
getPendingTriConfirmList(params).then(({data}) => {
this.dataListLoading = false
if (data && data.code === 0) {
this.dataList = data.list || []
this.filterData() // 应用筛选条件
} else {
this.dataList = []
this.filteredDataList = []
this.$message.error(data.msg || '查询失败')
}
}).catch(error => {
this.dataListLoading = false
this.$message.error('查询异常')
})
},
/**
* 处理查询按钮点击(重新从后端获取数据)
*/
handleQuery() {
this.getDataList()
},
/**
* 筛选数据(前端筛选)
*/
filterData() {
let result = [...this.dataList]
// 申请单号筛选
if (this.queryData.applyNo) {
const keyword = this.queryData.applyNo.toLowerCase()
result = result.filter(item =>
item.applyNo && item.applyNo.toLowerCase().includes(keyword)
)
}
// 试验类型筛选
if (this.queryData.experimentType) {
result = result.filter(item => item.experimentType === this.queryData.experimentType)
}
// 试验名称筛选
if (this.queryData.title) {
const keyword = this.queryData.title.toLowerCase()
result = result.filter(item =>
item.title && item.title.toLowerCase().includes(keyword)
)
}
this.filteredDataList = result
},
/**
* 重置查询条件
*/
resetQuery() {
this.queryData.applyNo = ''
this.queryData.experimentType = ''
this.queryData.title = ''
this.getDataList()
},
/**
* 打开确认对话框
*/
openConfirmDialog(item) {
if (!item.canConfirm) {
this.$message.warning(`请等待第${item.processSeq - 1}道工序完成三方确认后再操作`)
return
}
this.confirmData = {
applyNo: item.applyNo,
processStep: item.processStep,
processSeq: item.processSeq,
roleType: item.currentRole,
sampleOk: true,
comment: '',
approverUserId: this.$store.state.user.id,
approverName: this.$store.state.user.name,
// 用于展示的信息
experimentType: item.experimentType,
title: item.title
}
this.confirmDialogVisible = true
},
/**
* 执行确认
*/
doConfirm() {
if ((!this.confirmData.comment || this.confirmData.comment.trim() === '') && !this.confirmData.sampleOk) {
this.$message.warning('请输入确认意见')
return
}
this.confirmLoading = true
confirmTriApproval(this.confirmData).then(({data}) => {
this.confirmLoading = false
if (data && data.code === 0) {
this.$message.success('确认成功')
this.confirmDialogVisible = false
this.getDataList() // 重新加载列表
} else {
this.$message.error(data.msg || '确认失败')
}
}).catch(error => {
this.confirmLoading = false
this.$message.error('确认异常')
})
},
/**
* 获取角色名称
*/
getRoleName(roleType) {
const names = {
'PROD': '生产负责人',
'QA': '质量负责人',
'TECH': '技术负责人'
}
return names[roleType] || roleType
},
/**
* 获取角色标签类型
*/
getRoleTagType(roleType) {
const types = {
'PROD': 'warning',
'QA': 'success',
'TECH': 'primary'
}
return types[roleType] || 'info'
}
}
}
</script>
<style scoped>
/* 整体容器 */
.tri-confirm-container {
padding: 15px;
background: #f5f7fa;
min-height: calc(100vh - 80px);
}
/* ===== 页面头部 ===== */
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 5px 20px;
background: #FFFFFF;
border-radius: 4px;
border: 1px solid #EBEEF5;
}
.header-left {
color: #303133;
}
.page-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: #303133;
display: flex;
align-items: center;
gap: 10px;
}
.page-title i {
font-size: 22px;
color: #409EFF;
}
.page-subtitle {
margin: 6px 0 0 0;
font-size: 13px;
color: #909399;
}
.header-right {
display: flex;
gap: 12px;
}
/* ===== 统计卡片 ===== */
.stat-cards {
display: flex;
gap: 12px;
}
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: #FFFFFF;
border-radius: 4px;
border: 1px solid #EBEEF5;
transition: all 0.3s ease;
cursor: pointer;
min-width: 140px;
}
.stat-card:hover {
border-color: #409EFF;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.15);
}
.stat-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 20px;
color: white;
}
.stat-total .stat-icon {
background: #409EFF;
}
.stat-can-confirm .stat-icon {
background: #67C23A;
}
.stat-waiting .stat-icon {
background: #E6A23C;
}
.stat-content {
display: flex;
flex-direction: column;
}
.stat-value {
font-size: 24px;
font-weight: 600;
color: #303133;
line-height: 1;
}
.stat-label {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
/* ===== 搜索区域 ===== */
.search-section {
margin-bottom: 15px;
background: white;
border-radius: 4px;
border: 1px solid #EBEEF5;
overflow: hidden;
}
.search-section >>> .el-collapse-item__header {
padding: 0 15px;
height: 45px;
line-height: 45px;
background: white;
border-bottom: 1px solid #ebeef5;
font-size: 13px;
color: #303133;
font-weight: 500;
}
.search-section >>> .el-collapse-item__content {
padding: 15px;
background: #FFFFFF;
}
.search-form {
margin: 0;
}
.search-form >>> .el-form-item {
margin-bottom: 10px;
}
/* ===== 卡片容器 ===== */
.cards-container {
min-height: 400px;
position: relative;
}
.cards-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 15px;
margin-bottom: 15px;
}
/* ===== 确认卡片 ===== */
.confirm-card {
background: white;
border-radius: 4px;
padding: 12px;
border: 1px solid #EBEEF5;
transition: all 0.3s ease;
cursor: pointer;
position: relative;
}
.confirm-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 1px;
height: 100%;
background: #67C23A;
transition: all 0.3s ease;
border-radius: 4px 0 0 4px;
}
.confirm-card.high-risk::before {
background: #F56C6C;
}
.confirm-card.disabled {
opacity: 0.7;
cursor: not-allowed;
}
.confirm-card.disabled::before {
background: #909399;
}
.confirm-card:hover:not(.disabled) {
border-color: #409EFF;
box-shadow: 0 4px 12px rgba(64, 158, 255, 0.15);
}
/* 卡片头部 */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
padding-bottom: 6px;
border-bottom: 1px solid #EBEEF5;
}
.card-title-area {
display: flex;
align-items: center;
gap: 10px;
}
.risk-tag {
font-weight: 500;
padding: 0px 12px;
font-size: 13px;
}
.risk-tag i {
margin-right: 4px;
}
.apply-no {
font-size: 13px;
color: #606266;
font-family: 'Courier New', monospace;
font-weight: 500;
}
.role-tag {
font-weight: 500;
padding: 0px 12px;
font-size: 13px;
}
/* 卡片主体 */
.card-body {
margin-bottom: 4px;
}
.experiment-title {
font-size: 15px;
font-weight: 600;
color: #303133;
margin: 0 0 4px 0;
line-height: 1.5;
display: flex;
align-items: flex-start;
gap: 6px;
}
.experiment-title i {
color: #409EFF;
margin-top: 2px;
font-size: 16px;
}
.process-info {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 6px;
padding: 10px;
border-radius: 4px;
background: #F5F7FA;
//border-radius: 3px;
transition: all 0.2s ease;
}
.process-badge {
display: flex;
align-items: center;
gap: 6px;
font-size: 14px;
font-weight: 600;
}
.process-badge i {
font-size: 16px;
}
.process-name {
flex: 1;
font-size: 14px;
font-weight: 500;
}
.card-details {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 6px;
}
.detail-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
padding: 6px 10px;
background: #F5F7FA;
border-radius: 3px;
transition: all 0.2s ease;
}
.detail-row:hover {
background: #ECF5FF;
}
.detail-label {
color: #909399;
display: flex;
align-items: center;
gap: 5px;
font-weight: 500;
}
.detail-label i {
color: #409EFF;
font-size: 14px;
}
.detail-value {
color: #606266;
font-weight: 500;
text-align: right;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 三方确认状态 */
.tri-status {
display: flex;
justify-content: space-around;
padding: 5px;
background: #F5F7FA;
border-radius: 4px;
}
.status-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
color: #909399;
font-size: 13px;
transition: all 0.3s ease;
}
.status-item i {
font-size: 24px;
}
.status-item.confirmed {
color: #67C23A;
}
.status-item.confirmed i {
animation: checkBounce 0.6s ease;
}
@keyframes checkBounce {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
/* 卡片底部 */
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 6px;
border-top: 1px solid #EBEEF5;
}
.status-tip {
font-size: 12px;
color: #E6A23C;
display: flex;
align-items: center;
gap: 5px;
font-weight: 500;
}
.status-tip i {
font-size: 14px;
}
.status-tip.ready {
color: #67C23A;
}
.action-buttons {
display: flex;
gap: 8px;
}
.confirm-btn {
background: #f0f9ff;
border-color: #b3e19d;
color: #67C23A;
font-weight: 500;
padding: 7px 20px;
font-size: 13px;
transition: all 0.3s ease;
}
.confirm-btn:hover:not(:disabled) {
background: #67C23A;
border-color: #67C23A;
color: #FFFFFF;
}
.confirm-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* ===== 空状态 ===== */
.empty-state {
text-align: center;
padding: 80px 20px;
background: white;
border-radius: 4px;
border: 1px solid #EBEEF5;
}
.empty-icon {
font-size: 64px;
color: #c0c4cc;
margin-bottom: 12px;
}
.empty-text {
font-size: 15px;
color: #606266;
margin: 0 0 6px 0;
font-weight: 500;
}
.empty-subtext {
font-size: 13px;
color: #909399;
margin: 0;
}
/* ===== 卡片动画 ===== */
.card-list-enter-active {
animation: cardFadeIn 0.4s ease;
}
.card-list-leave-active {
animation: cardFadeOut 0.3s ease;
}
@keyframes cardFadeIn {
from {
opacity: 0;
transform: translateY(15px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes cardFadeOut {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.95);
}
}
/* ===== 弹窗样式 ===== */
.confirm-info {
margin-bottom: 20px;
}
.confirm-info >>> .el-descriptions {
font-size: 13px;
}
/* ===== 响应式设计 ===== */
@media screen and (max-width: 1600px) {
.cards-grid {
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
}
}
@media screen and (max-width: 1200px) {
.cards-grid {
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
}
.page-header {
flex-direction: column;
gap: 15px;
align-items: flex-start;
}
.stat-cards {
width: 100%;
overflow-x: auto;
}
}
@media screen and (max-width: 768px) {
.cards-grid {
grid-template-columns: 1fr;
}
.stat-cards {
flex-direction: column;
}
.stat-card {
width: 100%;
}
}
/deep/ .no-arrow .el-collapse-item__header .el-collapse-item__arrow {
display: none !important;
}
</style>