feat(quality): 新增质量管理-报告管理模块
- 数据库:新增 `qms_inspection_report` 质检报告表及配套 SQL 脚本。
- 后端:新增报告管理的实体类、Mapper、Service 及 Controller,实现列表查询、新增、修改与状态注销(软删除)接口,查询按到期时间正序排列。
- 前端:在报告分析下新增“报告管理”页面 (`inspectionReport`)。
- 采用 PIMTable 规范构建列表页,新增“到期前30天预警”的数据标红高亮功能。
- 新增表单弹窗,实现填写“报告日期”和“有效期(月)”后自动通过 dayjs 推算“到期时间”的联动逻辑。
- 接入标准多文件上传组件,规范化核心资产报告的管理流程。
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from '@/utils/request' |
| | | |
| | | // æ¥è¯¢æ¥åå表 |
| | | export function listInspectionReport(query) { |
| | | return request({ |
| | | url: '/quality/inspectionReport/listPage', |
| | | method: 'get', |
| | | params: query |
| | | }) |
| | | } |
| | | |
| | | // æ¥è¯¢æ¥åè¯¦ç» |
| | | export function getInspectionReport(id) { |
| | | return request({ |
| | | url: '/quality/inspectionReport/' + id, |
| | | method: 'get' |
| | | }) |
| | | } |
| | | |
| | | // æ°å¢æ¥å |
| | | export function addInspectionReport(data) { |
| | | return request({ |
| | | url: '/quality/inspectionReport', |
| | | method: 'post', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // ä¿®æ¹æ¥å |
| | | export function updateInspectionReport(data) { |
| | | return request({ |
| | | url: '/quality/inspectionReport', |
| | | method: 'put', |
| | | data: data |
| | | }) |
| | | } |
| | | |
| | | // å 餿¥å |
| | | export function delInspectionReport(id) { |
| | | return request({ |
| | | url: '/quality/inspectionReport/' + id, |
| | | method: 'delete' |
| | | }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-dialog |
| | | :title="title" |
| | | v-model="visible" |
| | | width="600px" |
| | | append-to-body |
| | | :close-on-click-modal="false" |
| | | > |
| | | <el-form ref="formRef" :model="form" :rules="rules" label-width="120px"> |
| | | <el-form-item label="产ååç§°" prop="productName"> |
| | | <el-input v-model="form.productName" placeholder="请è¾å
¥äº§ååç§°" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="产ååå·" prop="productModel"> |
| | | <el-input v-model="form.productModel" placeholder="请è¾å
¥äº§ååå·" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="æ¥åç¼å·" prop="reportNo"> |
| | | <el-input v-model="form.reportNo" placeholder="请è¾å
¥æ¥åç¼å·" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="æ¥åç±»å" prop="reportType"> |
| | | <el-select v-model="form.reportType" placeholder="è¯·éæ©æ¥åç±»å" style="width: 100%"> |
| | | <el-option label="åå¼è¯éªæ¥å" value="åå¼è¯éªæ¥å" /> |
| | | <el-option label="åºåæ£éªæ¥å" value="åºåæ£éªæ¥å" /> |
| | | <el-option label="å§ææ£éªæ¥å" value="å§ææ£éªæ¥å" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="æ£æµåä½" prop="testingAgency"> |
| | | <el-input v-model="form.testingAgency" placeholder="请è¾å
¥æ£æµåä½" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="æ¥åæ¥æ" prop="reportDate"> |
| | | <el-date-picker |
| | | v-model="form.reportDate" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©æ¥åæ¥æ" |
| | | style="width: 100%" |
| | | @change="calculateExpiration" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="æææ(æ)" prop="validityPeriod"> |
| | | <el-input-number |
| | | v-model="form.validityPeriod" |
| | | :min="1" |
| | | :max="120" |
| | | controls-position="right" |
| | | style="width: 100%" |
| | | placeholder="请è¾å
¥æææ(æ)" |
| | | @change="calculateExpiration" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="å°ææ¶é´" prop="expirationTime"> |
| | | <el-date-picker |
| | | v-model="form.expirationTime" |
| | | type="date" |
| | | value-format="YYYY-MM-DD" |
| | | disabled |
| | | placeholder="ç³»ç»èªå¨è®¡ç®" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="夿³¨" prop="remark"> |
| | | <el-input v-model="form.remark" type="textarea" placeholder="请è¾å
¥å¤æ³¨å
容" /> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="éä»¶" prop="attachmentUrls"> |
| | | <!-- è¿éä½¿ç¨æ®éçel-uploadå ä½ï¼å®é
项ç®ä¸å¯æ¿æ¢ä¸ºå°è£
ç FileUpload --> |
| | | <FileUpload v-model="form.attachmentUrls" :limit="5" :fileSize="50" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">ç¡® å®</el-button> |
| | | <el-button @click="cancel">å æ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, toRefs, getCurrentInstance } from "vue"; |
| | | import { addInspectionReport, updateInspectionReport, getInspectionReport } from "@/api/reportAnalysis/inspectionReport.js"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | const emits = defineEmits(["close"]); |
| | | |
| | | const visible = ref(false); |
| | | const title = ref(""); |
| | | const formRef = ref(null); |
| | | |
| | | const data = reactive({ |
| | | form: {}, |
| | | rules: { |
| | | productName: [{ required: true, message: "产ååç§°ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | reportNo: [{ required: true, message: "æ¥åç¼å·ä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | reportDate: [{ required: true, message: "æ¥åæ¥æä¸è½ä¸ºç©º", trigger: "change" }], |
| | | validityPeriod: [{ required: true, message: "æææä¸è½ä¸ºç©º", trigger: "blur" }], |
| | | expirationTime: [{ required: true, message: "å°ææ¶é´ä¸è½ä¸ºç©º", trigger: "change" }], |
| | | } |
| | | }); |
| | | |
| | | const { form, rules } = toRefs(data); |
| | | |
| | | // èªå¨è®¡ç®å°ææ¶é´ |
| | | const calculateExpiration = () => { |
| | | if (form.value.reportDate && form.value.validityPeriod) { |
| | | form.value.expirationTime = dayjs(form.value.reportDate) |
| | | .add(form.value.validityPeriod, 'month') |
| | | .format('YYYY-MM-DD'); |
| | | } else { |
| | | form.value.expirationTime = null; |
| | | } |
| | | }; |
| | | |
| | | const openDialog = (type, row) => { |
| | | reset(); |
| | | if (type === "add") { |
| | | title.value = "æ°å¢æ¥å"; |
| | | visible.value = true; |
| | | } else { |
| | | title.value = "ä¿®æ¹æ¥å"; |
| | | getInspectionReport(row.id).then(response => { |
| | | form.value = response.data; |
| | | visible.value = true; |
| | | }); |
| | | } |
| | | }; |
| | | |
| | | const reset = () => { |
| | | form.value = { |
| | | id: null, |
| | | productName: null, |
| | | productModel: null, |
| | | reportNo: null, |
| | | reportDate: null, |
| | | validityPeriod: 12, // é»è®¤ä¸å¹´ |
| | | expirationTime: null, |
| | | testingAgency: null, |
| | | reportType: null, |
| | | attachmentUrls: null, |
| | | remark: null |
| | | }; |
| | | if (formRef.value) { |
| | | formRef.value.resetFields(); |
| | | } |
| | | }; |
| | | |
| | | const submitForm = () => { |
| | | formRef.value.validate(valid => { |
| | | if (valid) { |
| | | if (form.value.id != null) { |
| | | updateInspectionReport(form.value).then(() => { |
| | | proxy.$modal.msgSuccess("ä¿®æ¹æå"); |
| | | visible.value = false; |
| | | emits("close"); |
| | | }); |
| | | } else { |
| | | addInspectionReport(form.value).then(() => { |
| | | proxy.$modal.msgSuccess("æ°å¢æå"); |
| | | visible.value = false; |
| | | emits("close"); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | const cancel = () => { |
| | | visible.value = false; |
| | | reset(); |
| | | }; |
| | | |
| | | defineExpose({ |
| | | openDialog |
| | | }); |
| | | </script> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <div class="search_form"> |
| | | <el-form :model="searchForm" inline style="margin-bottom: 0;"> |
| | | <el-form-item label="产ååç§°ï¼"> |
| | | <el-input |
| | | v-model="searchForm.productName" |
| | | style="width: 200px" |
| | | placeholder="请è¾å
¥äº§ååç§°" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥åç¼å·ï¼"> |
| | | <el-input |
| | | v-model="searchForm.reportNo" |
| | | style="width: 200px" |
| | | placeholder="请è¾å
¥æ¥åç¼å·" |
| | | @change="handleQuery" |
| | | clearable |
| | | :prefix-icon="Search" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥åç±»åï¼"> |
| | | <el-select v-model="searchForm.reportType" clearable style="width: 200px" @change="handleQuery" placeholder="è¯·éæ©"> |
| | | <!-- 临æ¶åå
¸æ°æ®ï¼åç»å¯æ¹ä¸ºä»åå
¸æ¥å£è·å --> |
| | | <el-option label="åå¼è¯éªæ¥å" value="åå¼è¯éªæ¥å" /> |
| | | <el-option label="åºåæ£éªæ¥å" value="åºåæ£éªæ¥å" /> |
| | | <el-option label="å§ææ£éªæ¥å" value="å§ææ£éªæ¥å" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleQuery">æç´¢</el-button> |
| | | <el-button @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | |
| | | <div class="mb20" style="text-align: right;"> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">注éä½åº</el-button> |
| | | </div> |
| | | |
| | | <div class="table_list"> |
| | | <PIMTable |
| | | rowKey="id" |
| | | :column="tableColumn" |
| | | :tableData="tableData" |
| | | :page="page" |
| | | :isSelection="true" |
| | | @selection-change="handleSelectionChange" |
| | | :tableLoading="tableLoading" |
| | | @pagination="pagination" |
| | | :total="page.total" |
| | | ></PIMTable> |
| | | </div> |
| | | |
| | | <FormDia ref="formDia" @close="handleQuery"></FormDia> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { Search } from "@element-plus/icons-vue"; |
| | | import { onMounted, ref, reactive, toRefs, nextTick, getCurrentInstance } from "vue"; |
| | | import FormDia from "./components/formDia.vue"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { listInspectionReport, delInspectionReport } from "@/api/reportAnalysis/inspectionReport.js"; |
| | | import dayjs from "dayjs"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const data = reactive({ |
| | | searchForm: { |
| | | productName: "", |
| | | reportNo: "", |
| | | reportType: "", |
| | | }, |
| | | }); |
| | | const { searchForm } = toRefs(data); |
| | | |
| | | const tableColumn = ref([ |
| | | { |
| | | label: "产ååç§°", |
| | | prop: "productName", |
| | | }, |
| | | { |
| | | label: "产ååå·", |
| | | prop: "productModel", |
| | | }, |
| | | { |
| | | label: "æ¥åç¼å·", |
| | | prop: "reportNo", |
| | | }, |
| | | { |
| | | label: "æ¥åæ¥æ", |
| | | prop: "reportDate", |
| | | width: 120 |
| | | }, |
| | | { |
| | | label: "æ¥åç±»å", |
| | | prop: "reportType", |
| | | }, |
| | | { |
| | | label: "æ£æµåä½", |
| | | prop: "testingAgency", |
| | | }, |
| | | { |
| | | label: "å°ææ¶é´", |
| | | prop: "expirationTime", |
| | | dataType: "tag", |
| | | width: 120, |
| | | formatData: (val) => val, // åæ ·è¿åæ¶é´ |
| | | formatType: (val) => { |
| | | // é¢è¦é»è¾ï¼å°äºçäº30天尿ï¼è¿å danger æ 红 |
| | | if (!val) return 'info'; |
| | | const days = dayjs(val).diff(dayjs(), 'day'); |
| | | if (days <= 30 && days >= 0) { |
| | | return "danger"; |
| | | } else if (days < 0) { |
| | | return "danger"; // å·²è¿æ |
| | | } |
| | | return "success"; |
| | | } |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: "right", |
| | | width: 120, |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | } |
| | | } |
| | | ] |
| | | } |
| | | ]); |
| | | |
| | | const tableData = ref([]); |
| | | const selectedRows = ref([]); |
| | | const tableLoading = ref(false); |
| | | const page = reactive({ |
| | | current: 1, |
| | | size: 10, |
| | | total: 0 |
| | | }); |
| | | const formDia = ref(); |
| | | |
| | | // æ¥è¯¢å表 |
| | | const handleQuery = () => { |
| | | page.current = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | const resetQuery = () => { |
| | | searchForm.value = { |
| | | productName: "", |
| | | reportNo: "", |
| | | reportType: "", |
| | | }; |
| | | handleQuery(); |
| | | }; |
| | | |
| | | const pagination = (obj) => { |
| | | page.current = obj.page; |
| | | page.size = obj.limit; |
| | | getList(); |
| | | }; |
| | | |
| | | const getList = () => { |
| | | tableLoading.value = true; |
| | | const params = { ...searchForm.value, current: page.current, size: page.size }; |
| | | |
| | | listInspectionReport(params).then(res => { |
| | | tableLoading.value = false; |
| | | // PIMTable expected formats usually res.data.records or similar, if it uses IPage |
| | | if(res.data && res.data.records) { |
| | | tableData.value = res.data.records; |
| | | page.total = res.data.total; |
| | | } else { |
| | | // Fallback standard structure |
| | | tableData.value = res.data; |
| | | page.total = res.total || 0; |
| | | } |
| | | }).catch(err => { |
| | | tableLoading.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // è¡¨æ ¼éæ©æ°æ® |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection; |
| | | }; |
| | | |
| | | // æå¼å¼¹æ¡ |
| | | const openForm = (type, row) => { |
| | | nextTick(() => { |
| | | formDia.value?.openDialog(type, row); |
| | | }); |
| | | }; |
| | | |
| | | // å é¤(注éä½åº) |
| | | const handleDelete = () => { |
| | | if (selectedRows.value.length === 0) { |
| | | proxy.$modal.msgWarning("è¯·éæ©è¦æ³¨éçæ°æ®"); |
| | | return; |
| | | } |
| | | let ids = selectedRows.value.map((item) => item.id); |
| | | |
| | | ElMessageBox.confirm("éä¸çæ¥åå°è¢«æ³¨éä½åºï¼æ¯å¦ç¡®è®¤ï¼", "è¦å", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | delInspectionReport(ids.join(',')).then(() => { |
| | | proxy.$modal.msgSuccess("注éæå"); |
| | | getList(); |
| | | }); |
| | | }) |
| | | .catch(() => {}); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | </style> |