feat(order): 优化订单管理功能并增加设备绑定支持
- 修改生产日期字段支持清除和显示默认值
- 在标准库中增加设备绑定功能,支持模板与设备关联
- 新增价格字段用于检验项目定价管理
- 增强模板绑定界面,支持多选和设备分配操作
| | |
| | | # LIMS实验室管理系统/开发环境 |
| | | VUE_APP_BASE_API = '/dev-api' |
| | | |
| | | # 本地环境ip+端口 |
| | | VUE_APP_BASE_URL = 'http://127.0.0.1:8001/lims' |
| | | # 文件访问与普通接口统一走本地开发代理 |
| | | VUE_APP_BASE_URL = '/dev-api' |
| | | |
| | | # OnlyOffice 文档服务地址(预览 pdf 用,没有 http:// 前缀会当成相对路径) |
| | | VUE_APP_ONLYOFFICE_API = 'http://192.168.21.53:9001' |
| | |
| | | return request({ url: "/standardTree/saveTemplateBindings", method: "post", data }); |
| | | } |
| | | |
| | | // 查询可绑定到原始记录模板的设备 |
| | | export function searchDevices(query) { |
| | | return request({ |
| | | url: "/deviceScope/search", |
| | | method: "get", |
| | | params: query, |
| | | }); |
| | | } |
| | | |
| | | // 获取标准方法枚举 |
| | | export function selectStandardMethods(query) { |
| | | return request({ |
| | |
| | | <!--onlyoffice 编辑器--> |
| | | <template> |
| | | <div id="vabOnlyOffice"></div> |
| | | <div class="document-preview"> |
| | | <iframe |
| | | v-if="isPdf" |
| | | :key="option.url" |
| | | :src="option.url" |
| | | class="native-pdf-preview" |
| | | title="PDF预览" |
| | | ></iframe> |
| | | <div v-else id="vabOnlyOffice"></div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | |
| | | } |
| | | }; |
| | | }, |
| | | computed: { |
| | | isPdf() { |
| | | return this.isPdfFile(this.option); |
| | | } |
| | | }, |
| | | created() { |
| | | if(this.options){ |
| | | const option = this.options |
| | |
| | | }, |
| | | async setEditor(option) { |
| | | this.destroyEditor(); |
| | | // 没有文档地址就不初始化编辑器,避免访问尚未加载的 DocsAPI |
| | | if (!option || !option.url) { |
| | | // PDF 使用浏览器原生预览;其他文档才初始化 OnlyOffice。 |
| | | if (!option || !option.url || this.isPdfFile(option)) { |
| | | return; |
| | | } |
| | | // 等待 v-else 中的 OnlyOffice 容器完成渲染。 |
| | | await this.$nextTick(); |
| | | this.doctype = this.getFileType(option.fileType); |
| | | let config = { |
| | | document: { |
| | |
| | | return; |
| | | } |
| | | this.docEditor = new window.DocsAPI.DocEditor("vabOnlyOffice", config); |
| | | }, |
| | | isPdfFile(option) { |
| | | if (!option) { |
| | | return false; |
| | | } |
| | | const fileType = String(option.fileType || "").toLowerCase(); |
| | | const url = String(option.url || "").split(/[?#]/)[0].toLowerCase(); |
| | | return fileType === "pdf" || url.endsWith(".pdf"); |
| | | }, |
| | | getFileType(fileType) { |
| | | let docType = ""; |
| | |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .document-preview, |
| | | .native-pdf-preview, |
| | | #vabOnlyOffice { |
| | | width: 100%; |
| | | height: 100%; |
| | | } |
| | | .native-pdf-preview { |
| | | display: block; |
| | | border: 0; |
| | | } |
| | | html, |
| | | body { |
| | | height: 100%; |
| | |
| | | Vue.prototype.HaveJson = (val) => { |
| | | return JSON.parse(JSON.stringify(val)); |
| | | }; |
| | | // Vue.prototype.javaApi = process.env.NODE_ENV === "production"?"http://192.168.21.53:8001/lims":"http://127.0.0.1:8001/lims"; |
| | | // Vue.prototype.javaApi = process.env.VUE_APP_BASE_URL && process.env.NODE_ENV==='production'?process.env.VUE_APP_BASE_URL : "http://127.0.0.1:8001/lims"; |
| | | Vue.prototype.javaApi = |
| | | process.env.VUE_APP_BASE_URL || process.env.VUE_APP_BASE_API; |
| | | const apiBase = process.env.VUE_APP_BASE_URL || process.env.VUE_APP_BASE_API; |
| | | Vue.prototype.javaApi = /^https?:\/\//i.test(apiBase) |
| | | ? apiBase.replace(/\/$/, "") |
| | | : window.location.origin + "/" + apiBase.replace(/^\/+|\/+$/g, ""); |
| | | Vue.prototype.checkPermi = checkPermi; |
| | | Vue.prototype.uploadHeader = { |
| | | Authorization: "Bearer " + getToken(), |
| | |
| | | return { |
| | | tableData: [], |
| | | column: [ |
| | | { label: '委托编号', prop: 'entrustCode', width: 160 }, |
| | | {label: '检验项分类', prop: 'inspectionItemClass', width: 160}, |
| | | {label: '检验项', prop: 'inspectionItem', width: 160}, |
| | | {label: '检验子项', prop: 'inspectionItemSubclass', width: 160}, |
| | | { label: '收费标准(元/次)', prop: 'rates' }, |
| | | { label: '分组系数', prop: 'manHourGroup' }, |
| | | { label: '检验项目', prop: 'number', width: 160 }, |
| | | { label: '模板名称', prop: 'name', width: 200 }, |
| | | { label: '价格(元)', prop: 'price' }, |
| | | ], |
| | | viewInfoDia: false, |
| | | tableLoading: false, |
| | |
| | | <span class="info">{{ item.partDesc }}</span> |
| | | </div> |
| | | <div class="item"> |
| | | <span class="full-title">委托日期</span>: |
| | | <span class="full-title">送样日期</span>: |
| | | <span class="info">{{ item.sendTime }}</span> |
| | | </div> |
| | | <div class="item"> |
| | |
| | | <span class="info4">{{ item.partDesc }}</span> |
| | | </div> |
| | | <div class="item"> |
| | | <span class="full-title2">委托日期</span>: |
| | | <span class="full-title2">送样日期</span>: |
| | | <span class="info">{{ item.sendTime }}</span> |
| | | </div> |
| | | <div class="item"> |
| | |
| | | <el-col :span="6"> |
| | | <el-form-item label="生产日期"> |
| | | <el-date-picker v-model="addObj.productionDate" type="date" value-format="yyyy-MM-dd" |
| | | :disabled="active>1" size="small"/> |
| | | :disabled="active>1" clearable placeholder="不选择时报告显示/" size="small"/> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | |
| | | quarterItemId: '', |
| | | sampleView: '', // 样品名称(报告展示字段) |
| | | sampleViewEn: '', // 样品名称英文(报告展示字段) |
| | | productionDate: null, |
| | | sampleDate: null, |
| | | }, |
| | | addObjRules: { // 表单校验规则 |
| | | orderType: [ |
| | |
| | | }, |
| | | saveData(sampleList) { |
| | | this.saveLoad = true |
| | | const insOrder = { |
| | | ...this.addObj, |
| | | productionDate: this.addObj.productionDate === '/' || !this.addObj.productionDate |
| | | ? null : this.addObj.productionDate, |
| | | typeSource: -1, |
| | | } |
| | | if (this.tabIndex == 4 && this.active == 2) { |
| | | // 退回后提交 |
| | | updateInsOrder({insOrder: this.addObj, sampleProduct: sampleList}).then(res => { |
| | | updateInsOrder({insOrder, sampleProduct: sampleList}).then(res => { |
| | | this.saveLoad = false |
| | | this.$message.success('已提交') |
| | | this.bsm3Dia = false; |
| | |
| | | }) |
| | | } else { |
| | | // 常规提交 |
| | | const newJuOrder = {...this.addObj, typeSource: -1} |
| | | const newJuOrder = {...insOrder} |
| | | delete newJuOrder.temperature |
| | | delete newJuOrder.humidity |
| | | const payload = this.addObj.standardTreeId |
| | |
| | | sampleCode: this.sampleCode, |
| | | templateList: this.boundTemplateList.map((item, index) => ({templateId: item.templateId, sort: index})) |
| | | } |
| | | : {insOrder: {...this.addObj, typeSource: -1}, sampleList: sampleList} |
| | | : {insOrder, sampleList: sampleList} |
| | | addInsOrder(payload).then(res => { |
| | | this.saveLoad = false |
| | | this.$message.success('已提交') |
| | |
| | | <el-descriptions-item label="样品编号">{{ sampleCode || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="样品外观">{{ addObj.sampleStatus || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="送样人">{{ addObj.prepareUser || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="生产日期">{{ addObj.productionDate || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="生产日期">{{ addObj.productionDate || '/' }}</el-descriptions-item> |
| | | <el-descriptions-item label="生产批号">{{ addObj.productionBatch || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="送样日期">{{ addObj.sampleDate || '-' }}</el-descriptions-item> |
| | | <el-descriptions-item label="联系方式">{{ addObj.phone || '-' }}</el-descriptions-item> |
| | |
| | | <span class="info">{{ item.sampleModel }}</span> |
| | | </div> |
| | | <div class="item"> |
| | | <span class="full-title">委托日期</span>: |
| | | <span class="full-title">送样日期</span>: |
| | | <span class="info">{{ item.sendTime }}</span> |
| | | </div> |
| | | <div class="item"> |
| | |
| | | <span class="info4">{{ item.sampleModel }}</span> |
| | | </div> |
| | | <div class="item"> |
| | | <span class="full-title2">委托日期</span>: |
| | | <span class="full-title2">送样日期</span>: |
| | | <span class="info">{{ item.sendTime }}</span> |
| | | </div> |
| | | <div class="item"> |
| | |
| | | <el-dialog :before-close="closeCopyTem" :close-on-click-modal="false" :close-on-press-escape="false" |
| | | :visible.sync="isShowCopyTem" :title="title" width="35%" :modal-append-to-body="false"> |
| | | <el-form ref="copyForm" :model="copyForm" :rules="copyFormRules" label-position="right" label-width="80px"> |
| | | <el-form-item label="模版编号" prop="number"> |
| | | <el-form-item label="检验项目" prop="number"> |
| | | <el-input v-model="copyForm.number" clearable size="small"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label="模版名称" prop="name"> |
| | |
| | | </el-form-item> |
| | | <el-form-item label="备注" prop="remark"> |
| | | <el-input v-model="copyForm.remark" clearable size="small"></el-input> |
| | | </el-form-item> |
| | | <el-form-item label="价格(元)" prop="price"> |
| | | <el-input v-model="copyForm.price" clearable size="small"></el-input> |
| | | </el-form-item> |
| | | </el-form> |
| | | <span slot="footer" class="dialog-footer"> |
| | |
| | | number: "", |
| | | name: "", |
| | | remark: "", |
| | | price: "", |
| | | id: "", |
| | | }, |
| | | copyFormRules: { |
| | |
| | | }, |
| | | tableData: [], |
| | | column: [ |
| | | { label: "模板编号", prop: "number" }, |
| | | { label: "检验项目", prop: "number" }, |
| | | { label: "模板名称", prop: "name" }, |
| | | { label: "备注", prop: "remark" }, |
| | | { label: "价格(元)", prop: "price" }, |
| | | { label: "创建用户", prop: "createUserName" }, |
| | | { label: "创建时间", prop: "createTime", width: "160" }, |
| | | { label: "更新用户", prop: "updateUserName" }, |
| | |
| | | name: this.copyForm.name, |
| | | number: this.copyForm.number, |
| | | remark: this.copyForm.remark, |
| | | price: this.copyForm.price === "" || this.copyForm.price == null ? null : this.copyForm.price, |
| | | }; |
| | | switch (this.title) { |
| | | case "新增": |
| | |
| | | this.isShowCopyTem = false; |
| | | this.submitCopyInfoLoading = false; |
| | | this.$message.success("新增成功"); |
| | | this.refreshTable("page"); |
| | | this.getList(); |
| | | }) |
| | | .catch((err) => { |
| | | console.log("copyTemplate----", err); |
| | |
| | | <p v-if="!templateBindings.length" class="binding-tip">请为当前标准绑定原始记录模板。</p> |
| | | <el-table :data="templateBindings" border size="small"> |
| | | <el-table-column type="index" label="顺序" width="70" /><el-table-column label="模板编号" prop="number" width="180" /><el-table-column label="模板名称" prop="name" /><el-table-column label="检测依据(模板备注)" prop="remark" /> |
| | | <el-table-column label="设备绑定" min-width="360"> |
| | | <template slot-scope="scope"> |
| | | <el-select v-model="scope.row.deviceIds" |
| | | :disabled="!checkPermi(['standard:standardLibrary:upStandardProduct'])" |
| | | multiple filterable clearable size="small" |
| | | placeholder="请选择该模板的默认设备" style="width:100%"> |
| | | <el-option v-for="device in deviceOptions" :key="device.id" |
| | | :label="device.label" :value="device.id" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="checkPermi(['standard:standardLibrary:upStandardProduct'])" label="操作" width="150"><template slot-scope="scope"><el-button type="text" :disabled="scope.$index === 0" @click="moveBinding(scope.$index, -1)">上移</el-button><el-button type="text" :disabled="scope.$index === templateBindings.length - 1" @click="moveBinding(scope.$index, 1)">下移</el-button><el-button type="text" @click="removeBinding(scope.$index)">移除</el-button></template></el-table-column> |
| | | </el-table> |
| | | <el-select v-if="checkPermi(['standard:standardLibrary:upStandardProduct'])" v-model="templateToBind" filterable placeholder="选择原始记录模板" size="small" style="width:320px;margin-top:16px" @change="addBinding"><el-option v-for="template in templateList.filter(item => !templateBindings.some(binding => binding.templateId === item.id))" :key="template.id" :label="template.number ? template.number + ' - ' + template.name : template.name" :value="template.id" /></el-select> |
| | | <el-select v-if="checkPermi(['standard:standardLibrary:upStandardProduct'])" v-model="templateToBind" multiple filterable placeholder="选择原始记录模板" size="small" style="width:320px;margin-top:16px"><el-option v-for="template in templateList.filter(item => !templateBindings.some(binding => binding.templateId === item.id))" :key="template.id" :label="template.number ? template.number + ' - ' + template.name : template.name" :value="template.id" /></el-select> |
| | | <el-button v-if="checkPermi(['standard:standardLibrary:upStandardProduct'])" type="primary" size="small" style="margin-top:16px;margin-left:8px" @click="confirmAddBindings">确认</el-button> |
| | | </el-card> |
| | | <el-empty v-else description="请选择二级标准,维护其原始记录模板" /> |
| | | <div v-if="false"> |
| | |
| | | getStandardTemplate, |
| | | selectTemplateBindings, |
| | | saveTemplateBindings, |
| | | searchDevices, |
| | | } from "@/api/standard/standardLibrary"; |
| | | import bindSupplierDensityDialogAsk from "./components/bindSupplierDensityDialogAsk.vue"; |
| | | import BatchCopy from "./components/BatchCopy.vue"; |
| | |
| | | sectionList: [], |
| | | templateList: [], |
| | | templateBindings: [], |
| | | templateToBind: null, |
| | | templateToBind: [], |
| | | deviceOptions: [], |
| | | total: 0, |
| | | currentPage: 1, |
| | | standardId: 0, |
| | |
| | | this.selectEnumByCategoryForSonLaboratory(); |
| | | this.selectEnumByCategoryForsampleType(); |
| | | this.getStandardTemplate(); |
| | | this.loadDeviceOptions(); |
| | | this.selectStandardMethodsSec(); |
| | | this.token = { |
| | | token: sessionStorage.getItem("token"), |
| | |
| | | this.templateList = res.data; |
| | | }); |
| | | }, |
| | | loadTemplateBindings(standardTreeId) { |
| | | selectTemplateBindings({ standardTreeId }).then((res) => { |
| | | this.templateBindings = res.data || []; |
| | | this.templateToBind = null; |
| | | loadDeviceOptions() { |
| | | searchDevices({ status: 0 }).then((res) => { |
| | | this.deviceOptions = (res.data || []).map((device) => ({ |
| | | id: device.id, |
| | | label: `${device.deviceName}--${device.managementNumber}`, |
| | | })); |
| | | }); |
| | | }, |
| | | addBinding(templateId) { |
| | | const template = this.templateList.find((item) => item.id === templateId); |
| | | if (template && !this.templateBindings.some((item) => item.templateId === templateId)) { |
| | | this.templateBindings.push({ ...template, templateId, sort: this.templateBindings.length }); |
| | | } |
| | | this.templateToBind = null; |
| | | loadTemplateBindings(standardTreeId) { |
| | | selectTemplateBindings({ standardTreeId }).then((res) => { |
| | | this.templateBindings = (res.data || []).map((item) => ({ |
| | | ...item, |
| | | deviceIds: Array.isArray(item.deviceIds) ? [...item.deviceIds] : [], |
| | | })); |
| | | this.templateToBind = []; |
| | | }); |
| | | }, |
| | | confirmAddBindings() { |
| | | const selectedIds = Array.isArray(this.templateToBind) ? this.templateToBind : []; |
| | | selectedIds.forEach((templateId) => { |
| | | const template = this.templateList.find((item) => item.id === templateId); |
| | | if (template && !this.templateBindings.some((item) => item.templateId === templateId)) { |
| | | this.templateBindings.push({ ...template, templateId, sort: this.templateBindings.length, deviceIds: [] }); |
| | | } |
| | | }); |
| | | this.templateToBind = []; |
| | | }, |
| | | removeBinding(index) { |
| | | this.templateBindings.splice(index, 1); |
| | |
| | | }, |
| | | saveBindings() { |
| | | saveTemplateBindings({ standardTreeId: this.selected.id, |
| | | templates: this.templateBindings.map((item, index) => ({ templateId: item.templateId, sort: index })) |
| | | templates: this.templateBindings.map((item, index) => ({ |
| | | templateId: item.templateId, |
| | | sort: index, |
| | | deviceIds: Array.isArray(item.deviceIds) ? [...item.deviceIds] : [], |
| | | })) |
| | | }).then(() => { |
| | | this.$message.success("模板绑定已保存"); |
| | | this.loadTemplateBindings(this.selected.id); |
| | |
| | | proxy: { |
| | | // detail: https://cli.vuejs.org/config/#devserver-proxy |
| | | [process.env.VUE_APP_BASE_API]: { |
| | | // target: `http://36.213.90.123:9015/lims`, |
| | | target: `http://192.168.21.53:9001/lims`, |
| | | // 本地开发统一访问本机后端,报告文件读取本机 wordUrl 目录。 |
| | | target: `http://127.0.0.1:9509/lims`, |
| | | changeOrigin: true, |
| | | pathRewrite: { |
| | | ["^" + process.env.VUE_APP_BASE_API]: "", |