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