mes-vue/src/views/biz/qualityItem/index.vue
shaoleiliu-netizen123 fa3a7599d0 保修前端代码
2026-07-31 09:51:41 +08:00

821 lines
24 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.name" placeholder="请输入质检项名称" clearable />
</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"
:row-key="(row) => row.id"
:scroll-x="1200"
v-model:checked-row-keys="selectedIds"
/>
<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"
@update:checked-row-keys="handleCheck"
>
<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="质检名称" path="name">
<n-input v-model:value="formData.name" placeholder="请输入质检项名称" />
</n-form-item>
<n-form-item label="质检规格" path="spec">
<n-input v-model:value="formData.spec" placeholder="请输入质检规格" />
</n-form-item>
<n-form-item label="质检方案" path="plans">
<n-upload
multiple
directory-dnd
auto-upload="false"
:max="5"
v-model:file-list="fileList"
@before-upload ="handleBeforeUpload"
@remove="handleRemove"
>
<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">
文件上传PDF或Word;文件大小20MB以内
</n-p>
</n-upload-dragger>
</n-upload>
</n-form-item>
<n-form-item label="质检描述" path="description">
<n-input v-model:value="formData.description" type="textarea" placeholder="请输入质检描述" />
</n-form-item>
<n-form-item label="状态" path="status">
<n-switch v-model:value="formData.status" :checked-value="1" :unchecked-value="0">
<template #checked>检查</template>
<template #unchecked>不检查</template>
</n-switch>
</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-drawer v-model:show="plansShow" :width="502">
<n-drawer-content :title="title" closable>
<n-virtual-list
ref="virtualListInst"
style="max-height: 240px"
:item-size="42"
:items="sysFileList"
>
<template #default="{ item, index }">
<div :key="item.key" class="item" style="height: 42px">
<n-grid x-gap="22">
<n-gi span="12">
<n-icon v-if="item.fileSuffix == '.pdf'"><DocumentOutline /> </n-icon>
<n-icon v-if="item.fileSuffix == '.docx'"><NewspaperOutline /> </n-icon>
{{ item.originalName }}
</n-gi>
<n-gi span="6">
{{ calculateSize(item.fileSize) }}
</n-gi>
<n-gi span="4">
<Button @click="downloadPlans(item)"><n-icon><DownloadOutline /></n-icon></Button>
</n-gi>
</n-grid>
</div>
</template>
</n-virtual-list>
</n-drawer-content>
</n-drawer>
<!--关联工序抽屉-->
<n-drawer v-model:show="processModel" :width="800" >
<n-drawer-content title="工序列表">
<!-- 全选栏 -->
<n-space justify="space-between" class="mb-3">
<n-checkbox v-model:checked="isAllCheck" @update:checked="handleAllCheck">
全选当前筛选工序
</n-checkbox>
<n-tag type="info">已勾选 {{ selectedProcessIds.length }} 道工序</n-tag>
</n-space>
<!-- 工序表格,带复选框 -->
<n-data-table
:columns="processColumns"
:data="processList"
max-height="700"
v-model:checked-row-keys="selectedProcessIds"
:row-key="rowKey"
:scroll-x="1200"
:loading="relateLoading"
@update:checked-row-keys="handleUpateCheck"
/>
<div class="pagination-container" style="display: flex; justify-content: flex-end; margin-top: 12px">
<n-pagination
v-model:page="relatePagination.page"
v-model:page-size="relatePagination.pageSize"
:item-count="relatePagination.itemCount"
:page-sizes="[10, 20, 50, 100]"
show-size-picker
show-quick-jumper
@update:page="handleRelatePageChange"
@update:page-size="handleRelatePageSizeChange"
>
<template #prefix>
共 {{ relatePagination.itemCount }} 条
</template>
</n-pagination>
</div>
<!-- 底部操作按钮 -->
<template #footer>
<n-space justify="end">
<n-button @click="processModel = false">取消</n-button>
</n-space>
</template>
</n-drawer-content>
</n-drawer>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, h, onMounted,computed } from 'vue'
import { NButton, NSpace, NIcon, NUpload, useMessage, useDialog, type DataTableColumns,
type UploadCustomRequestOptions,UploadFileInfo, NTag } from 'naive-ui'
import { SearchOutline, RefreshOutline, AddOutline
, CreateOutline, CloudUploadOutline,NewspaperOutline,DocumentOutline,
DownloadOutline,ArchiveOutline as ArchiveIcon } from '@vicons/ionicons5'
import { qcItemApi, type QcItem } from '@/api/qcItem'
import { fileApi,SysFile } from '@/api/system'
import Button from 'naive-ui/es/button/src/Button'
import { basicProcessPlanApi,BasicProcessPlan } from '@/api/basicProcessPlan'
import {qcitemProcessApi,QcitemProcess} from '@/api/qcitemProcess'
const message = useMessage()
const dialog = useDialog()
//关联工序
const processModel = ref<Boolean>(false)
const processList = ref<BasicProcessPlan[]>([])
const selectedProcessIds = ref<number[]>([])
const qcProcessRelate =ref<QcitemProcess> ({
qualityItemId: undefined,
processId: undefined,
})
const relateLoading = ref<boolean>(false)
const relatePagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
//上传文件
const fileList = ref<UploadFileInfo[]>([])
//质检方案抽屉显示隐藏
const title = ref<string>()
const plansShow = ref<Boolean>(false)
const sysFileList = ref<SysFile>([])
// 搜索表单
const searchForm = reactive({
name: '',
})
// 表格数据
const tableData = ref<QcItem[]>([])
const loading = ref(false)
const selectedIds = ref<number[]>([])
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
showSizePicker: true,
pageSizes: [10, 20, 50]
})
// 弹窗
const modalVisible = ref(false)
const modalTitle = ref('')
const importModalVisible = ref(false)
const formRef = ref()
const defaultFormData: QcItem = {
id:undefined,
name: '',
spec: '',
plans: '',
description: '',
createby: undefined,
status:0
}
const formData = reactive<QcItem>({ ...defaultFormData })
// 字典选项(下拉框/单选框/复选框关联字典时使用)
// 表单校验规则
const formRules = {
name: [{ required: true, message: '请输入质检项名称', trigger: 'blur' }],
spec: [{ required: true, message: '请输入质检项规格', trigger: 'blur' }],
}
// 表格列
const columns: DataTableColumns<QcItem> = [
{ type: 'selection' },
{ title: '质检项名称', key: 'name' },
{ title: '质检规格', key: 'spec' },
{ title: '质检方案', key: 'plans',
render(row) {
return h(NButton,{size:"small", onClick: () => handlePlans(row)},{
default: () => ['下载方案']
})
}
},
{ title: '质检描述', key: 'description' },
{ title: '状态', key: 'status',
render(row){
if(row.status == 0){
return h(NTag, {
type:'error',
size: 'small'
},
{
default: () => '不检查'
}
)
}
if(row.status == 1){
return h(NTag, {
type:'success',
size: 'small'
},
{
default: () => '检查'
}
)
}
}
},
{ title: '创建人', key: 'userName' },
{ title: '创建时间', key: 'createTime', width: 180 },
{ title: '修改时间', key: 'updateTime', 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(NButton, { size: 'small', quaternary: true, onClick: () => handleProccess(row) }, {
default: () => [' 关联工序']
})
])
}
}
]
// 加载数据
async function loadData() {
loading.value = true
try {
const res = await qcItemApi.page({
page: pagination.page,
pageSize: pagination.pageSize,
name: searchForm.name || undefined,
})
tableData.value = res.list
pagination.itemCount = res.total
} finally {
loading.value = false
}
}
// 搜索
function handleSearch() {
pagination.page = 1
loadData()
}
// 重置
function handleReset() {
searchForm.name = ''
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() {
fileList.value = []
modalTitle.value = '新增质检项表'
Object.assign(formData, defaultFormData)
modalVisible.value = true
}
// 编辑
async function handleEdit(row: QcItem) {
fileList.value = []
modalTitle.value = '编辑质检项表'
const res = await qcItemApi.getPlansFiles({ plans: row.plans })
const fileArr = res || []
fileList.value = fileArr.map(item => {
return {
id: item.id,
name: item.originalName,
file: null, // 回显已有文件无原生File对象填空
size: item.fileSize,
type: item.fileType === 'pdf'
? 'application/pdf'
: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
status: 'finished', // 固定为已完成
response: item, // 存储后端完整文件信息,下载时取用
url: item.url // 文件预览/下载地址
} as UploadFileInfo
})
Object.assign(formData, row)
formData.status = Number(row.status)
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() {
let plans = []
console.log(fileList.value);
fileList.value.forEach(item=>{
plans.push(item.url)
})
console.log();
formData.plans = plans.join(",")
console.log(formData);
await formRef.value?.validate()
try {
const submitData = { ...formData } as QcItem
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', ' ')
}
console.log(submitData)
// if (submitData.id) {
// await qcItemApi.update(submitData)
// message.success('修改成功')
// } else {
// await qcItemApi.create(submitData)
// message.success('新增成功')
// }
// modalVisible.value = false
// 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.name) params.name = searchForm.name
const blob = await qcItemApi.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 qcItemApi.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 qcItemApi.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() {
}
//文件上传前校验
const handleBeforeUpload = async (data: {
file: UploadFileInfo
fileList: UploadFileInfo[]
})=> {
const file = data.file
//文件格式校验
const allowMime = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
]
if(!allowMime.includes(file.type)){
message.error(`仅支持上传 PDF、Worddoc/docx格式文件当前文件${file.name}`)
return false // 拦截上传
}
//限制文件大小20MB
const maxSize = 20 * 1024 *1024
if(file.file.size > maxSize) {
message.error(`文件${file.name}超出20MB限制`)
return false;
}
try {
const res = await qcItemApi.uploadFile(file.file)
message.success(`${file.name} 上传成功`)
//保存url路径
file.url = res.url
return true
} catch (err) {
message.error(`${file.name} 上传失败`)
return false
}
}
//文件清除
const handleRemove = (data: {
file: UploadFileInfo
fileList: UploadFileInfo[]
}) =>{
fileList.value = fileList.value.filter(item => item.id !== data.file.id)
}
//显示抽屉
const handlePlans = async (row) => {
title.value = row.name + "质检方案"
plansShow.value = true
try {
// 打印入参,确认传给后端的 plans 是否有值
console.log('请求参数 plans', row.plans)
const res = await qcItemApi.getPlansFiles({ plans: row.plans })
sysFileList.value = res
console.log('接口返回成功数据:', res);
} catch (err) {
// 打印完整错误看是401/404/500/参数问题
console.error('请求失败详情:', err)
// 提取后端返回的提示信息
const msg = err || '获取文件列表接口异常'
message.error(msg)
}
}
//计算文件大小
function calculateSize(fileSize) {
if (fileSize === 0) return '0 B'
const unitArr = ['B', 'KB', 'MB', 'GB']
let size = fileSize
let index = 0
while (size >= 1024 && index < unitArr.length - 1) {
size /= 1024
index++
}
return size.toFixed(2) + ' ' + unitArr[index]
}
//下载文件
async function downloadPlans(row: any) {
try {
// res 现在是 axios完整响应 { data:Blob, headers }
const res = await fileApi.downloadPlanFile(row.id)
const blobData = res // 真实二进制Blob
let fileName = ''
let ext = ''
const nameArr = row.url.split('/')
const fullName = nameArr.pop()!
const extMatch = fullName.match(/\.(pdf|doc|docx)$/i)
if (extMatch) ext = extMatch[1].toLowerCase()
fileName = `${row.originalName }`
// 关键直接使用接口返回的Blob不要new Blob重复包装
const url = URL.createObjectURL(blobData)
const a = document.createElement('a')
a.href = url
a.download = fileName
document.body.appendChild(a)
a.click()
// 释放内存
URL.revokeObjectURL(url)
document.body.removeChild(a)
message.success(`${fileName} 下载成功`)
} catch (err) {
console.error('下载失败', err)
message.error('文件下载失败,请重试')
}
}
//关联工序抽屉
function handleProccess(row) {
relatePagination.page = 1
relatePagination.pageSize = 10
relateLoading.value = true
processModel.value = true
qcProcessRelate.qualityItemId = row.id
getProcessList(row.id)
}
async function getProcessList(id) {
try {
const res = await basicProcessPlanApi.list({
id:id,
page:relatePagination.page,
pageSize:relatePagination.pageSize
});
processList.value = res.records
relatePagination.itemCount = res.total
//回显
selectedProcessIds.value = processList.value.filter(item=> item.isBind).map(item => item.id)
relateLoading.value = false
} catch (error) {
console.log("异常信息:"+error);
message.error("获取关联异常")
relateLoading.value = false
}
}
//取消/全选
const isAllCheck = computed({
get() {
return processList.value.length > 0 && selectedProcessIds.value.length === processList.value.length
},
set(val) {
if(val) {
selectedProcessIds.value = processList.value.map(item=> item.id)
}else{
selectedProcessIds.value = []
}
loadCreateApi()
}
})
const handleAllCheck = (val:boolean) => {
isAllCheck.value = val
}
function rowKey(row: RowData) {
return row.id
}
const handleUpateCheck = async (keys: Array<string | number>,) =>{
selectedProcessIds.value = keys
loadCreateApi()
}
const loadCreateApi = async ()=>{
qcProcessRelate.processIds = selectedProcessIds.value
qcProcessRelate.page = relatePagination.page
qcProcessRelate.pageSize = relatePagination.pageSize
console.log(qcProcessRelate);
try{
await qcitemProcessApi.create(qcProcessRelate);
message.success("关联成功")
getProcessList(qcProcessRelate.qualityItemId)
}catch(error) {
message.error("关联失败")
}
}
const processColumns: DataTableColumns<BasicProcessPlan> = [
{ type: 'selection' },
{ title:"工序名称",key:"name"},
{ title: "关联状态",key:"isBind",
render(row) {
const val = row.isBind;
if (val) {
return h(NTag, { type: "success" }, { default: () => "已关联" });
} else {
return h(NTag, { type: "default" }, { default: () => "未关联" });
}
}
}
]
// 分页
function handleRelatePageChange(page: number) {
relatePagination.page = page
getProcessList(qcProcessRelate.qualityItemId)
}
function handleRelatePageSizeChange(pageSize: number) {
relatePagination.pageSize = pageSize
relatePagination.page = 1
getProcessList(qcProcessRelate.qualityItemId)
}
onMounted(() => {
loadData()
loadDictOptions()
})
</script>
<style scoped>
.search-form {
margin-bottom: 16px;
}
.table-toolbar {
margin-bottom: 16px;
}
.menu-tree-wrapper {
width: 100%;
max-height: 300px;
overflow-y: auto;
border: 1px solid #E5E7EB;
border-radius: 8px;
padding: 12px;
}
</style>