feat: 新增质量评审流程全量功能

- 新增质量评审相关的实体、Mapper、Service、Controller接口与实现
- 新增前端页面与API接口,支持评审单增删改查、提交送审、审批驳回、改判执行
- 新增数据库初始化脚本与字典数据
- 修复vue.config热加载监视系统文件导致的EBUSY错误
- 清理HRM模块API导出注释
This commit is contained in:
王文昊
2026-06-30 17:58:21 +08:00
parent c997a4a1a0
commit 450fca0c45
25 changed files with 2816 additions and 1 deletions

View File

@@ -1,4 +1,3 @@
// 导出所有 HRM 模块 API
export * from './employee'
export * from './org'
export * from './certContract'

View File

@@ -0,0 +1,94 @@
import request from '@/utils/request'
// 查询质量评审单列表
export function listQualityReview(query) {
return request({
url: '/qc/qualityReview/list',
method: 'get',
params: query
})
}
// 查询质量评审单详细
export function getQualityReview(reviewId) {
return request({
url: '/qc/qualityReview/' + reviewId,
method: 'get'
})
}
// 新增质量评审单
export function addQualityReview(data) {
return request({
url: '/qc/qualityReview',
method: 'post',
data: data
})
}
// 修改质量评审单
export function updateQualityReview(data) {
return request({
url: '/qc/qualityReview',
method: 'put',
data: data
})
}
// 删除质量评审单
export function delQualityReview(reviewIds) {
return request({
url: '/qc/qualityReview/' + reviewIds,
method: 'delete'
})
}
// 提交送审
export function submitQualityReview(reviewId) {
return request({
url: '/qc/qualityReview/submit/' + reviewId,
method: 'post'
})
}
// 审批通过
export function approveQualityReview(data) {
return request({
url: '/qc/qualityReview/approve',
method: 'post',
data: data
})
}
// 驳回
export function rejectQualityReview(reviewId, reason) {
return request({
url: '/qc/qualityReview/reject/' + reviewId,
method: 'post',
params: { reason }
})
}
// 执行改判
export function executeQualityReview(reviewId) {
return request({
url: '/qc/qualityReview/execute/' + reviewId,
method: 'post'
})
}
// 查询钢卷明细
export function listQualityReviewCoil(reviewId) {
return request({
url: '/qc/qualityReview/coilList/' + reviewId,
method: 'get'
})
}
// 查询审批日志
export function listQualityReviewLog(reviewId) {
return request({
url: '/qc/qualityReview/logList/' + reviewId,
method: 'get'
})
}

View File

