mes-vue/src/views/biz/qualityTesting/index.vue
shaoleiliu-netizen123 f67a24d8b3 让步接收触发审批推送钉钉
让步接收申请单前端页面
2026-06-06 16:44:34 +08:00

1132 lines
38 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page-container">
<n-card>
<!-- 搜索表单 -->
<div class="search-form">
<n-form inline :model="searchForm" label-placement="left">
<n-form-item label="质检编号">
<n-input v-model:value="searchForm.qcNo" placeholder="请输入质检编号" clearable />
</n-form-item>
<n-form-item label="质检状态">
<n-select
v-model:value="searchForm.status"
:options="qcStatusOptions"
placeholder="请选择质检状态"
clearable
style="width: 200px"
/>
</n-form-item>
<n-form-item>
<n-space>
<n-button type="primary" @click="handleSearch">
<template #icon><n-icon><SearchOutline /></n-icon></template>
搜索
</n-button>
<n-button @click="handleReset">
<template #icon><n-icon><RefreshOutline /></n-icon></template>
重置
</n-button>
</n-space>
</n-form-item>
</n-form>
</div>
<!-- 工具栏 -->
<div class="table-toolbar">
<n-space>
<n-button type="primary" @click="handleAdd">
<template #icon><n-icon><AddOutline /></n-icon></template>
新增
</n-button>
<n-button @click="importModalVisible = true">
<template #icon><n-icon><CloudUploadOutline /></n-icon></template>
导入
</n-button>
<n-button @click="handleExport">
<template #icon><n-icon><DownloadOutline /></n-icon></template>
导出{{ selectedIds.length > 0 ? `(${selectedIds.length})` : '' }}
</n-button>
<n-button type="error" :disabled="selectedIds.length === 0" @click="handleBatchDelete">
<template #icon><n-icon><TrashOutline /></n-icon></template>
删除
</n-button>
</n-space>
</div>
<!-- 表格 -->
<n-data-table
:columns="columns"
:data="tableData"
:loading="loading"
size="small"
remote
:border="false"
:single-line="false"
striped
/>
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
<n-pagination
v-model:page="pagination.page"
v-model:page-size="pagination.pageSize"
:item-count="pagination.itemCount"
:page-sizes="[10, 20, 50, 100]"
show-size-picker
show-quick-jumper
@update:page="handlePageChange"
@update:page-size="handlePageSizeChange"
>
<template #prefix>
{{ pagination.itemCount }}
</template>
</n-pagination>
</div>
</n-card>
<!-- 新增/编辑弹窗 -->
<n-modal v-model:show="modalVisible" preset="card" :title="modalTitle" style="width: 600px">
<n-form ref="formRef" :model="formData" :rules="formRules" label-placement="left" label-width="100px">
<n-form-item label="质检编号">
<n-input v-model:value="formData.qcNo" placeholder="请输入质检编号" disabled />
</n-form-item>
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="来源类型" path="sourceType">
<n-select
v-model:value="formData.sourceType"
:options="qcSourceTypeOptions"
placeholder="请选择来源类型"
clearable
style="width: 200px"
/>
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="业务来源" path="sourceId">
<n-select
v-model:value="formData.sourceId"
:options="assingWork"
placeholder="请选择业务来源"
clearable
:disabled="assingWorkDisabled"
style="width: 200px"
/>
</n-form-item>
</n-gi>
</n-grid>
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="报检总数" path="totalQty">
<n-input-number v-model:value="formData.totalQty" placeholder="请输入报检总数" :min="1" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="质检状态" >
<n-select v-model:value="formData.status" placeholder="请选择状态:0待检/1检验中/2已判定/3已关闭" :options="qcStatusOptions" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="追溯类型" path="traceType">
<n-select
v-model:value="formData.traceType"
:options="qcTraceTypeOptions"
placeholder="请选择追溯类型:0无/1批次/2SN"
clearable
style="width: 200px"
/>
</n-form-item>
</n-form>
<template #footer>
<n-space justify="end">
<n-button @click="modalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmit">确定</n-button>
</n-space>
</template>
</n-modal>
<!-- 导入弹窗 -->
<n-modal v-model:show="importModalVisible" preset="card" title="导入质检单表" style="width: 500px">
<n-space vertical>
<n-alert type="info">
<template #header>导入说明</template>
<ul style="margin: 0; padding-left: 16px; line-height: 1.8">
<li>请先下载导入模板按模板格式填写数据</li>
<li>支持 .xlsx .xls 格式</li>
</ul>
</n-alert>
<n-space>
<n-button type="primary" @click="handleDownloadTemplate">
<template #icon><n-icon><DownloadOutline /></n-icon></template>
下载模板
</n-button>
</n-space>
<n-upload :max="1" accept=".xlsx,.xls" :show-file-list="true" :custom-request="handleImportUpload">
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3"><CloudUploadOutline /></n-icon>
</div>
<n-text style="font-size: 16px">点击或拖拽文件到此处上传</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">支持 .xlsx .xls 格式</n-p>
</n-upload-dragger>
</n-upload>
</n-space>
<template #footer>
<n-button @click="importModalVisible = false">关闭</n-button>
</template>
</n-modal>
<!--质检单详情面板-->
<n-modal v-model:show="detailVisible" preset="card" :title="detailTitle" style="width: 800px">
<n-card hoverable style="margin-bottom: 12px" >
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center;width:100%">
<span>主表信息</span>
<n-button text @click="infoIsCollapse = !infoIsCollapse">
<n-icon :component="infoIsCollapse ? ArrowDownOutline : ArrowUpOutline" />
</n-button>
</div>
</template>
<n-descriptions :column="3" bordered v-show="!infoIsCollapse">
<n-descriptions-item
v-for="item in mappedInfoList"
:key="item.key"
:label="item.label"
>
{{ formatDisplayValue(item.key, item.value) }}
</n-descriptions-item>
</n-descriptions>
</n-card>
<n-card hoverable style="margin-bottom: 12px">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center;width:100%">
<span>明细列表</span>
<n-button text @click="detailIsCollapse = !detailIsCollapse">
<n-icon :component="detailIsCollapse ? ArrowDownOutline : ArrowUpOutline" />
</n-button>
</div>
</template>
<n-data-table
v-show="!detailIsCollapse"
:columns="detailColumns"
:data="detailTableData"
:loading="loading"
:row-key="(row) => row.id"
:scroll-x="700"
max-height="320"
:pagination="{ pageSize:4 }"
/>
</n-card>
<!-- <n-card title="追溯码" hoverable>-->
<!-- 追溯码:{{}}-->
<!-- </n-card>-->
</n-modal>
<!--提交判定明细面板-->
<n-modal v-model:show="submitDetailModalVisible" preset="card" title="提交判定明细" style="width: 800px">
<n-collapse default-expanded-names="['ok', 'scrap', 'rework','concession']">
<n-collapse-item title="合格" name="ok">
<n-form ref="formRef" :model="formDetailOKData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailOKData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailOKData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailOKData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-form>
</n-collapse-item>
<n-collapse-item title="报废" name="scrap">
<n-form ref="formRef" :model="formDetailScrapData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailScrapData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailScrapData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailScrapData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-form>
</n-collapse-item>
<n-collapse-item title="返工" name="rework">
<n-form ref="formRef" :model="formDetailReworkData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailReworkData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailReworkData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailReworkData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
</n-form>
</n-collapse-item>
<n-collapse-item title="让步接收" name="concession">
<n-form ref="formRef" :model="formDetailConcessionData" :rules="formRules" label-placement="left" label-width="100px">
<n-grid x-gap="12" :cols="2">
<n-gi>
<n-form-item label="判定数量" path="qty">
<n-input-number v-model:value="formDetailConcessionData.qty" placeholder="请输入判定数量" :min="0" />
</n-form-item>
</n-gi>
<n-gi>
<n-form-item label="批次号或SN码" path="traceCode" v-show="traceCodeShow">
<n-input v-model:value="formDetailConcessionData.traceCode" placeholder="请输入批次号或SN码(启用追溯时必填)" />
</n-form-item>
</n-gi>
</n-grid>
<n-form-item label="备注" >
<n-input
v-model:value="formDetailConcessionData.remark"
type="textarea"
placeholder="输入备注原因"
/>
</n-form-item>
<n-form-item label="附件">
<n-upload
multiple
directory-dnd
:max="5"
:action="null"
:custom-request="()=>{}"
@change="handleUploadChange"
:file-list="fileList"
accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png,.gif,.bmp"
:max-size="10*1024*1024"
>
<n-upload-dragger>
<div style="margin-bottom: 12px">
<n-icon size="48" :depth="3">
<ArchiveIcon />
</n-icon>
</div>
<n-text style="font-size: 16px">
点击或者拖动文件到该区域来上传
</n-text>
<n-p depth="3" style="margin: 8px 0 0 0">
仅支持 PDFDOCDOCXXLSXLSXJPGPNGGIF单文件最大10MB
</n-p>
</n-upload-dragger>
</n-upload>
</n-form-item>
</n-form>
</n-collapse-item>
</n-collapse>
<template #footer>
<n-space justify="end">
<n-button @click="submitDetailModalVisible = false">取消</n-button>
<n-button type="primary" @click="handleSubmitResultDetail">确定</n-button>
</n-space>
</template>
</n-modal>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted } from 'vue'
import {
NButton, NSpace, NIcon, NUpload,NTag, useMessage, useDialog, type DataTableColumns, type UploadCustomRequestOptions,
NDropdown
} from 'naive-ui'
import {
SearchOutline, RefreshOutline, AddOutline, TrashOutline, CreateOutline, CloudUploadOutline, DownloadOutline,
EllipsisHorizontalOutline, ArrowDownOutline, ArrowUpOutline,ArchiveOutline as ArchiveIcon
} from '@vicons/ionicons5'
import { qualityTestingApi, type QualityTesting } from '@/api/qualityTesting'
import {assingReworkApi} from '@/api/assingRework'
import { dictDataApi } from '@/api/org'
import {Language} from "@xterm/xterm/src/vs/base/common/platform.ts";
import {fileApi} from '@/api/system1'
import value = Language.value;
import {QcResultDetail} from "@/api/qcResultDetail.ts";
import router from "@/router";
const message = useMessage()
const dialog = useDialog()
// 表格数据
const tableData = ref<QualityTesting[]>([])
const loading = ref(false)
const selectedIds = ref<number[]>([])
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
// 搜索表单
const searchForm = reactive({
qcNo: null as number | null,
status: null as number | null,
})
// 弹窗
const modalVisible = ref(false)
const modalTitle = ref('')
const importModalVisible = ref(false)
//质检单详情
const detailVisible = ref(false)
const detailTitle = ref('')
const infoIsCollapse = ref(false)
const detailIsCollapse = ref(true)
//提交判定明细
const submitDetailModalVisible = ref(false)
const traceCodeShow = ref(false)
//提交明细/部分提交明细标识
const submitFlag = ref<Boolean>()
const customOrderKeys = [
'materialCode',
'materialName',
'materialQty',
'processCode',
'processName',
'processQty',
'assingCode',
'workerName',
'assingQty',
'qcNo',
'sourceType',
'totalQty',
'status',
'inspector',
'inspectTime',
'traceType'
]
const infoDataMapping = {
materialCode: '物料编码',
materialName: '物料名称',
materialQty: '物料数量',
processCode:'工序编号',
processName:'工序名称',
processQty:'生产总数',
assingCode:'派工编号',
workerName:'派工人',
assingQty:'派工数量',
qcNo: '质检单编号',
sourceType: '来源类型',
totalQty: '报检总数',
status: '质检状态',
inspector: '质检员',
inspectTime: '质检时间',
traceType: '追溯类型'
}
const infoDataMap = new Map(Object.entries(infoDataMapping))
const mappedInfoList = ref<Array<{ label: string; key: string; value: any }>>([])
//详情列表名
const detailColumns: DataTableColumns<Object> = [
{
title: '判定类型',
key: 'resultType',
align: 'center',
width: 80,
render: (row) => {
const val = row.resultType
const opt = qcResultTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}
},
{
title: '判定数量',
key: 'qty',
align: 'center',
width: 80
},
{
title: '批次号',
key: 'traceCode',
width: 80,
align: 'center',
render: (row) => {
const val = row.traceCode
return val ?? '-'
}
},
{
title: '不良代码',
key: 'defectCode',
width: 80,
align: 'center',
render: (row) => {
const val = row.defectCode
return val ?? '-'
}
},
{
title: '后续动作',
key: 'handleAction',
width: 80,
align: 'center'
},
{
title: '动作执行状态',
key: 'actionStatus',
width: 80,
align: 'center',
render: (row) => {
const val = row.actionStatus
const opt = qcActionStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}
}
]
const detailTableData = ref<Object[]>([])
const formRef = ref()
const defaultFormData: QualityTesting = {
qcNo: '',
sourceType: undefined,
sourceId: undefined,
orderId: undefined,
processId: undefined,
itemId: undefined,
totalQty: undefined,
status: undefined,
inspectorId: undefined,
inspectTime: undefined,
traceType: undefined,
}
const formData = reactive<QualityTesting>({ ...defaultFormData })
const defaultFormDetailData:QcResultDetail = {
qcId: undefined,
resultType: undefined,
qty: 0,
traceCode: undefined,
defectCode: undefined,
handleAction: undefined,
actionStatus: undefined
}
//提交附件
const fileList = ref<any[]>([])
const uploadFiles = ref<File[]>([])
const formDetailOKData = reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 })
const formDetailScrapData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 })
const formDetailReworkData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 })
const formDetailConcessionData = reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'让步接收',actionStatus:1 })
const assingWorkDisabled = ref(true)
const assingWork = ref<{ label: string; value: any }[]>([])
// 字典选项(下拉框/单选框/复选框关联字典时使用)
const statusOptions = ref<{ label: string; value: any }[]>([])
//质检状态字典
const qcStatusOptions = ref<{ label: string; value: any }[]>([])
//业务来源字典
const qcSourceTypeOptions = ref<{ label: string; value: any }[]>([])
//追溯状态字段
const qcTraceTypeOptions = ref<{ label: string; value: any }[]>([])
//判定类型
const qcResultTypeOptions = ref<{ label: string; value: any }[]>([])
//动作执行状态
const qcActionStatusOptions = ref<{ label: string; value: any }[]>([])
// 表单校验规则
const formRules = {
sourceType: [{ required: true, type: 'number', message: '请选择来源类型:1工序汇报/2委外/3手工', trigger: 'change' }],
totalQty: [{ required: true,type: 'number', message: '请输入报检总数', trigger: 'blur' }],
}
// 表格列
const columns: DataTableColumns<QualityTesting> = [
{ type: 'selection' },
{ title: '质检编号', key: 'qcNo',align: 'center' },
{ title: '来源类型', key: 'sourceType',align: 'center',
render(row) {
const val = row.sourceType
const opt = qcSourceTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}
},
// { title: '来源业务ID(汇报单/派工单等)', key: 'sourceId' },
// { title: '关联生产订单ID', key: 'orderId' },
// { title: '关联工序计划ID', key: 'processId' },
// { title: '物料ID', key: 'itemId' },
{ title: '报检总数', key: 'totalQty',align: 'center' },
{ title: '已检验数量', key: 'inspectQty',align: 'center' },
{ title: '质检状态', key: 'status',align: 'center',
render(row) {
const val = row.status
const opt = qcStatusOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}
},
{ title: '检验员', key: 'inspector',align: 'center' },
{ title: '检验完成时间', key: 'inspectTime',align: 'center' },
{ title: '追溯类型', key: 'traceType',align: 'center',
render(row) {
const val = row.traceType
const opt = qcTraceTypeOptions.value.find(o => o.value === val || String(o.value) === String(val))
if (!opt) return val ?? '-'
return h(NTag, { type: opt.class, size: 'small' }, { default: () => opt.label })
}
},
{ title: '创建时间', key: 'createTime',align: 'center', width: 180 },
{ title: '更新时间', key: 'updateTime',align: 'center', width: 180 },
{
title: '操作',
key: 'actions',
width: 140,
fixed: 'right',
render(row) {
return h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flexWrap: 'nowrap' } }, [
h(NButton, { size: 'small', quaternary: true, onClick: () => handleEdit(row) }, {
default: () => [h(NIcon, null, { default: () => h(CreateOutline) }), ' 编辑']
}),
h(NButton, { size: 'small', quaternary: true, type: 'error', onClick: () => handleDelete(row) }, {
default: () => [h(NIcon, null, { default: () => h(TrashOutline) }), ' 删除']
}),
h(NDropdown, {
trigger: 'click', // 点击展开
options: [
{ label: '查看详情', key: 'view' },
{ type: 'divider' }, // 分割线
{ label: '提交判定明细', key: 'submitResultDetail',show:row.status == 1 },
{ type: 'divider',show:row.status == 1 }, // 分割线
{ label: '提交部分判定明细', key: 'submitBFResultDetail',show:(row.status == 1 || row.status == 2) },
{ type: 'divider',show:(row.status == 1 || row.status == 2) }, // 分割线
{ label: '查询判定明细', key: 'queryResultDetail' },
{ type: 'divider' }, // 分割线
{ label: '质检撤回', key: 'cancelQuality' },
],
// 下拉菜单项点击事件
onSelect: (key: string) => {
switch (key) {
case 'view':
handleDetail(row)
break
case 'submitResultDetail':
submitResultDetail(row)
break
case 'submitBFResultDetail':
submitBFResultDetail(row)
break
case 'queryResultDetail':
queryResultDetail(row)
break
case 'cancelQuality':
handleCancelQuality(row)
break
}
}
}, {
// 下拉触发按钮:三点图标 / 更多文字
default: () => h(NButton, { size: 'small', quaternary: true }, {
default: () => [h(NIcon, null, { default: () => h(EllipsisHorizontalOutline) })]
})
})
])
}
}
]
// 加载数据
async function loadData() {
loading.value = true
try {
const res = await qualityTestingApi.page({
page:pagination.page,
pageSize: pagination.pageSize,
qcNo:searchForm.qcNo,
status: searchForm.status
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
// 搜索
function handleSearch() {
pagination.page = 1
loadData()
}
// 重置
function handleReset() {
searchForm.id = null
searchForm.status = null
handleSearch()
}
// 分页
function handlePageChange(page: number) {
pagination.page = page
loadData()
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize
pagination.page = 1
loadData()
}
// 选择
function handleCheck(keys: Array<string | number>) {
selectedIds.value = keys as number[]
}
// 新增
function handleAdd() {
modalTitle.value = '新增质检单表'
assingWorkDisabled.value = false
Object.assign(formData, defaultFormData)
modalVisible.value = true
}
// 编辑
function handleEdit(row: QualityTesting) {
modalTitle.value = '编辑质检单表'
assingWorkDisabled.value = true
Object.assign(formData, row)
if (formData.inspectTime && typeof formData.inspectTime === 'string') {
formData.inspectTime = new Date(formData.inspectTime.replace(' ', 'T')).getTime()
}
if (formData.createTime && typeof formData.createTime === 'string') {
formData.createTime = new Date(formData.createTime.replace(' ', 'T')).getTime()
}
if (formData.updateTime && typeof formData.updateTime === 'string') {
formData.updateTime = new Date(formData.updateTime.replace(' ', 'T')).getTime()
}
modalVisible.value = true
}
// 提交
async function handleSubmit() {
await formRef.value?.validate()
try {
const submitData = { ...formData } as QualityTesting
if (typeof submitData.inspectTime === 'number') {
submitData.inspectTime = new Date(submitData.inspectTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.createTime === 'number') {
submitData.createTime = new Date(submitData.createTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (typeof submitData.updateTime === 'number') {
submitData.updateTime = new Date(submitData.updateTime).toISOString().slice(0, 19).replace('T', ' ')
}
if (submitData.id) {
await qualityTestingApi.update(submitData)
message.success('修改成功')
} else {
await qualityTestingApi.create(submitData)
message.success('新增成功')
}
modalVisible.value = false
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
// 删除
function handleDelete(row: QualityTesting) {
dialog.warning({
title: '提示',
content: '确定要删除该记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await qualityTestingApi.delete([row.id!])
message.success('删除成功')
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
})
}
// 批量删除
function handleBatchDelete() {
dialog.warning({
title: '提示',
content: `确定要删除选中的 ${selectedIds.value.length} 条记录吗?`,
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await qualityTestingApi.delete(selectedIds.value)
message.success('删除成功')
selectedIds.value = []
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
})
}
// 导出
async function handleExport() {
try {
const params: Record<string, any> = {}
if (selectedIds.value.length > 0) params.ids = selectedIds.value
if (searchForm.id != null) params.id = searchForm.id
if (searchForm.status != null) params.status = searchForm.status
const blob = await qualityTestingApi.export(params)
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '质检单表数据.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// 错误已在拦截器处理
}
}
// 下载导入模板
async function handleDownloadTemplate() {
try {
const blob = await qualityTestingApi.downloadTemplate()
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = '质检单表导入模板.xlsx'
link.click()
window.URL.revokeObjectURL(url)
} catch (error) {
// 错误已在拦截器处理
}
}
// 导入上传
async function handleImportUpload({ file }: UploadCustomRequestOptions) {
if (!file.file) return
try {
const result = await qualityTestingApi.importData(file.file)
if (result.fail > 0) {
dialog.warning({
title: '导入结果',
content: `成功: ${result.success} 条,失败: ${result.fail}\n错误信息: ${(result.errors || []).join('\n') || '无'}`,
positiveText: '确定'
})
} else {
message.success(`导入成功,共 ${result.success} 条数据`)
importModalVisible.value = false
}
loadData()
} catch (error) {
// 错误已在拦截器处理
}
}
// 加载字典选项
async function loadDictOptions() {
try {
const data = await dictDataApi.listByType('sys_status')
statusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
} catch {}
try {
const data = await dictDataApi.listByType('qc_status')
qcStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {}
try {
const data = await dictDataApi.listByType('source_type')
qcSourceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {}
try {
const data = await dictDataApi.listByType('qc_trace_type')
qcTraceTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {}
try {
const data = await dictDataApi.listByType('qc_result_type')
qcResultTypeOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {}
try {
const data = await dictDataApi.listByType('qc_action_status')
qcActionStatusOptions.value = data.map(d => ({ label: d.dictLabel, value: (Number(d.dictValue) || d.dictValue),class: d.listClass }))
}catch {}
}
//查看质检单详情
async function handleDetail(row: QualityTesting) {
detailTitle.value = '查看质检单详情'
detailVisible.value = true
const data = await qualityTestingApi.detail(row.id!)
// // 映射后的数据
// mappedInfoList.value = Object.entries(data.info)
// .filter(([key]) => infoDataMap.has(key)) // 只保留配置里有的字段
// .map(([key, value]) => ({
// label: infoDataMap.get(key) || key, // 中文标签
// key, // 原字段名
// value // 原始值
// }))
setMappedInfoList(data.info)
detailTableData.value = data.detail
}
// 组装列表时按上面自定义顺序循环
function setMappedInfoList(rawData: Record<string, any>) {
mappedInfoList.value = customOrderKeys.map(key => ({
key,
label: infoDataMapping[key as keyof typeof infoDataMapping],
value: rawData[key]
}))
}
function formatDisplayValue(key: string, value: any): string {
if (value === null || value === undefined) return '-'
// 根据字段类型格式化
switch (key) {
case 'sourceType': {
const sourceOpt = qcSourceTypeOptions.value.find(o => o.value === value || String(o.value) === String(value))
return sourceOpt ? sourceOpt.label : String(value)
}
case 'status': {
const statusOpt = qcStatusOptions.value.find(o => o.value === value || String(o.value) === String(value))
return statusOpt ? statusOpt.label : String(value)
}
case 'traceType': {
const traceOpt = qcTraceTypeOptions.value.find(o => o.value === value || String(o.value) === String(value))
return traceOpt ? traceOpt.label : String(value)
}
case 'inspectTime':
case 'createTime':
case 'updateTime':
return value ? new Date(value).toLocaleString('zh-CN') : '-'
default:
return String(value)
}
}
//加载派工单
async function loadAssignWorkOrder() {
const data = await assingReworkApi.allList()
console.log(data)
assingWork.value = data.map(d => ({ label: d.assingCode, value: (Number(d.id) || d.id) }))
console.log(assingWork)
}
//提交判定明细
function submitResultDetail(row) {
Object.assign(formDetailOKData, reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 }))
Object.assign(formDetailScrapData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 }))
Object.assign(formDetailReworkData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 }))
Object.assign(formDetailConcessionData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'让步接收',actionStatus:1 }))
if (row.traceType != 1) {
traceCodeShow.value = true
}
submitFlag.value = true
submitDetailModalVisible.value = true
formDetailOKData.qcId = row.id
formDetailScrapData.qcId = row.id
formDetailReworkData.qcId = row.id
formDetailConcessionData.qcId = row.id
}
async function handleSubmitResultDetail() {
formDetailConcessionData.files = uploadFiles.value
const submitParam = [
formDetailOKData,
formDetailScrapData,
formDetailReworkData,
formDetailConcessionData]
console.log(submitParam);
submitFlag.value? await qualityTestingApi.submit(submitParam) : await qualityTestingApi.submitBF(submitParam)
message.success('提交成功')
submitDetailModalVisible.value = false
loadData()
}
//提交部分判定明细
function submitBFResultDetail(row) {
Object.assign(formDetailOKData, reactive<QcResultDetail>({ ...defaultFormDetailData, resultType: 1,handleAction:'下一道工序',actionStatus:1 }))
Object.assign(formDetailScrapData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 2,handleAction:'报废次品',actionStatus:1 }))
Object.assign(formDetailReworkData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 3,handleAction:'返工',actionStatus:1 }))
Object.assign(formDetailConcessionData, reactive<QcResultDetail>({ ...defaultFormDetailData,resultType: 4,handleAction:'让步接收',actionStatus:1 }))
if (row.traceType != 1) {
traceCodeShow.value = true
}
submitFlag.value = false
submitDetailModalVisible.value = true
formDetailOKData.qcId = row.id
formDetailScrapData.qcId = row.id
formDetailReworkData.qcId = row.id
formDetailConcessionData.qcId = row.id
}
//查询判定明细
function queryResultDetail(row) {
let path = `detail/${row.id}`;
router.push(path)
}
//质检撤回
function handleCancelQuality(row) {
dialog.warning({
title: '提示',
content: '确定要撤回ID:'+row.id+'质检记录吗?',
positiveText: '确定',
negativeText: '取消',
onPositiveClick: async () => {
try {
await qualityTestingApi.cancel(row.id)
message.success('撤回成功')
loadData()
} catch (error) {
console.log("撤回失败"+error)
}
}
})
}
//上传附件
async function handleUploadChange ({ file, fileList: list }: any) {
// 允许的后缀白名单
const allowExt = ['pdf','doc','docx','xls','xlsx','jpg','jpeg','png','gif','bmp']
const invalidFiles:any[] = []
list.forEach(item=>{
const name = item.file.name.toLowerCase()
const ext = name.split('.').pop()
if(!allowExt.includes(ext)){
invalidFiles.push(item.file.name)
item.status = 'error'
}
})
if(invalidFiles.length){
window.$message.error(`${invalidFiles.join('、')}】文件格式不支持`)
}
fileList.value = list
console.log('选中的文件列表', fileList.value)
// ✅关键只筛选【pending等待上传】的文件success/error/removed全部跳过不再重复上传
const validList = list.filter(item =>
item.status === 'pending' && item.file && item.status !== 'error'
)
const successIds = list.filter(x=>x.status==='success' && x.file).map(x=>x.id)
// 只保留页面还存在的附件信息
uploadFiles.value = uploadFiles.value.filter(item=> successIds.includes(item.uid))
// 只上传pending新文件
const pendingList = list.filter(item => item.status === 'pending' && item.file && item.status !== 'error')
if(pendingList.length === 0) return
const uploadPromises = validList.map(async (item: any) => {
try {
const res = await fileApi.upload(item.file, null, null)
uploadFiles.value.push(res)
const targetFile = fileList.value.find(f => f.id === item.id)
if (targetFile) {
targetFile.status = 'success'
targetFile.percentage = 100
}
} catch (error) {
console.error('文件上传失败', error)
const targetFile = fileList.value.find(f => f.id === item.id)
if (targetFile) targetFile.status = 'error'
}
})
await Promise.all(uploadPromises)
}
onMounted(() => {
loadData()
loadDictOptions()
loadAssignWorkOrder()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
</style>