feat: 完成多模块功能迭代与优化
本次提交包含多模块功能更新: 1. 权限系统重构:移除硬编码admin放行逻辑,新增精细化权限控制,完善权限树形结构与用户/角色保护 2. 新增物料导入导出接口,补充台账查询条件 3. 优化列表查询排序逻辑,新增区域编码大小写不敏感搜索 4. 修复帮助抽屉重复弹出问题,补充页面权限控制 5. 新增弱密码提示逻辑,优化MES/WMS用户/角色管理权限 6. 调整路由结构,新增权限校验脚本与前端权限树重构 7. 补充数据库字段与依赖包更新
This commit is contained in:
@@ -1,11 +1,19 @@
|
||||
<script setup>
|
||||
defineOptions({ name: 'Inspection' })
|
||||
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import request from '../utils/request'
|
||||
import { getRealName } from '../utils/auth'
|
||||
import { fmtTime } from '../utils/format'
|
||||
import { exportXlsxFetch } from '../utils/export'
|
||||
import { can } from '../utils/perm'
|
||||
import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpInspection } from '../help'
|
||||
|
||||
// ===== 多 Tab:录入在前默认打开,记录在后点击才见(参照出库管理) =====
|
||||
const activeTab = ref('entry') // entry=质量检验(录入) / records=检验记录
|
||||
|
||||
/* ===================== Tab1 质量检验(录入,默认打开) ===================== */
|
||||
const targetType = ref('BATCH')
|
||||
const status = ref('合格')
|
||||
const inspector = ref(getRealName())
|
||||
@@ -43,7 +51,6 @@ async function loadPicker() {
|
||||
pickerOptions.value = []
|
||||
}
|
||||
}
|
||||
onMounted(loadPicker)
|
||||
|
||||
function pickerLabel(opt) {
|
||||
const id = targetType.value === 'BATCH' ? opt.batchNo : opt.snCode
|
||||
@@ -100,93 +107,288 @@ async function submitInspection() {
|
||||
passQty.value = 0
|
||||
checkDesc.value = ''
|
||||
failReason.value = ''
|
||||
// 提交成功后主动刷新记录 Tab(用户切过去即可见最新)
|
||||
await loadRecords()
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Tab2 检验记录(点击才加载) ===================== */
|
||||
// 默认近3个月起始日期
|
||||
function defaultStart() {
|
||||
const d = new Date()
|
||||
d.setMonth(d.getMonth() - 3)
|
||||
const y = d.getFullYear()
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(d.getDate()).padStart(2, '0')
|
||||
return `${y}-${m}-${day}`
|
||||
}
|
||||
|
||||
// 筛选独立条件(不再笼统关键字);默认近3个月
|
||||
const filters = reactive({
|
||||
targetId: '', materialCode: '', inspectionType: '', status: '', inspector: '',
|
||||
startDate: defaultStart(), endDate: ''
|
||||
})
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const page = reactive({ current: 1, size: 20 })
|
||||
|
||||
function typeLabel(t) {
|
||||
const m = { incoming: '来料检', process: '过程检', finished: '成品检' }
|
||||
return m[t] || t || '-'
|
||||
}
|
||||
function targetTypeLabel(t) {
|
||||
return t === 'SN' ? 'SN' : t === 'BATCH' ? '批次' : (t || '-')
|
||||
}
|
||||
|
||||
async function loadRecords() {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await request.get('/inspection/query', {
|
||||
params: {
|
||||
targetId: filters.targetId.trim(),
|
||||
materialCode: filters.materialCode.trim(),
|
||||
inspectionType: filters.inspectionType,
|
||||
status: filters.status,
|
||||
inspector: filters.inspector.trim(),
|
||||
startDate: filters.startDate,
|
||||
endDate: filters.endDate,
|
||||
page: page.current,
|
||||
pageSize: page.size
|
||||
}
|
||||
})
|
||||
list.value = data?.list || []
|
||||
total.value = data?.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
page.current = 1
|
||||
loadRecords()
|
||||
}
|
||||
|
||||
// 全量导出按当前筛选条件(无分页),后端生成 .xlsx
|
||||
async function exportCsv() {
|
||||
try {
|
||||
await exportXlsxFetch('/api/inspection/export', {
|
||||
targetId: filters.targetId.trim(),
|
||||
materialCode: filters.materialCode.trim(),
|
||||
inspectionType: filters.inspectionType,
|
||||
status: filters.status,
|
||||
inspector: filters.inspector.trim(),
|
||||
startDate: filters.startDate,
|
||||
endDate: filters.endDate
|
||||
}, '检验记录.xlsx')
|
||||
} catch (e) {
|
||||
/* 错误提示由请求层统一处理 */
|
||||
}
|
||||
}
|
||||
|
||||
function onTabChange(tab) {
|
||||
// 进入记录 Tab 才加载,控制首次进入即查询
|
||||
if (tab === 'records') loadRecords()
|
||||
}
|
||||
|
||||
// 仅录入时需预载库存拾取器;记录 Tab 懒加载
|
||||
onMounted(() => {
|
||||
loadPicker()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-card shadow="never" style="max-width:860px;margin:0 auto">
|
||||
<el-form label-width="96px">
|
||||
<!-- 目标类型直接改变输入框标题与占位,直观体现当前录入对象 -->
|
||||
<el-form-item label="目标类型">
|
||||
<el-radio-group v-model="targetType" @change="loadPicker">
|
||||
<el-radio-button value="BATCH">批次 (结构件)</el-radio-button>
|
||||
<el-radio-button value="SN">序列号 SN (精密件)</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="检验结论">
|
||||
<el-radio-group v-model="status">
|
||||
<el-radio value="合格">合格</el-radio>
|
||||
<el-radio value="不合格">不合格</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="检验员">
|
||||
<el-input v-model="inspector" placeholder="检验员姓名" clearable style="width:260px" />
|
||||
</el-form-item>
|
||||
<div>
|
||||
<el-tabs v-model="activeTab" @tab-change="onTabChange">
|
||||
<!-- ============ 质量检验(录入,默认打开) ============ -->
|
||||
<el-tab-pane label="质量检验录入" name="entry">
|
||||
<el-card shadow="never" style="max-width:860px;margin:0 auto">
|
||||
<el-form label-width="96px">
|
||||
<!-- 目标类型直接改变输入框标题与占位,直观体现当前录入对象 -->
|
||||
<el-form-item label="目标类型">
|
||||
<el-radio-group v-model="targetType" @change="loadPicker">
|
||||
<el-radio-button value="BATCH">批次 (结构件)</el-radio-button>
|
||||
<el-radio-button value="SN">序列号 SN (精密件)</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="检验结论">
|
||||
<el-radio-group v-model="status">
|
||||
<el-radio value="合格">合格</el-radio>
|
||||
<el-radio value="不合格">不合格</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="检验员">
|
||||
<el-input v-model="inspector" placeholder="检验员姓名" clearable style="width:260px" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">检验量化(可空)</el-divider>
|
||||
<el-form-item label="检验数量">
|
||||
<el-input-number v-model="inspectQty" :min="0" :max="999999" placeholder="抽检件数" style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合格数量">
|
||||
<el-input-number v-model="passQty" :min="0" :max="999999" placeholder="合格件数(≤检验数量)" style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="检测说明">
|
||||
<el-input v-model="checkDesc" placeholder="检验项目/方法/标准(可空)" clearable style="width:420px" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="status === '不合格'" label="不合格原因">
|
||||
<el-input v-model="failReason" type="textarea" :rows="2" placeholder="不合格原因(可空)" style="width:420px" />
|
||||
</el-form-item>
|
||||
<el-divider content-position="left">检验量化(可空)</el-divider>
|
||||
<el-form-item label="检验数量">
|
||||
<el-input-number v-model="inspectQty" :min="0" :max="999999" placeholder="抽检件数" style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合格数量">
|
||||
<el-input-number v-model="passQty" :min="0" :max="999999" placeholder="合格件数(≤检验数量)" style="width:160px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="检测说明">
|
||||
<el-input v-model="checkDesc" placeholder="检验项目/方法/标准(可空)" clearable style="width:420px" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="status === '不合格'" label="不合格原因">
|
||||
<el-input v-model="failReason" type="textarea" :rows="2" placeholder="不合格原因(可空)" style="width:420px" />
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">待检编号(可扫/选多条,统一提交)</el-divider>
|
||||
<el-divider content-position="left">待检编号(可扫/选多条,统一提交)</el-divider>
|
||||
|
||||
<!-- 从库存直接拾取,避免手输批次号 -->
|
||||
<el-form-item :label="`从库存选${inputLabel}`">
|
||||
<el-select
|
||||
v-model="pickerValue"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="`选择已存在的${inputLabel}(来自库存)`"
|
||||
style="width:100%"
|
||||
@change="addFromPicker"
|
||||
>
|
||||
<el-option v-for="opt in pickerOptions" :key="targetType === 'BATCH' ? opt.batchNo : opt.snCode"
|
||||
:value="targetType === 'BATCH' ? opt.batchNo : opt.snCode"
|
||||
:label="pickerLabel(opt)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- 从库存直接拾取,避免手输批次号 -->
|
||||
<el-form-item :label="`从库存选${inputLabel}`">
|
||||
<el-select
|
||||
v-model="pickerValue"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="`选择已存在的${inputLabel}(来自库存)`"
|
||||
style="width:100%"
|
||||
@change="addFromPicker"
|
||||
>
|
||||
<el-option v-for="opt in pickerOptions" :key="targetType === 'BATCH' ? opt.batchNo : opt.snCode"
|
||||
:value="targetType === 'BATCH' ? opt.batchNo : opt.snCode"
|
||||
:label="pickerLabel(opt)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 批量/扫码录入:单条也走这里,无需单独的"单个录入" -->
|
||||
<el-form-item :label="`录入${inputLabel}`">
|
||||
<div style="width:100%">
|
||||
<el-input v-model="scanInput" type="textarea" :rows="4"
|
||||
:placeholder="inputPlaceholder" />
|
||||
<el-button type="primary" plain style="margin-top:6px" :disabled="!scanInput.trim()"
|
||||
@click="addFromInput">录入到列表</el-button>
|
||||
<span style="margin-left:8px;color:#909399;font-size:12px">支持扫码枪/粘贴,多编号用逗号、分号、空格或回车分隔</span>
|
||||
<!-- 批量/扫码录入:单条也走这里,无需单独的"单个录入" -->
|
||||
<el-form-item :label="`录入${inputLabel}`">
|
||||
<div style="width:100%">
|
||||
<el-input v-model="scanInput" type="textarea" :rows="4"
|
||||
:placeholder="inputPlaceholder" />
|
||||
<el-button type="primary" plain style="margin-top:6px" :disabled="!scanInput.trim()"
|
||||
@click="addFromInput">录入到列表</el-button>
|
||||
<span style="margin-left:8px;color:#909399;font-size:12px">支持扫码枪/粘贴,多编号用逗号、分号、空格或回车分隔</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 已选清单 -->
|
||||
<el-form-item label="已选清单">
|
||||
<div style="width:100%">
|
||||
<el-tag v-for="(v, i) in targetList" :key="v + i" closable type="info"
|
||||
style="margin:0 8px 8px 0" @close="removeAt(i)">{{ v }}</el-tag>
|
||||
<span v-if="!targetList.length" style="color:#909399;font-size:12px">尚未选择任何待检编号</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="submitInspection">提交检验</el-button>
|
||||
<el-button :disabled="!targetList.length" @click="clearAll">清空</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- ============ 检验记录(点击才加载) ============ -->
|
||||
<el-tab-pane label="检验记录" name="records">
|
||||
<el-card shadow="never" class="mb12">
|
||||
<el-form inline @submit.prevent="search">
|
||||
<el-form-item label="批次/SN">
|
||||
<el-input v-model="filters.targetId" placeholder="批次号或 SN" clearable style="width:160px"
|
||||
@keyup.enter="search" />
|
||||
</el-form-item>
|
||||
<el-form-item label="物料编码">
|
||||
<el-input v-model="filters.materialCode" placeholder="物料编码" clearable style="width:150px"
|
||||
@keyup.enter="search" />
|
||||
</el-form-item>
|
||||
<el-form-item label="检验类型">
|
||||
<el-select v-model="filters.inspectionType" placeholder="全部" clearable style="width:120px" @change="search">
|
||||
<el-option label="来料检" value="incoming" />
|
||||
<el-option label="过程检" value="process" />
|
||||
<el-option label="成品检" value="finished" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="结论">
|
||||
<el-select v-model="filters.status" placeholder="全部" clearable style="width:110px" @change="search">
|
||||
<el-option label="合格" value="合格" />
|
||||
<el-option label="不合格" value="不合格" />
|
||||
<el-option label="未检" value="未检" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="检验员">
|
||||
<el-input v-model="filters.inspector" placeholder="检验员" clearable style="width:120px"
|
||||
@keyup.enter="search" />
|
||||
</el-form-item>
|
||||
<el-form-item label="检验时间">
|
||||
<el-date-picker v-model="filters.startDate" type="date" value-format="YYYY-MM-DD" placeholder="开始"
|
||||
style="width:130px" @change="search" />
|
||||
<span style="margin:0 6px">至</span>
|
||||
<el-date-picker v-model="filters.endDate" type="date" value-format="YYYY-MM-DD" placeholder="结束"
|
||||
style="width:130px" @change="search" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="search">查询</el-button>
|
||||
<el-button v-if="can('inspection:export')" type="success" plain @click="exportCsv">导出</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card shadow="never" header="质量检验记录">
|
||||
<el-table :data="list" border size="small" v-loading="loading" empty-text="暂无检验记录">
|
||||
<el-table-column label="ID" prop="id" width="60" align="center" />
|
||||
<el-table-column label="对象" width="70" align="center">
|
||||
<template #default="{ row }">{{ targetTypeLabel(row.targetType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="targetId" label="批次/SN" min-width="150" />
|
||||
<el-table-column prop="materialCode" label="物料编码" min-width="130" />
|
||||
<el-table-column label="检验类型" width="90" align="center">
|
||||
<template #default="{ row }">{{ typeLabel(row.inspectionType) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结论" width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === '合格' ? 'success' : row.status === '不合格' ? 'danger' : 'info'"
|
||||
size="small">{{ row.status || '未检' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="inspectQty" label="检验数" width="80" align="center">
|
||||
<template #default="{ row }">{{ row.inspectQty || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="passQty" label="合格数" width="80" align="center">
|
||||
<template #default="{ row }">{{ row.passQty || 0 }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="检测说明" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.checkDesc" :content="row.checkDesc" placement="top">
|
||||
<span>{{ row.checkDesc }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="不合格原因" min-width="140">
|
||||
<template #default="{ row }">
|
||||
<el-tooltip v-if="row.failReason" :content="row.failReason" placement="top">
|
||||
<span style="color:#f56c6c">{{ row.failReason }}</span>
|
||||
</el-tooltip>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="inspector" label="检验员" width="90" align="center">
|
||||
<template #default="{ row }">{{ row.inspector || '-' }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="检验时间" width="155" align="center">
|
||||
<template #default="{ row }">{{ fmtTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<div class="pager">
|
||||
<el-pagination background layout="total, sizes, prev, pager, next" :total="total"
|
||||
:current-page="page.current" :page-size="page.size" :page-sizes="[10, 20, 50, 100]"
|
||||
@current-change="(p) => { page.current = p; loadRecords() }"
|
||||
@size-change="(s) => { page.size = s; page.current = 1; loadRecords() }" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 已选清单 -->
|
||||
<el-form-item label="已选清单">
|
||||
<div style="width:100%">
|
||||
<el-tag v-for="(v, i) in targetList" :key="v + i" closable type="info"
|
||||
style="margin:0 8px 8px 0" @close="removeAt(i)">{{ v }}</el-tag>
|
||||
<span v-if="!targetList.length" style="color:#909399;font-size:12px">尚未选择任何待检编号</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="submitting" @click="submitInspection">提交检验</el-button>
|
||||
<el-button :disabled="!targetList.length" @click="clearAll">清空</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<PageHelp :data="helpInspection" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<PageHelp :data="helpInspection" page="Inspection" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mb12 { margin-bottom: 12px; }
|
||||
.pager { display: flex; justify-content: flex-end; margin-top: 12px; }
|
||||
.tip { margin-left: 10px; color: #909399; font-size: 12px; }
|
||||
</style>
|
||||
</style>
|
||||
Reference in New Issue
Block a user