@@ -0,0 +1,815 @@
<template>
<div class="app-container quality-review-container">
<div class="left-panel">
<div class="panel-header">
<div class="header-title">
<i class="el-icon-document-checked"></i>
<span>质量评审单</span>
<el-button size="mini" type="text" icon="el-icon-refresh" @click="getList" style="margin-left:4px;" title="刷新列表"></el-button>
</div>
<el-select v-model="queryParams.flowStatus" placeholder="全部状态" clearable size="mini" @change="handleQuery" class="header-filter">
<el-option v-for="item in dict.type.quality_review_status" :key="item.value" :label="item.label" :value="parseInt(item.value)" />
</el-select>
</div>
<div class="search-row">
<el-input v-model="queryParams.reviewNo" placeholder="搜索评审单编号..." clearable prefix-icon="el-icon-search" size="small" @keyup.enter.native="handleQuery" @clear="handleQuery" />
<el-button type="primary" size="small" @click="handleAdd" v-hasPermi="['qc:qualityReview:add']">
<i class="el-icon-plus"></i>
</el-button>
</div>
<div v-loading="loading" class="list-body">
<div v-for="item in dataList" :key="item.reviewId" class="list-item"
:class="{ active: currentRow && currentRow.reviewId === item.reviewId }" @click="handleRowClick(item)">
<div class="item-main">
<span class="item-title">{{ item.reviewNo }}</span>
<span class="item-sub">{{ item.transmitUser }} · {{ item.productName }}</span>
</div>
<div class="item-meta">
<el-tag v-if="item.flowStatus === 1" type="info" size="mini">待提交</el-tag>
<el-tag v-else-if="item.flowStatus === 2" type="warning" size="mini">待审批</el-tag>
<el-tag v-else-if="item.flowStatus === 3" type="success" size="mini">已通过</el-tag>
<el-tag v-else-if="item.flowStatus === 4" type="danger" size="mini">已驳回</el-tag>
</div>
<div class="item-actions">
<el-button size="mini" type="text" icon="el-icon-edit" @click.stop="handleUpdate(item)" v-hasPermi="['qc:qualityReview:edit']"></el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click.stop="handleDelete(item)" v-hasPermi="['qc:qualityReview:delete']"></el-button>
</div>
</div>
<div v-if="dataList.length === 0 && !loading" class="list-empty">
<i class="el-icon-folder-opened"></i>
<span>暂无质量评审单</span>
</div>
</div>
<div class="list-footer">
<pagination :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
</div>
</div>
<!-- 右侧详情 -->
<div class="right-panel">
<div v-if="!currentRow" class="empty-tip">
<i class="el-icon-info"></i>
<span>请在左侧列表中选择一条评审单查看详情</span>
</div>
<div v-else v-loading="detailLoading" class="detail-content">
<!-- ===== 顶部操作栏 ===== -->
<div class="detail-header">
<div class="detail-header-left">
<span class="detail-title">{{ currentRow.reviewNo }}</span>
<el-tag v-if="currentRow.flowStatus === 1" type="info" size="small">待提交</el-tag>
<el-tag v-else-if="currentRow.flowStatus === 2" type="warning" size="small">待审批</el-tag>
<el-tag v-else-if="currentRow.flowStatus === 3" type="success" size="small">已通过</el-tag>
<el-tag v-else-if="currentRow.flowStatus === 4" type="danger" size="small">已驳回</el-tag>
</div>
<div class="detail-header-actions">
<!-- 待提交提交送审 -->
<el-button v-if="currentRow.flowStatus === 1" size="mini" type="primary" plain icon="el-icon-s-promotion"
@click="handleSubmit" v-hasPermi="['qc:qualityReview:submit']">提交送审</el-button>
<!-- 待审批审批/驳回 -->
<el-button v-if="currentRow.flowStatus === 2" size="mini" type="success" plain icon="el-icon-check"
@click="handleApprove" v-hasPermi="['qc:qualityReview:approve']">审批通过</el-button>
<el-button v-if="currentRow.flowStatus === 2" size="mini" type="danger" plain icon="el-icon-close"
@click="handleReject" v-hasPermi="['qc:qualityReview:approve']">驳回</el-button>
<!-- 已通过执行改判 -->
<el-button v-if="currentRow.flowStatus === 3" size="mini" type="warning" plain icon="el-icon-setting"
@click="handleExecute" v-hasPermi="['qc:qualityReview:execute']">执行改判</el-button>
<!-- 已驳回重新提交 -->
<el-button v-if="currentRow.flowStatus === 4" size="mini" type="primary" plain icon="el-icon-s-promotion"
@click="handleReSubmit" v-hasPermi="['qc:qualityReview:submit']">重新提交</el-button>
<!-- 通用 -->
<el-button size="mini" type="text" icon="el-icon-refresh" @click="handleRefreshDetail">刷新</el-button>
<el-button v-if="currentRow.flowStatus === 1 || currentRow.flowStatus === 4" size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(currentRow)">编辑</el-button>
<el-button v-if="currentRow.flowStatus === 1 || currentRow.flowStatus === 4" size="mini" type="text" icon="el-icon-delete" @click="handleDelete(currentRow)">删除</el-button>
</div>
</div>
<!-- ===== 基本信息 ===== -->
<el-card class="detail-section" shadow="never">
<div slot="header"><span><i class="el-icon-info"></i> 基本信息</span></div>
<el-form :model="currentRow" label-width="100px" size="small" class="basic-form">
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="评审单编号">{{ currentRow.reviewNo }}</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="产品名称">{{ currentRow.productName }}</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="传递部门">{{ currentRow.transmitDept }}</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="传递人">{{ currentRow.transmitUser }}</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="传递日期">{{ currentRow.transmitDate }}</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="生产日期">{{ currentRow.prodDateRange }}</el-form-item>
</el-col>
</el-row>
</el-form>
</el-card>
<!-- ===== 钢卷明细表详情只读 ===== -->
<el-card class="detail-section" shadow="never">
<div slot="header">
<span><i class="el-icon-s-management"></i> 问题钢卷明细</span>
</div>
<el-table :data="coilList" border style="width: 100%" size="small" max-height="400">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="currentCoilNo" label="产品卷号" min-width="140" />
<el-table-column prop="supplierCoilNo" label="原料卷号" min-width="140" />
<el-table-column prop="spec" label="规格(mm)" width="110" />
<el-table-column prop="netWeight" label="卷重(t)" width="90" align="right" />
<el-table-column prop="defectDesc" label="缺陷描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="beforeQuality" label="改判前" width="80" align="center">
<template slot-scope="scope">
<el-tag size="mini" type="danger">{{ scope.row.beforeQuality }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="regradeQuality" label="改判后" width="110" align="center">
<template slot-scope="scope">
<el-tag v-if="scope.row.regradeQuality" size="mini" type="warning">{{ scope.row.regradeQuality }}</el-tag>
<span v-else class="text-muted">待指定</span>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- ===== 品质部评审意见 ===== -->
<el-card class="detail-section" shadow="never">
<div slot="header"><span><i class="el-icon-edit-outline"></i> 品质部评审意见</span></div>
<div v-if="editing" class="section-editor">
<el-input type="textarea" :rows="3" v-model="editForm.deptOpinion" placeholder="请填写品质部评审意见..." />
<div style="margin-top:8px; display:flex; gap:8px;">
<el-input v-model="editForm.deptSign" placeholder="签字人" style="width:160px;" size="small" />
<el-date-picker v-model="editForm.deptSignDate" type="date" placeholder="签字日期" size="small" />
</div>
<el-button size="small" type="primary" @click="saveOpinion" style="margin-top:8px;">保存意见</el-button>
</div>
<div v-else class="section-display">
<div class="opinion-text">{{ currentRow.deptOpinion || '暂未填写' }}</div>
<div class="opinion-footer" v-if="currentRow.deptOpinion">
<span>签字{{ currentRow.deptSign || '-' }}</span>
<span>日期{{ currentRow.deptSignDate || '-' }}</span>
</div>
</div>
</el-card>
<!-- ===== 领导审批意见 ===== -->
<el-card class="detail-section" shadow="never">
<div slot="header"><span><i class="el-icon-s-check"></i> 领导审批意见</span></div>
<!-- flowStatus=2时显示审批表单权限由后端API控制前端不做限制 -->
<div v-if="currentRow.flowStatus === 2" class="section-editor">
<!-- 审批通过模式 -->
<el-radio-group v-model="approveAction" style="margin-bottom:12px;">
<el-radio-button label="approve">通过</el-radio-button>
<el-radio-button label="reject">驳回</el-radio-button>
</el-radio-group>
<template v-if="approveAction === 'approve'">
<el-input type="textarea" :rows="3" v-model="approveForm.leaderOpinion" placeholder="请输入领导审批意见..." />
<div style="margin:8px 0;">
<el-input v-model="approveForm.leaderSign" placeholder="签字人" style="width:160px;" size="small" />
</div>
<div class="approve-coil-section">
<div class="approve-coil-title">请为每个钢卷指定改判后质量状态</div>
<div v-for="coil in coilList" :key="coil.detailId" class="approve-coil-row">
<span class="approve-coil-label">{{ coil.currentCoilNo }}{{ coil.spec }}</span>
<el-select v-model="approveCoilMap[coil.detailId]" placeholder="请选择改判后状态" size="small" style="width:180px;">
<el-option v-for="item in regradeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
</div>
<el-button size="small" type="success" @click="doApprove">确认通过</el-button>
</template>
<template v-else>
<el-input type="textarea" :rows="3" v-model="rejectReason" placeholder="请输入驳回原因..." />
<el-button size="small" type="danger" @click="doReject" style="margin-top:8px;">确认驳回</el-button>
</template>
</div>
<div v-else class="section-display">
<div class="opinion-text">{{ currentRow.leaderOpinion || '暂无审批意见' }}</div>
<div class="opinion-footer" v-if="currentRow.leaderOpinion">
<span>签字{{ currentRow.leaderSign || '-' }}</span>
<span>日期{{ currentRow.leaderSignDate || '-' }}</span>
</div>
<div v-if="currentRow.flowStatus === 4 && currentRow.rejectReason" class="reject-reason">
<el-alert :title="'驳回原因:' + currentRow.rejectReason" type="error" show-icon :closable="false" />
</div>
</div>
</el-card>
<!-- ===== 审批日志 ===== -->
<el-card class="detail-section" shadow="never">
<div slot="header"><span><i class="el-icon-time"></i> 审批记录</span></div>
<div v-if="logList.length === 0" class="text-muted" style="padding:12px;">暂无审批记录</div>
<el-timeline v-else>
<el-timeline-item v-for="log in logList" :key="log.logId"
:timestamp="log.createTime"
:type="log.action === 'submit' ? 'primary' : (log.action === 'approve' ? 'success' : 'danger')"
:icon="log.action === 'submit' ? 'el-icon-s-promotion' : (log.action === 'approve' ? 'el-icon-check' : 'el-icon-close')">
<div>
<strong>{{ log.action === 'submit' ? '提交送审' : (log.action === 'approve' ? '审批通过' : '驳回') }}</strong>
<span v-if="log.operator"> {{ log.operator }}</span>
</div>
<div v-if="log.opinion" class="timeline-opinion">{{ log.opinion }}</div>
</el-timeline-item>
</el-timeline>
</el-card>
</div>
</div>
<!-- 编辑弹窗 -->
<el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="900px" :close-on-click-modal="false" append-to-body>
<el-form ref="form" :model="editForm" :rules="rules" label-width="100px" size="small">
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="产品名称" prop="productName">
<el-input v-model="editForm.productName" placeholder="如:镀锌产品" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="传递部门" prop="transmitDept">
<el-input v-model="editForm.transmitDept" placeholder="品质部" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="传递人" prop="transmitUser">
<el-input v-model="editForm.transmitUser" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="传递日期" prop="transmitDate">
<el-date-picker v-model="editForm.transmitDate" type="date" placeholder="选择日期" style="width:100%;" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="生产日期" prop="prodDateRange">
<el-input v-model="editForm.prodDateRange" placeholder="如14-15" />
</el-form-item>
</el-col>
</el-row>
<!-- ===== 钢卷选择 ===== -->
<div class="dialog-section">
<div class="dialog-section-header">
<span>问题钢卷</span>
<el-button size="mini" type="primary" plain icon="el-icon-plus" @click="showCoilSelector = true">选择O级钢卷</el-button>
</div>
<el-table :data="editForm.coilList" border style="width:100%" size="small" max-height="250">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="currentCoilNo" label="产品卷号" min-width="130" />
<el-table-column prop="supplierCoilNo" label="原料卷号" min-width="130" />
<el-table-column prop="spec" label="规格(mm)" width="100" />
<el-table-column prop="netWeight" label="卷重(t)" width="80" align="right" />
<el-table-column prop="defectDesc" label="缺陷描述" min-width="160">
<template slot-scope="scope">
<el-input v-model="scope.row.defectDesc" size="mini" placeholder="输入缺陷描述" />
</template>
</el-table-column>
<el-table-column label="操作" width="60" align="center">
<template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-delete" @click="editForm.coilList.splice(scope.$index, 1)"></el-button>
</template>
</el-table-column>
</el-table>
<div v-if="editForm.coilList.length === 0" class="dialog-table-empty">请点击上方按钮选择O级钢卷</div>
</div>
<!-- ===== 钢卷选择器嵌套在弹窗内 ===== -->
<CoilSelector
:visible.sync="showCoilSelector"
:multiple="true"
:filters="{ qualityStatusCsv: 'O', status: 0 }"
@confirm="onDialogCoilConfirm" />
<el-divider />
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="品质部意见" prop="deptOpinion">
<el-input type="textarea" :rows="3" v-model="editForm.deptOpinion" placeholder="请填写品质部评审意见" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="签字人">
<el-input v-model="editForm.deptSign" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="签字日期">
<el-date-picker v-model="editForm.deptSignDate" type="date" placeholder="选择日期" style="width:100%;" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<span slot="footer">
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="saveForm">确定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
import { listQualityReview, getQualityReview, addQualityReview, updateQualityReview, delQualityReview,
submitQualityReview, approveQualityReview, rejectQualityReview, executeQualityReview,
listQualityReviewCoil, listQualityReviewLog } from '@/api/mes/qc/qualityReview'
import CoilSelector from '@/components/CoilSelector'
export default {
name: 'QualityReview',
components: { CoilSelector },
dicts: ['quality_review_status', 'regrade_quality_type'],
data() {
return {
// 列表
loading: false,
dataList: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 20,
reviewNo: undefined,
flowStatus: undefined
},
currentRow: null,
// 详情
detailLoading: false,
coilList: [],
logList: [],
// 钢卷选择
showCoilSelector: false,
// 编辑弹窗
dialogVisible: false,
dialogTitle: '',
editing: false,
editForm: {
reviewId: undefined,
productName: '',
transmitDept: '品质部',
transmitUser: '',
transmitDate: undefined,
prodDateRange: '',
deptOpinion: '',
deptSign: '',
deptSignDate: undefined,
coilList: []
},
rules: {
productName: [{ required: true, message: '请输入产品名称', trigger: 'blur' }]
},
// 改判状态选项(硬编码后备,优先使用字典)
regradeOptions: [
{ label: '协议销售', value: 'protocol_sale' },
{ label: '转分剪', value: 'to_slitting' },
{ label: '降级', value: 'downgrade' },
{ label: '返修', value: 'rework' },
{ label: '报废', value: 'scrap' }
],
// 审批
approveAction: 'approve',
approveForm: {
reviewId: undefined,
leaderOpinion: '',
leaderSign: ''
},
approveCoilMap: {},
rejectReason: ''
}
},
created() {
this.getList()
},
methods: {
// ===== 列表 =====
handleQuery() {
this.queryParams.pageNum = 1
this.getList()
},
getList() {
this.loading = true
listQualityReview(this.queryParams).then(res => {
this.dataList = res.rows || []
this.total = res.total || 0
this.loading = false
}).catch(() => { this.loading = false })
},
handleRowClick(row) {
this.currentRow = row
this.loadDetail(row.reviewId)
},
// ===== 详情 =====
loadDetail(reviewId) {
this.detailLoading = true
getQualityReview(reviewId).then(res => {
const data = res.data
this.currentRow = data
this.coilList = data.coilList || []
this.logList = data.logList || []
// 初始化审批表单数据先构建完整对象再赋值解决Vue2响应式问题
const map = {}
this.coilList.forEach(c => {
map[c.detailId] = c.regradeQuality || ''
})
this.approveCoilMap = map
this.approveForm.reviewId = reviewId
this.approveForm.leaderOpinion = ''
this.approveForm.leaderSign = ''
this.rejectReason = ''
this.detailLoading = false
}).catch(() => { this.detailLoading = false })
},
handleRefreshDetail() {
if (this.currentRow) {
this.loadDetail(this.currentRow.reviewId)
this.getList()
}
},
// ===== 新增/编辑 =====
handleAdd() {
this.dialogTitle = '新建质量评审单'
this.editForm = {
reviewId: undefined,
productName: '',
transmitDept: '品质部',
transmitUser: '',
transmitDate: undefined,
prodDateRange: '',
deptOpinion: '',
deptSign: '',
deptSignDate: undefined,
coilList: []
}
this.editing = true
this.dialogVisible = true
},
handleUpdate(row) {
this.dialogTitle = '编辑质量评审单'
this.editForm = {
reviewId: row.reviewId,
productName: row.productName,
transmitDept: row.transmitDept,
transmitUser: row.transmitUser,
transmitDate: row.transmitDate,
prodDateRange: row.prodDateRange,
deptOpinion: row.deptOpinion,
deptSign: row.deptSign,
deptSignDate: row.deptSignDate,
coilList: this.coilList.map(c => ({ ...c }))
}
this.editing = true
this.dialogVisible = true
},
saveForm() {
this.$refs.form.validate(valid => {
if (!valid) return
const data = { ...this.editForm, coilList: this.editForm.coilList || [] }
if (this.editForm.reviewId) {
updateQualityReview(data).then(() => {
this.$modal.msgSuccess('修改成功')
this.dialogVisible = false
this.handleRefreshDetail()
})
} else {
addQualityReview(data).then(() => {
this.$modal.msgSuccess('创建成功')
this.dialogVisible = false
this.getList()
})
}
})
},
// ===== 钢卷选择(弹窗内) =====
onDialogCoilConfirm(selected) {
if (!selected || selected.length === 0) return
const startIdx = this.editForm.coilList.length || 0
const newCoils = selected.map((coil, idx) => ({
coilId: coil.coilId,
currentCoilNo: coil.currentCoilNo,
supplierCoilNo: coil.supplierCoilNo || coil.enterCoilNo,
spec: coil.spec,
netWeight: coil.netWeight,
defectDesc: '',
groupSeq: startIdx + idx + 1,
beforeQuality: coil.qualityStatus || 'O'
}))
this.editForm.coilList = [...(this.editForm.coilList || []), ...newCoils]
},
handleRemoveCoil(index) {
this.coilList.splice(index, 1)
},
// ===== 提交送审 =====
handleSubmit() {
this.$confirm('确认提交送审?', '提示', { type: 'warning' }).then(() => {
submitQualityReview(this.currentRow.reviewId).then(() => {
this.$modal.msgSuccess('送审成功')
this.handleRefreshDetail()
})
}).catch(() => {})
},
handleReSubmit() {
this.$confirm('确认重新提交?提交后将进入待审批状态。', '提示', { type: 'warning' }).then(() => {
submitQualityReview(this.currentRow.reviewId).then(() => {
this.$modal.msgSuccess('送审成功')
this.handleRefreshDetail()
})
}).catch(() => {})
},
// ===== 审批 =====
handleApprove() {
this.approveAction = 'approve'
this.approveForm = {
reviewId: this.currentRow.reviewId,
leaderOpinion: '',
leaderSign: ''
}
// 先构建完整对象再赋值
const map = {}
this.coilList.forEach(c => {
map[c.detailId] = c.regradeQuality || ''
})
this.approveCoilMap = map
},
doApprove() {
if (!this.approveForm.leaderOpinion) {
this.$modal.msgError('请输入审批意见')
return
}
const coilRegradeList = this.coilList.map(c => ({
detailId: c.detailId,
regradeQuality: this.approveCoilMap[c.detailId]
}))
const invalid = coilRegradeList.some(r => !r.regradeQuality)
if (invalid) {
this.$modal.msgError('请为每个钢卷指定改判后质量状态')
return
}
this.$confirm('确认审批通过?', '提示', { type: 'success' }).then(() => {
approveQualityReview({
reviewId: this.currentRow.reviewId,
leaderOpinion: this.approveForm.leaderOpinion,
leaderSign: this.approveForm.leaderSign,
coilRegradeList
}).then(() => {
this.$modal.msgSuccess('审批通过')
this.handleRefreshDetail()
})
}).catch(() => {})
},
handleReject() {
this.approveAction = 'reject'
this.rejectReason = ''
},
doReject() {
if (!this.rejectReason) {
this.$modal.msgError('请输入驳回原因')
return
}
this.$confirm('确认驳回该评审单?', '提示', { type: 'warning' }).then(() => {
rejectQualityReview(this.currentRow.reviewId, this.rejectReason).then(() => {
this.$modal.msgSuccess('已驳回')
this.handleRefreshDetail()
})
}).catch(() => {})
},
// ===== 执行改判 =====
handleExecute() {
this.$confirm('确认执行改判?执行后钢卷质量状态将被更新。', '提示', { type: 'warning' }).then(() => {
executeQualityReview(this.currentRow.reviewId).then(() => {
this.$modal.msgSuccess('改判执行成功')
this.handleRefreshDetail()
})
}).catch(() => {})
},
// ===== 删除 =====
handleDelete(row) {
this.$confirm('确认删除评审单【' + (row.reviewNo || row.reviewId) + '】?', '提示', { type: 'warning' }).then(() => {
delQualityReview(row.reviewId).then(() => {
this.$modal.msgSuccess('删除成功')
if (this.currentRow && this.currentRow.reviewId === row.reviewId) {
this.currentRow = null
this.coilList = []
this.logList = []
}
this.getList()
})
}).catch(() => {})
},
// ===== 辅助 =====
hasPermi(permi) {
return this.$store.getters.permissions && this.$store.getters.permissions.includes(permi)
},
saveOpinion() {
if (!this.currentRow) return
updateQualityReview({
reviewId: this.currentRow.reviewId,
deptOpinion: this.editForm.deptOpinion,
deptSign: this.editForm.deptSign,
deptSignDate: this.editForm.deptSignDate
}).then(() => {
this.$modal.msgSuccess('保存成功')
this.handleRefreshDetail()
})
}
}
}
</script>
<style lang="scss">
.quality-review-container {
display: flex;
height: calc(100vh - 84px);
gap: 12px;
padding: 0;
.left-panel {
width: 320px;
min-width: 320px;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 12px 0;
.header-title {
font-weight: 600;
font-size: 15px;
display: flex;
align-items: center;
gap: 6px;
}
.header-filter { width: 130px; }
}
.search-row {
display: flex;
padding: 8px 12px;
gap: 6px;
.el-input { flex: 1; }
}
.list-body {
flex: 1;
overflow-y: auto;
padding: 0 8px;
}
.list-item {
padding: 10px 12px;
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
border-radius: 4px;
margin: 2px 0;
&:hover { background: #f5f7fa; }
&.active { background: #ecf5ff; }
.item-main {
display: flex;
justify-content: space-between;
align-items: center;
.item-title { font-weight: 500; font-size: 13px; }
.item-sub { font-size: 12px; color: #909399; }
}
.item-meta { margin-top: 4px; }
.item-actions {
margin-top: 4px;
text-align: right;
}
}
.list-empty {
text-align: center;
padding: 40px 0;
color: #c0c4cc;
i { font-size: 36px; display: block; margin-bottom: 8px; }
}
.list-footer { padding: 8px; }
}
.right-panel {
flex: 1;
background: #fff;
border-radius: 4px;
border: 1px solid #e4e7ed;
overflow-y: auto;
padding: 16px;
.empty-tip {
text-align: center;
padding: 80px 0;
color: #c0c4cc;
i { font-size: 48px; display: block; margin-bottom: 12px; }
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 12px;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 12px;
.detail-header-left {
display: flex;
align-items: center;
gap: 8px;
.detail-title { font-size: 16px; font-weight: 600; }
}
.detail-header-actions { display: flex; gap: 4px; flex-wrap: wrap; }
}
.detail-section {
margin-bottom: 12px;
.basic-form .el-form-item { margin-bottom: 0; }
}
.section-editor {
.approve-coil-section {
background: #fafafa;
padding: 12px;
border-radius: 4px;
margin: 8px 0;
.approve-coil-title { font-weight: 500; margin-bottom: 8px; }
.approve-coil-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 6px;
.approve-coil-label { min-width: 200px; font-size: 13px; }
}
}
}
.section-display {
.opinion-text {
background: #fafafa;
padding: 12px;
border-radius: 4px;
line-height: 1.6;
white-space: pre-wrap;
}
.opinion-footer {
margin-top: 8px;
display: flex;
gap: 20px;
color: #909399;
font-size: 13px;
}
.reject-reason { margin-top: 12px; }
}
.dialog-section {
border: 1px solid #e4e7ed;
border-radius: 4px;
padding: 12px;
margin-bottom: 16px;
.dialog-section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
font-weight: 600;
font-size: 14px;
}
.dialog-table-empty {
text-align: center;
padding: 20px;
color: #c0c4cc;
font-size: 13px;
}
}
.text-muted { color: #c0c4cc; }
.timeline-opinion {
font-size: 13px;
color: #606266;
margin-top: 4px;
background: #f5f7fa;
padding: 6px 10px;
border-radius: 4px;
}
}
}
</style>

View File

@@ -0,0 +1,220 @@
<template>
<div class="app-container">
<div class="todo-header">
<h3><i class="el-icon-s-promotion"></i> 待审批质量评审单</h3>
</div>
<el-table :data="todoList" v-loading="loading" border stripe @row-click="handleRowClick" style="cursor:pointer;">
<el-table-column label="评审单编号" prop="reviewNo" width="180" />
<el-table-column label="产品名称" prop="productName" width="160" />
<el-table-column label="传递人" prop="transmitUser" width="100" />
<el-table-column label="传递日期" prop="transmitDate" width="120" />
<el-table-column label="生产日期" prop="prodDateRange" width="120" />
<el-table-column label="品质部意见" prop="deptOpinion" min-width="200" show-overflow-tooltip />
<el-table-column label="创建时间" prop="createTime" width="160" />
<el-table-column label="操作" width="200" align="center" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="success" plain @click.stop="handleQuickApprove(scope.row)">通过</el-button>
<el-button size="mini" type="danger" plain @click.stop="handleQuickReject(scope.row)">驳回</el-button>
</template>
</el-table-column>
</el-table>
<pagination :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList" />
<!-- 快速审批弹窗 -->
<el-dialog title="审批质量评审单" :visible.sync="approveDialog.visible" width="700px" :close-on-click-modal="false" append-to-body>
<div v-if="approveDialog.review">
<div class="approve-info">
<el-descriptions :column="2" size="small" border>
<el-descriptions-item label="评审单编号">{{ approveDialog.review.reviewNo }}</el-descriptions-item>
<el-descriptions-item label="产品名称">{{ approveDialog.review.productName }}</el-descriptions-item>
<el-descriptions-item label="传递人">{{ approveDialog.review.transmitUser }}</el-descriptions-item>
<el-descriptions-item label="传递日期">{{ approveDialog.review.transmitDate }}</el-descriptions-item>
</el-descriptions>
<div class="approve-opinion-box">
<label>品质部意见</label>
<div class="opinion-content">{{ approveDialog.review.deptOpinion }}</div>
</div>
</div>
<el-divider />
<el-radio-group v-model="approveAction" style="margin-bottom:12px;">
<el-radio-button label="approve">通过</el-radio-button>
<el-radio-button label="reject">驳回</el-radio-button>
</el-radio-group>
<template v-if="approveAction === 'approve'">
<el-input type="textarea" :rows="3" v-model="approveForm.leaderOpinion" placeholder="请输入领导审批意见..." style="margin-bottom:12px;" />
<el-input v-model="approveForm.leaderSign" placeholder="签字人" style="width:160px; margin-bottom:12px;" size="small" />
<div class="coil-regrade-section">
<div class="coil-regrade-title">请为以下钢卷指定改判后质量状态</div>
<el-table :data="approveCoilList" size="small" border max-height="300">
<el-table-column prop="currentCoilNo" label="产品卷号" width="140" />
<el-table-column prop="spec" label="规格" width="100" />
<el-table-column prop="beforeQuality" label="当前等级" width="80">
<template slot-scope="s"><el-tag size="mini" type="danger">{{ s.row.beforeQuality }}</el-tag></template>
</el-table-column>
<el-table-column label="改判后状态" width="160">
<template slot-scope="s">
<el-select v-model="approveCoilMap[s.row.detailId]" placeholder="请选择" size="small">
<el-option v-for="item in regradeOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</template>
</el-table-column>
</el-table>
</div>
<div style="margin-top:12px;">
<el-button type="success" @click="doApprove" :loading="approveLoading">确认通过</el-button>
</div>
</template>
<template v-else>
<el-input type="textarea" :rows="3" v-model="rejectReason" placeholder="请输入驳回原因..." />
<el-button type="danger" @click="doReject" :loading="approveLoading" style="margin-top:12px;">确认驳回</el-button>
</template>
</div>
</el-dialog>
</div>
</template>
<script>
import { listQualityReview, approveQualityReview, rejectQualityReview, getQualityReview } from '@/api/mes/qc/qualityReview'
export default {
name: 'QualityReviewTodo',
dicts: ['quality_review_status', 'regrade_quality_type'],
data() {
return {
loading: false,
todoList: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 20,
flowStatus: 2
},
regradeOptions: [
{ label: '协议销售', value: 'protocol_sale' },
{ label: '转分剪', value: 'to_slitting' },
{ label: '降级', value: 'downgrade' },
{ label: '返修', value: 'rework' },
{ label: '报废', value: 'scrap' }
],
approveDialog: {
visible: false,
review: null
},
approveAction: 'approve',
approveForm: {
reviewId: undefined,
leaderOpinion: '',
leaderSign: ''
},
approveCoilList: [],
approveCoilMap: {},
approveLoading: false,
rejectReason: ''
}
},
created() {
this.getList()
},
methods: {
getList() {
this.loading = true
listQualityReview(this.queryParams).then(res => {
this.todoList = res.rows || []
this.total = res.total || 0
this.loading = false
}).catch(() => { this.loading = false })
},
handleRowClick(row) {
this.handleQuickApprove(row)
},
handleQuickApprove(row) {
this.approveAction = 'approve'
this.approveDialog.review = row
this.approveForm = { reviewId: row.reviewId, leaderOpinion: '', leaderSign: '' }
this.rejectReason = ''
this.approveDialog.visible = true
// 加载明细
getQualityReview(row.reviewId).then(res => {
const data = res.data
this.approveCoilList = data.coilList || []
const map = {}
;(data.coilList || []).forEach(c => {
map[c.detailId] = c.regradeQuality || ''
})
this.approveCoilMap = map
})
},
handleQuickReject(row) {
this.$prompt('请输入驳回原因', '驳回评审单', { inputType: 'textarea', inputPattern: /.+/, inputErrorMessage: '驳回原因不能为空' }).then(({ value }) => {
rejectQualityReview(row.reviewId, value).then(() => {
this.$modal.msgSuccess('已驳回')
this.getList()
})
}).catch(() => {})
},
doApprove() {
if (!this.approveForm.leaderOpinion) {
this.$modal.msgError('请输入审批意见')
return
}
const coilRegradeList = this.approveCoilList.map(c => ({
detailId: c.detailId,
regradeQuality: this.approveCoilMap[c.detailId]
}))
if (coilRegradeList.some(r => !r.regradeQuality)) {
this.$modal.msgError('请为每个钢卷指定改判后质量状态')
return
}
this.approveLoading = true
approveQualityReview({
reviewId: this.approveDialog.review.reviewId,
leaderOpinion: this.approveForm.leaderOpinion,
leaderSign: this.approveForm.leaderSign,
coilRegradeList
}).then(() => {
this.$modal.msgSuccess('审批通过')
this.approveDialog.visible = false
this.approveLoading = false
this.getList()
}).catch(() => { this.approveLoading = false })
},
doReject() {
if (!this.rejectReason) {
this.$modal.msgError('请输入驳回原因')
return
}
this.approveLoading = true
rejectQualityReview(this.approveDialog.review.reviewId, this.rejectReason).then(() => {
this.$modal.msgSuccess('已驳回')
this.approveDialog.visible = false
this.approveLoading = false
this.getList()
}).catch(() => { this.approveLoading = false })
}
}
}
</script>
<style lang="scss">
.todo-header h3 { margin: 0 0 16px; i { margin-right: 6px; } }
.approve-info { margin-bottom: 8px; }
.approve-opinion-box {
margin-top: 12px;
label { font-weight: 600; display: block; margin-bottom: 6px; }
.opinion-content {
background: #f5f7fa;
padding: 10px;
border-radius: 4px;
line-height: 1.6;
white-space: pre-wrap;
}
}
.coil-regrade-section {
margin-top: 12px;
.coil-regrade-title { font-weight: 500; margin-bottom: 8px; }
}
</style>

View File

@@ -86,6 +86,10 @@ module.exports = {
'@': resolve('src')
}
},
// 解决 chokidar 监视系统文件 (pagefile.sys) 导致的 EBUSY 错误
watchOptions: {
ignored: ['node_modules', '\.git', /pagefile\.sys$/, /swapfile\.sys$/]
},
plugins: [
// http://doc.klp.vip/klp-vue/other/faq.html#使用gzip解压缩静态文件
new CompressionPlugin({