feat(LIMS): 样品/实验/数据接口三页面增强
样品管理:加「出库」按钮与操作列项、出库弹窗(数量/领用方/去向/事由/关联实验)、
详情抽屉(基本信息 + 出入库流水表 + 状态变更时间轴)。
实验管理:加「分配资源」按钮与操作列项、详情抽屉(基本信息 + 进度条 + 流程节点时间轴
可标记完成 + 已分配资源表)、资源分配弹窗(仪器/试剂/人员三类,试剂显示库存)、
流程编辑弹窗(可套用三套 SOP 模板、增删步骤、填 SOP 内容与预计耗时)。
数据接口管理:操作列加 测试/采集/字段映射/日志 四项;字段映射弹窗(源字段+JSON路径+
目标字段+转换方式+转换参数+默认值,可增删行);调用日志抽屉(时间/类型/结果/耗时/
写入数/错误信息,带分页)。
新增 6 个 API 文件,3 个现有 API 追加方法。
| | |
| | | data: ids |
| | | }) |
| | | } |
| | | |
| | | // ===== ç¬¬äºæ¹éæ±æ°å¢ ===== |
| | | |
| | | // æµè¯è¿æ¥ |
| | | export function testInterface(id) { |
| | | return request({ url: '/lims/dataInterface/test/' + id, method: 'post' }) |
| | | } |
| | | |
| | | // ç«å³éé䏿¬¡ |
| | | export function collectInterface(id) { |
| | | return request({ url: '/lims/dataInterface/collect/' + id, method: 'post' }) |
| | | } |
| | | |
| | | // æ¥è¯¢å段æ å° |
| | | export function listInterfaceFields(interfaceId) { |
| | | return request({ url: '/lims/dataInterface/fields/' + interfaceId, method: 'get' }) |
| | | } |
| | | |
| | | // æ¹éä¿ååæ®µæ å° |
| | | export function saveInterfaceFields(data) { |
| | | return request({ url: '/lims/dataInterface/field/saveBatch', method: 'post', data: data }) |
| | | } |
| | | |
| | | // æ¥è¯¢è°ç¨æ¥å¿ |
| | | export function listInterfaceLogs(query) { |
| | | return request({ url: '/lims/dataInterface/logs', method: 'get', params: query }) |
| | | } |
| | |
| | | data: ids |
| | | }) |
| | | } |
| | | |
| | | // ===== ç¬¬äºæ¹éæ±æ°å¢ ===== |
| | | |
| | | // å®éªè¯¦æ
ï¼å«æµç¨èç¹ä¸èµæºåé
ï¼ |
| | | export function getExperimentDetail(id) { |
| | | return request({ url: '/lims/experiment/detail/' + id, method: 'get' }) |
| | | } |
| | | |
| | | // æ¹éä¿åå®éªæµç¨èç¹ |
| | | export function saveExperimentSteps(data) { |
| | | return request({ url: '/lims/experiment/step/saveBatch', method: 'post', data: data }) |
| | | } |
| | | |
| | | // 宿å®éªæ¥éª¤ |
| | | export function completeExperimentStep(data) { |
| | | return request({ url: '/lims/experiment/step/complete', method: 'post', data: data }) |
| | | } |
| | | |
| | | // 为å®éªåé
èµæº |
| | | export function assignExperimentResource(data) { |
| | | return request({ url: '/lims/experiment/assignResource', method: 'post', data: data }) |
| | | } |
| | | |
| | | // ç§»é¤å®éªèµæº |
| | | export function removeExperimentResource(id) { |
| | | return request({ url: '/lims/experiment/resource/' + id, method: 'delete' }) |
| | | } |
| | | |
| | | // è·åæµç¨æ¨¡æ¿ |
| | | export function getStepTemplate(type) { |
| | | return request({ url: '/lims/experiment/stepTemplate', method: 'get', params: { type } }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // å®éªèµæºåé
æ¥å£ |
| | | import request from '@/utils/request' |
| | | |
| | | export function listExperimentResource(query) { |
| | | return request({ url: '/lims/experimentResource/listPage', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function listAllExperimentResource(query) { |
| | | return request({ url: '/lims/experimentResource/listAll', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function getExperimentResource(id) { |
| | | return request({ url: '/lims/experimentResource/' + id, method: 'get' }) |
| | | } |
| | | |
| | | export function addExperimentResource(data) { |
| | | return request({ url: '/lims/experimentResource/add', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function updateExperimentResource(data) { |
| | | return request({ url: '/lims/experimentResource/update', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function delExperimentResource(ids) { |
| | | return request({ url: '/lims/experimentResource/delete', method: 'delete', data: ids }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // å®éªæµç¨èç¹æ¥å£ |
| | | import request from '@/utils/request' |
| | | |
| | | export function listExperimentStep(query) { |
| | | return request({ url: '/lims/experimentStep/listPage', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function listAllExperimentStep(query) { |
| | | return request({ url: '/lims/experimentStep/listAll', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function getExperimentStep(id) { |
| | | return request({ url: '/lims/experimentStep/' + id, method: 'get' }) |
| | | } |
| | | |
| | | export function addExperimentStep(data) { |
| | | return request({ url: '/lims/experimentStep/add', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function updateExperimentStep(data) { |
| | | return request({ url: '/lims/experimentStep/update', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function delExperimentStep(ids) { |
| | | return request({ url: '/lims/experimentStep/delete', method: 'delete', data: ids }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ¥å£å段æ å°æ¥å£ |
| | | import request from '@/utils/request' |
| | | |
| | | export function listInterfaceField(query) { |
| | | return request({ url: '/lims/interfaceField/listPage', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function listAllInterfaceField(query) { |
| | | return request({ url: '/lims/interfaceField/listAll', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function getInterfaceField(id) { |
| | | return request({ url: '/lims/interfaceField/' + id, method: 'get' }) |
| | | } |
| | | |
| | | export function addInterfaceField(data) { |
| | | return request({ url: '/lims/interfaceField/add', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function updateInterfaceField(data) { |
| | | return request({ url: '/lims/interfaceField/update', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function delInterfaceField(ids) { |
| | | return request({ url: '/lims/interfaceField/delete', method: 'delete', data: ids }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ¥å£è°ç¨æ¥å¿æ¥å£ |
| | | import request from '@/utils/request' |
| | | |
| | | export function listInterfaceLog(query) { |
| | | return request({ url: '/lims/interfaceLog/listPage', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function listAllInterfaceLog(query) { |
| | | return request({ url: '/lims/interfaceLog/listAll', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function getInterfaceLog(id) { |
| | | return request({ url: '/lims/interfaceLog/' + id, method: 'get' }) |
| | | } |
| | | |
| | | export function addInterfaceLog(data) { |
| | | return request({ url: '/lims/interfaceLog/add', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function updateInterfaceLog(data) { |
| | | return request({ url: '/lims/interfaceLog/update', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function delInterfaceLog(ids) { |
| | | return request({ url: '/lims/interfaceLog/delete', method: 'delete', data: ids }) |
| | | } |
| | |
| | | data: ids |
| | | }) |
| | | } |
| | | |
| | | // ===== ç¬¬äºæ¹éæ±æ°å¢ ===== |
| | | |
| | | // æ ·å详æ
ï¼å«åºå
¥åºæµæ°´ä¸ç¶æåæ´åå²ï¼ |
| | | export function getSampleDetail(id) { |
| | | return request({ url: '/lims/sample/detail/' + id, method: 'get' }) |
| | | } |
| | | |
| | | // æ ·ååºåº |
| | | export function outStockSample(data) { |
| | | return request({ url: '/lims/sample/outStock', method: 'post', data: data }) |
| | | } |
| | | |
| | | // æ ·åå
¥åº |
| | | export function inStockSample(data) { |
| | | return request({ url: '/lims/sample/inStock', method: 'post', data: data }) |
| | | } |
| | | |
| | | // æ ·åç¶ææµè½¬ |
| | | export function changeSampleStatus(data) { |
| | | return request({ url: '/lims/sample/changeStatus', method: 'post', data: data }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ ·åç¶æåæ´å岿¥å£ |
| | | import request from '@/utils/request' |
| | | |
| | | export function listSampleStatusLog(query) { |
| | | return request({ url: '/lims/sampleStatusLog/listPage', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function listAllSampleStatusLog(query) { |
| | | return request({ url: '/lims/sampleStatusLog/listAll', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function getSampleStatusLog(id) { |
| | | return request({ url: '/lims/sampleStatusLog/' + id, method: 'get' }) |
| | | } |
| | | |
| | | export function addSampleStatusLog(data) { |
| | | return request({ url: '/lims/sampleStatusLog/add', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function updateSampleStatusLog(data) { |
| | | return request({ url: '/lims/sampleStatusLog/update', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function delSampleStatusLog(ids) { |
| | | return request({ url: '/lims/sampleStatusLog/delete', method: 'delete', data: ids }) |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // æ ·ååºå
¥åºæµæ°´æ¥å£ |
| | | import request from '@/utils/request' |
| | | |
| | | export function listSampleStockRecord(query) { |
| | | return request({ url: '/lims/sampleStockRecord/listPage', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function listAllSampleStockRecord(query) { |
| | | return request({ url: '/lims/sampleStockRecord/listAll', method: 'get', params: query }) |
| | | } |
| | | |
| | | export function getSampleStockRecord(id) { |
| | | return request({ url: '/lims/sampleStockRecord/' + id, method: 'get' }) |
| | | } |
| | | |
| | | export function addSampleStockRecord(data) { |
| | | return request({ url: '/lims/sampleStockRecord/add', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function updateSampleStockRecord(data) { |
| | | return request({ url: '/lims/sampleStockRecord/update', method: 'post', data: data }) |
| | | } |
| | | |
| | | export function delSampleStockRecord(ids) { |
| | | return request({ url: '/lims/sampleStockRecord/delete', method: 'delete', data: ids }) |
| | | } |
| | |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | |
| | | <!-- ===== åæ®µæ å°å¼¹çª ===== --> |
| | | <el-dialog v-model="fieldVisible" :title="'åæ®µæ å° ââ ' + (fieldInterface.interfaceName || '')" width="1000px"> |
| | | <el-alert type="info" :closable="false" style="margin-bottom: 12px"> |
| | | <template #title> |
| | | 设å¤è¿å JSON æ¶ï¼ç¨ãJSON è·¯å¾ãåå¼ï¼å¦ $.data.temperatureï¼ï¼è¿åçº¯ææ¬æ¶åªéå¡«ãæºåæ®µåãï¼æ´æ®µææ¬ä¼ä½ä¸ºå¼ã |
| | | </template> |
| | | </el-alert> |
| | | <el-table :data="fieldData" border size="small" max-height="420"> |
| | | <el-table-column label="æºå段å" width="150"> |
| | | <template #default="{ row }"><el-input v-model="row.sourceField" placeholder="å¦ temp" size="small" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="JSON è·¯å¾" width="180"> |
| | | <template #default="{ row }"><el-input v-model="row.sourcePath" placeholder="å¦ $.data.temp" size="small" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="ç®æ åæ®µ" width="140"> |
| | | <template #default="{ row }"> |
| | | <el-select v-model="row.targetField" size="small" style="width: 100%"> |
| | | <el-option v-for="t in TARGET_FIELDS" :key="t.value" :label="t.label" :value="t.value" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="è½¬æ¢æ¹å¼" width="130"> |
| | | <template #default="{ row }"> |
| | | <el-select v-model="row.transformType" size="small" style="width: 100%"> |
| | | <el-option v-for="t in TRANSFORM_TYPES" :key="t.value" :label="t.label" :value="t.value" /> |
| | | </el-select> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="转æ¢åæ°" width="130"> |
| | | <template #default="{ row }"><el-input v-model="row.transformValue" placeholder="ç³»æ°/åç§»/æ å°JSON" size="small" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="é»è®¤å¼" width="110"> |
| | | <template #default="{ row }"><el-input v-model="row.defaultValue" size="small" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="70" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button link type="danger" @click="removeFieldRow($index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <template #footer> |
| | | <el-button @click="addFieldRow">æ°å¢ä¸è¡</el-button> |
| | | <el-button @click="fieldVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="saveFields">ä¿åæ å°</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- ===== è°ç¨æ¥å¿æ½å± ===== --> |
| | | <el-drawer v-model="logVisible" title="æ¥å£è°ç¨æ¥å¿" size="900px" destroy-on-close> |
| | | <el-table :data="logData" :loading="logLoading" border size="small" max-height="620"> |
| | | <el-table-column prop="invokeTime" label="æ¶é´" width="160" /> |
| | | <el-table-column prop="interfaceName" label="æ¥å£" width="160" show-overflow-tooltip /> |
| | | <el-table-column prop="invokeType" label="ç±»å" width="80"> |
| | | <template #default="{ row }">{{ { test: "æµè¯", manual: "æå¨", auto: "宿¶" }[row.invokeType] || row.invokeType }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="success" label="ç»æ" width="70"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.success === '1' ? 'success' : 'danger'" size="small">{{ row.success === "1" ? "æå" : "失败" }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="durationMs" label="èæ¶" width="80"> |
| | | <template #default="{ row }">{{ row.durationMs }} ms</template> |
| | | </el-table-column> |
| | | <el-table-column prop="recordCount" label="åå
¥" width="70" /> |
| | | <el-table-column prop="errorMsg" label="éè¯¯ä¿¡æ¯ / ååºæè¦" show-overflow-tooltip> |
| | | <template #default="{ row }">{{ row.errorMsg || row.responseData || "â" }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="operator" label="触å人" width="90" /> |
| | | </el-table> |
| | | <div style="margin-top: 12px; text-align: right"> |
| | | <el-pagination |
| | | v-model:current-page="logQuery.pageNum" |
| | | v-model:page-size="logQuery.pageSize" |
| | | :total="logTotal" |
| | | :page-sizes="[20, 50, 100]" |
| | | layout="total, sizes, prev, pager, next" |
| | | @current-change="loadLogs" |
| | | @size-change="loadLogs" |
| | | /> |
| | | </div> |
| | | </el-drawer> |
| | | </template> |
| | | |
| | | <script setup> |
| | |
| | | listDataInterface, |
| | | updateDataInterface, |
| | | } from "@/api/lims/dataInterface.js"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { ElMessage, ElMessageBox, ElLoading } from "element-plus"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import { testInterface, collectInterface, listInterfaceFields, saveInterfaceFields, listInterfaceLogs } from "@/api/lims/dataInterface"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | |
| | | width: 100, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | width: 300, |
| | | operation: [ |
| | | { |
| | | name: "æµè¯", |
| | | type: "text", |
| | | clickFun: (row) => { doTest(row); }, |
| | | }, |
| | | disabled: (row) => { |
| | | return row.maintainer !== userStore.nickName |
| | | } |
| | | }, |
| | | ], |
| | | { |
| | | name: "éé", |
| | | type: "text", |
| | | clickFun: (row) => { doCollect(row); }, |
| | | }, |
| | | { |
| | | name: "åæ®µæ å°", |
| | | type: "text", |
| | | clickFun: (row) => { openFields(row); }, |
| | | }, |
| | | { |
| | | name: "æ¥å¿", |
| | | type: "text", |
| | | clickFun: (row) => { openLogs(row); }, |
| | | }, |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { openForm("edit", row); }, |
| | | disabled: (row) => row.maintainer !== userStore.nickName, |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | |
| | | return `${year}-${month}-${day}`; |
| | | } |
| | | |
| | | // ==================== ç¬¬äºæ¹éæ±ï¼æµè¯/éé/åæ®µæ å°/æ¥å¿ ==================== |
| | | const fieldVisible = ref(false); |
| | | const fieldData = ref([]); |
| | | const fieldInterface = ref({}); |
| | | const logVisible = ref(false); |
| | | const logData = ref([]); |
| | | const logLoading = ref(false); |
| | | const logTotal = ref(0); |
| | | const logQuery = ref({ pageNum: 1, pageSize: 20 }); |
| | | const collecting = ref(false); |
| | | |
| | | const TARGET_FIELDS = [ |
| | | { value: "collectionValue", label: "ééæ°å¼" }, |
| | | { value: "deviceName", label: "设å¤åç§°" }, |
| | | { value: "deviceCode", label: "设å¤ç¼å·" }, |
| | | { value: "dataType", label: "æ°æ®ç±»å" }, |
| | | { value: "unit", label: "计éåä½" }, |
| | | { value: "rawData", label: "åå§æ°æ®" }, |
| | | { value: "processedData", label: "å¤çåæ°æ®" }, |
| | | ]; |
| | | const TRANSFORM_TYPES = [ |
| | | { value: "none", label: "ç´æ¥ä½¿ç¨" }, |
| | | { value: "multiply", label: "ä¹ä»¥ç³»æ°" }, |
| | | { value: "divide", label: "é¤ä»¥ç³»æ°" }, |
| | | { value: "offset", label: "å åç§»é" }, |
| | | { value: "map", label: "åå
¸æ å°" }, |
| | | ]; |
| | | |
| | | // æµè¯è¿æ¥ |
| | | const doTest = (row) => { |
| | | const loading = ElLoading.service({ text: "æ£å¨æµè¯è¿æ¥â¦", background: "rgba(0,0,0,0.3)" }); |
| | | testInterface(row.id).then((res) => { |
| | | loading.close(); |
| | | ElMessageBox.alert(res.msg || "è¿æ¥æå", "æµè¯ç»æ", { type: "success" }); |
| | | }).catch((e) => { |
| | | loading.close(); |
| | | const m = (e && e.msg) || (e && e.response && e.response.data && e.response.data.msg) || "è¿æ¥å¤±è´¥"; |
| | | ElMessageBox.alert(m, "æµè¯ç»æ", { type: "error" }); |
| | | }); |
| | | }; |
| | | |
| | | // ç«å³éé |
| | | const doCollect = (row) => { |
| | | ElMessageBox.confirm(`确认对ã${row.interfaceName}ãç«å³éé䏿¬¡æ°æ®ï¼`, "æç¤º", { type: "info" }).then(() => { |
| | | collecting.value = true; |
| | | const loading = ElLoading.service({ text: "æ£å¨ééâ¦", background: "rgba(0,0,0,0.3)" }); |
| | | collectInterface(row.id).then((res) => { |
| | | loading.close(); |
| | | collecting.value = false; |
| | | const d = res.data || {}; |
| | | let msg = res.msg || "éé宿"; |
| | | if (d.rawLength !== undefined) msg += `\nååºé¿åº¦ï¼${d.rawLength} å符`; |
| | | if (d.warnings && d.warnings.length) msg += "\næç¤ºï¼" + d.warnings.join("ï¼"); |
| | | ElMessageBox.alert(msg.replace(/\n/g, "<br/>"), "ééç»æ", { |
| | | type: "success", |
| | | dangerouslyUseHTMLString: true, |
| | | }); |
| | | }).catch((e) => { |
| | | loading.close(); |
| | | collecting.value = false; |
| | | const m = (e && e.msg) || "éé失败"; |
| | | ElMessageBox.alert(m, "ééç»æ", { type: "error" }); |
| | | }); |
| | | }).catch(() => {}); |
| | | }; |
| | | |
| | | // åæ®µæ å° |
| | | const openFields = (row) => { |
| | | fieldInterface.value = row; |
| | | fieldData.value = []; |
| | | fieldVisible.value = true; |
| | | listInterfaceFields(row.id).then((res) => { |
| | | const list = res.data || []; |
| | | fieldData.value = list.length ? list.map((x) => ({ ...x })) : [newFieldRow()]; |
| | | }); |
| | | }; |
| | | const newFieldRow = () => ({ |
| | | sourceField: "", |
| | | sourcePath: "", |
| | | targetField: "collectionValue", |
| | | transformType: "none", |
| | | transformValue: "", |
| | | defaultValue: "", |
| | | remark: "", |
| | | }); |
| | | const addFieldRow = () => { fieldData.value.push(newFieldRow()); }; |
| | | const removeFieldRow = (i) => { fieldData.value.splice(i, 1); }; |
| | | const saveFields = () => { |
| | | const rows = fieldData.value.filter((r) => r.sourceField && r.sourceField.trim()); |
| | | if (!rows.length) { ElMessage.warning("è³å°é
ç½®ä¸æ¡å段æ å°"); return; } |
| | | saveInterfaceFields({ |
| | | interfaceId: fieldInterface.value.id, |
| | | fields: rows.map((r, i) => ({ ...r, interfaceCode: fieldInterface.value.interfaceCode, sortNo: i + 1 })), |
| | | }).then((res) => { |
| | | ElMessage.success(res.msg || "å·²ä¿å"); |
| | | fieldVisible.value = false; |
| | | }); |
| | | }; |
| | | |
| | | // è°ç¨æ¥å¿ |
| | | const openLogs = (row) => { |
| | | logQuery.value = { pageNum: 1, pageSize: 20, interfaceId: row ? row.id : undefined }; |
| | | logData.value = []; |
| | | logVisible.value = true; |
| | | loadLogs(); |
| | | }; |
| | | const loadLogs = () => { |
| | | logLoading.value = true; |
| | | listInterfaceLogs(logQuery.value).then((res) => { |
| | | logLoading.value = false; |
| | | logData.value = res.data.records || []; |
| | | logTotal.value = res.data.total || 0; |
| | | }).catch(() => (logLoading.value = false)); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢å®éª</el-button> |
| | | <el-button type="success" plain @click="openResource()">åé
èµæº</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | |
| | | <!-- ===== å®éªè¯¦æ
æ½å±ï¼æµç¨èç¹ + èµæºåé
===== --> |
| | | <el-drawer v-model="detailVisible" title="å®éªè¯¦æ
" size="900px" destroy-on-close> |
| | | <div v-if="detailData.experiment" style="padding: 0 12px"> |
| | | <el-descriptions :column="3" border size="small" title="åºæ¬ä¿¡æ¯"> |
| | | <el-descriptions-item label="å®éªç¼å·">{{ detailData.experiment.experimentCode }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®éªåç§°">{{ detailData.experiment.experimentName }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®éªç±»å">{{ detailData.experiment.experimentType }}</el-descriptions-item> |
| | | <el-descriptions-item label="å®éªæ å">{{ detailData.experiment.experimentStandard }}</el-descriptions-item> |
| | | <el-descriptions-item label="è´è´£äºº">{{ detailData.experiment.responsiblePerson }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç¶æ"> |
| | | <el-tag :type="expStatusTag(detailData.experiment.experimentStatus)" size="small">{{ expStatusText(detailData.experiment.experimentStatus) }}</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="è®¡åæ¶é´" :span="2">{{ detailData.experiment.planStartDate }} ~ {{ detailData.experiment.planEndDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="æµç¨è¿åº¦"> |
| | | <el-progress :percentage="detailData.progress || 0" :stroke-width="14" style="width: 160px" /> |
| | | <span style="margin-left: 8px; color: #909399">{{ detailData.stepCompleted || 0 }}/{{ detailData.stepTotal || 0 }} æ¥</span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-divider content-position="left"> |
| | | å®éªæµç¨èç¹ï¼{{ (detailData.steps || []).length }} æ¥ï¼ |
| | | <el-button link type="primary" style="margin-left: 8px" @click="openStepEditor">ç¼è¾æµç¨</el-button> |
| | | </el-divider> |
| | | <el-timeline v-if="(detailData.steps || []).length"> |
| | | <el-timeline-item |
| | | v-for="s in detailData.steps" |
| | | :key="s.id" |
| | | :timestamp="'第 ' + s.stepNo + ' æ¥ã' + (s.actualEndTime || s.planStartTime || '')" |
| | | :type="s.stepStatus === 'completed' ? 'success' : s.stepStatus === 'running' ? 'warning' : 'info'" |
| | | placement="top" |
| | | > |
| | | <div style="font-weight: 600">{{ s.stepName }} |
| | | <el-tag :type="stepStatusTag(s.stepStatus)" size="small" style="margin-left: 8px">{{ stepStatusText(s.stepStatus) }}</el-tag> |
| | | <el-button v-if="s.stepStatus !== 'completed'" link type="primary" size="small" style="margin-left: 8px" @click="doCompleteStep(s)">æ è®°å®æ</el-button> |
| | | </div> |
| | | <div style="color: #606266; font-size: 13px; margin-top: 4px; white-space: pre-wrap">{{ s.stepContent }}</div> |
| | | <div v-if="s.stepResult" style="color: #67c23a; font-size: 12px; margin-top: 4px">æ§è¡ç»æï¼{{ s.stepResult }}</div> |
| | | <div v-if="s.executor" style="color: #909399; font-size: 12px">æ§è¡äººï¼{{ s.executor }}</div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="å°æªé
ç½®æµç¨èç¹ï¼ç¹å³ä¸ãç¼è¾æµç¨ãæå¥ç¨æ¨¡æ¿" :image-size="60" /> |
| | | |
| | | <el-divider content-position="left">å·²åé
èµæºï¼{{ (detailData.resources || []).length }} 项ï¼</el-divider> |
| | | <el-table :data="detailData.resources || []" border size="small" max-height="260"> |
| | | <el-table-column prop="resourceType" label="ç±»å" width="90"> |
| | | <template #default="{ row }">{{ resTypeText(row.resourceType) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="resourceName" label="åç§°" show-overflow-tooltip /> |
| | | <el-table-column prop="quantity" label="æ°é" width="90" /> |
| | | <el-table-column prop="unit" label="åä½" width="70" /> |
| | | <el-table-column prop="usageTime" label="ä½¿ç¨æ¶æ®µ" width="150" show-overflow-tooltip /> |
| | | <el-table-column prop="status" label="ç¶æ" width="90"> |
| | | <template #default="{ row }">{{ resStatusText(row.status) }}</template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="80" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-button link type="danger" @click="doRemoveResource(row)">ç§»é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-drawer> |
| | | |
| | | <!-- ===== èµæºåé
å¼¹çª ===== --> |
| | | <el-dialog v-model="resourceVisible" title="为å®éªåé
èµæº" width="600px"> |
| | | <el-form :model="resForm" label-position="top"> |
| | | <el-form-item label="å®éª"> |
| | | <el-input :model-value="resForm.experimentName + 'ï¼' + resForm.experimentCode + 'ï¼'" disabled /> |
| | | </el-form-item> |
| | | <el-form-item label="èµæºç±»å"> |
| | | <el-radio-group v-model="resForm.resourceType" @change="onResTypeChange"> |
| | | <el-radio-button label="instrument">仪å¨è®¾å¤</el-radio-button> |
| | | <el-radio-button label="reagent">è¯åèæ</el-radio-button> |
| | | <el-radio-button label="personnel">åä¸äººå</el-radio-button> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | <el-form-item v-if="resForm.resourceType !== 'personnel'" :label="resForm.resourceType === 'instrument' ? 'éæ©ä»ªå¨' : 'éæ©è¯å/èæ'"> |
| | | <el-select v-model="resForm.resourceId" filterable style="width: 100%" placeholder="è¯·éæ©" @change="onResSelect"> |
| | | <el-option |
| | | v-for="o in resOptions" |
| | | :key="o.id" |
| | | :label="resOptionLabel(o)" |
| | | :value="o.id" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item v-else label="人åå§å"> |
| | | <el-input v-model="resForm.resourceName" placeholder="请è¾å
¥åä¸äºº" /> |
| | | </el-form-item> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item :label="resForm.resourceType === 'reagent' ? 'é¢ç¨æ°éï¼ä¼æ£ååºåï¼' : 'æ°é'"> |
| | | <el-input-number v-model="resForm.quantity" :precision="4" :min="0" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åä½"> |
| | | <el-input v-model="resForm.unit" placeholder="å¦ å° / mL / 人" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="ä½¿ç¨æ¶æ®µ"> |
| | | <el-input v-model="resForm.usageTime" placeholder="å¦ 2026-10-29 09:00~12:00" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨"> |
| | | <el-input v-model="resForm.remark" type="textarea" :rows="2" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="resourceVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitResource">确认åé
</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- ===== æµç¨ç¼è¾å¼¹çª ===== --> |
| | | <el-dialog v-model="stepEditorVisible" title="ç¼è¾å®éªæµç¨" width="900px"> |
| | | <div style="margin-bottom: 12px"> |
| | | <el-select v-model="templateType" placeholder="å¥ç¨æµç¨æ¨¡æ¿" style="width: 240px" clearable @change="applyTemplate"> |
| | | <el-option label="åå¦åæï¼éç¨ï¼" value="chemicalAnalysis" /> |
| | | <el-option label="ç©çæµè¯ï¼éç¨ï¼" value="physicalTest" /> |
| | | <el-option label="å¾®çç©æ£æµï¼éç¨ï¼" value="microbialTest" /> |
| | | </el-select> |
| | | <el-button type="primary" plain style="margin-left: 10px" @click="addStepRow">æ°å¢æ¥éª¤</el-button> |
| | | </div> |
| | | <el-table :data="stepEditorData" border size="small" max-height="420"> |
| | | <el-table-column type="index" label="åºå·" width="60" align="center" /> |
| | | <el-table-column label="æ¥éª¤åç§°" width="180"> |
| | | <template #default="{ row }"><el-input v-model="row.stepName" placeholder="请è¾å
¥" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="æ åæä½å
容ï¼SOPï¼"> |
| | | <template #default="{ row }"><el-input v-model="row.stepContent" type="textarea" :rows="2" placeholder="请è¾å
¥" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="èæ¶(åé)" width="110"> |
| | | <template #default="{ row }"><el-input-number v-model="row.expectedDuration" :min="0" controls-position="right" style="width: 100%" /></template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="70" align="center"> |
| | | <template #default="{ $index }"> |
| | | <el-button link type="danger" @click="removeStepRow($index)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <template #footer> |
| | | <el-button @click="stepEditorVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="saveSteps">ä¿åæµç¨</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | |
| | | listExperiment, |
| | | updateExperiment, |
| | | } from "@/api/lims/experiment.js"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import { getExperimentDetail, assignExperimentResource, saveExperimentSteps, completeExperimentStep, getStepTemplate } from "@/api/lims/experiment"; |
| | | import { listInstrument } from "@/api/lims/instrument"; |
| | | import { listReagent } from "@/api/lims/reagent"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | |
| | | width: 100, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openDetail(row); |
| | | }, |
| | | }, |
| | | disabled: (row) => { |
| | | return row.maintainer !== userStore.nickName |
| | | } |
| | | }, |
| | | ], |
| | | { |
| | | name: "åé
èµæº", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openResource(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | disabled: (row) => { |
| | | return row.maintainer !== userStore.nickName |
| | | } |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | |
| | | return `${year}-${month}-${day}`; |
| | | } |
| | | |
| | | // ==================== ç¬¬äºæ¹éæ±ï¼è¯¦æ
/ æ¥éª¤ / èµæº ==================== |
| | | const detailVisible = ref(false); |
| | | const detailData = ref({ experiment: null, steps: [], resources: [], progress: 0, stepTotal: 0, stepCompleted: 0 }); |
| | | const resourceVisible = ref(false); |
| | | const resOptions = ref([]); |
| | | const resForm = ref({}); |
| | | const stepEditorVisible = ref(false); |
| | | const stepEditorData = ref([]); |
| | | const templateType = ref(""); |
| | | |
| | | const EXP_STATUS_MAP = { planned: "计åä¸", inProgress: "è¿è¡ä¸", completed: "已宿", cancelled: "已忶" }; |
| | | const STEP_STATUS_MAP = { pending: "å¾
æ§è¡", running: "æ§è¡ä¸", completed: "已宿", skipped: "已跳è¿", failed: "失败" }; |
| | | const RES_TYPE_MAP = { instrument: "仪å¨è®¾å¤", reagent: "è¯åèæ", personnel: "åä¸äººå" }; |
| | | const RES_STATUS_MAP = { planned: "已宿", inuse: "使ç¨ä¸", returned: "å·²å½è¿", consumed: "å·²æ¶è" }; |
| | | const expStatusText = (s) => EXP_STATUS_MAP[s] || s || "â"; |
| | | const stepStatusText = (s) => STEP_STATUS_MAP[s] || s || "â"; |
| | | const resTypeText = (s) => RES_TYPE_MAP[s] || s || "â"; |
| | | const resStatusText = (s) => RES_STATUS_MAP[s] || s || "â"; |
| | | const expStatusTag = (s) => (s === "completed" ? "success" : s === "inProgress" ? "warning" : s === "cancelled" ? "danger" : "info"); |
| | | const stepStatusTag = (s) => (s === "completed" ? "success" : s === "running" ? "warning" : s === "failed" ? "danger" : "info"); |
| | | |
| | | // 详æ
|
| | | const openDetail = (row) => { |
| | | detailData.value = { experiment: null, steps: [], resources: [], progress: 0, stepTotal: 0, stepCompleted: 0 }; |
| | | detailVisible.value = true; |
| | | getExperimentDetail(row.id).then((res) => { |
| | | detailData.value = res.data || detailData.value; |
| | | }); |
| | | }; |
| | | |
| | | // åé
èµæº |
| | | const openResource = (row) => { |
| | | let target = row; |
| | | if (!target) { |
| | | if (!selectedRows.value.length) { ElMessage.warning("请å
éæ©å®éª"); return; } |
| | | if (selectedRows.value.length > 1) { ElMessage.warning("䏿¬¡åªè½ç»ä¸ä¸ªå®éªåé
èµæº"); return; } |
| | | target = selectedRows.value[0]; |
| | | } |
| | | resForm.value = { |
| | | experimentId: target.id, |
| | | experimentCode: target.experimentCode, |
| | | experimentName: target.experimentName, |
| | | resourceType: "instrument", |
| | | resourceId: null, |
| | | resourceName: "", |
| | | quantity: null, |
| | | unit: "", |
| | | usageTime: "", |
| | | remark: "", |
| | | }; |
| | | resOptions.value = []; |
| | | loadResOptions("instrument"); |
| | | resourceVisible.value = true; |
| | | }; |
| | | const loadResOptions = (type) => { |
| | | if (type === "instrument") { |
| | | listInstrument({ pageNum: 1, pageSize: 500 }).then((r) => { resOptions.value = r.data.records || []; }); |
| | | } else if (type === "reagent") { |
| | | listReagent({ pageNum: 1, pageSize: 500 }).then((r) => { resOptions.value = r.data.records || []; }); |
| | | } else { |
| | | resOptions.value = []; |
| | | } |
| | | }; |
| | | const onResTypeChange = (t) => { |
| | | resForm.value.resourceId = null; |
| | | resForm.value.resourceName = ""; |
| | | loadResOptions(t); |
| | | }; |
| | | const onResSelect = (id) => { |
| | | const o = resOptions.value.find((x) => x.id === id); |
| | | if (!o) return; |
| | | if (resForm.value.resourceType === "reagent") { |
| | | resForm.value.unit = o.unit || ""; |
| | | resForm.value.quantity = null; |
| | | } else { |
| | | resForm.value.unit = "å°"; |
| | | resForm.value.quantity = 1; |
| | | } |
| | | }; |
| | | const resOptionLabel = (o) => { |
| | | if (resForm.value.resourceType === "reagent") { |
| | | return `${o.reagentName}ï¼åºå ${o.currentStock ?? 0}${o.unit || ""}ï¼`; |
| | | } |
| | | return `${o.instrumentName}ï¼${o.instrumentCode}ï¼`; |
| | | }; |
| | | const submitResource = () => { |
| | | const f = resForm.value; |
| | | if (f.resourceType !== "personnel" && !f.resourceId) { ElMessage.warning("è¯·éæ©èµæº"); return; } |
| | | if (f.resourceType === "personnel" && !f.resourceName) { ElMessage.warning("请è¾å
¥äººåå§å"); return; } |
| | | assignExperimentResource({ ...f }).then((res) => { |
| | | ElMessage.success(res.msg || "èµæºå·²åé
"); |
| | | resourceVisible.value = false; |
| | | if (detailVisible.value) openDetail({ id: f.experimentId }); |
| | | }); |
| | | }; |
| | | const doRemoveResource = (row) => { |
| | | ElMessageBox.confirm("确认移é¤è¯¥èµæºåé
ï¼", "æç¤º", { type: "warning" }).then(() => { |
| | | removeExperimentResource(row.id).then(() => { |
| | | ElMessage.success("已移é¤"); |
| | | openDetail({ id: row.experimentId }); |
| | | }); |
| | | }).catch(() => {}); |
| | | }; |
| | | |
| | | // æ¥éª¤ |
| | | const openStepEditor = () => { |
| | | const steps = detailData.value.steps || []; |
| | | stepEditorData.value = steps.length |
| | | ? steps.map((s) => ({ stepName: s.stepName, stepContent: s.stepContent, expectedDuration: s.expectedDuration })) |
| | | : []; |
| | | templateType.value = ""; |
| | | stepEditorVisible.value = true; |
| | | }; |
| | | const addStepRow = () => { |
| | | stepEditorData.value.push({ stepName: "", stepContent: "", expectedDuration: null }); |
| | | }; |
| | | const removeStepRow = (i) => { stepEditorData.value.splice(i, 1); }; |
| | | const saveSteps = () => { |
| | | const exp = detailData.value.experiment; |
| | | if (!exp) { ElMessage.warning("å®éªä¸åå¨"); return; } |
| | | const steps = stepEditorData.value |
| | | .filter((s) => s.stepName && s.stepName.trim()) |
| | | .map((s, i) => ({ |
| | | stepNo: i + 1, |
| | | stepName: s.stepName, |
| | | stepContent: s.stepContent, |
| | | expectedDuration: s.expectedDuration, |
| | | stepStatus: "pending", |
| | | })); |
| | | if (!steps.length) { ElMessage.warning("è³å°éè¦ä¸ä¸ªæ¥éª¤"); return; } |
| | | saveExperimentSteps({ experimentId: exp.id, steps }).then((res) => { |
| | | ElMessage.success(res.msg || "æµç¨å·²ä¿å"); |
| | | stepEditorVisible.value = false; |
| | | openDetail({ id: exp.id }); |
| | | }); |
| | | }; |
| | | const applyTemplate = (type) => { |
| | | if (!type) return; |
| | | getStepTemplate(type).then((res) => { |
| | | const list = res.data || []; |
| | | stepEditorData.value = list.map((s) => ({ |
| | | stepName: s.stepName, |
| | | stepContent: s.stepContent, |
| | | expectedDuration: s.expectedDuration, |
| | | })); |
| | | ElMessage.success("å·²å¥ç¨æ¨¡æ¿ï¼å
± " + list.length + " æ¥"); |
| | | }); |
| | | }; |
| | | const doCompleteStep = (s) => { |
| | | ElMessageBox.prompt("å¯å¡«åæ§è¡ç»æï¼éå¡«ï¼", "宿æ¥éª¤ï¼" + s.stepName, { |
| | | inputPlaceholder: "å¦ æµå®å®æï¼ç»æ 45.2", |
| | | confirmButtonText: "ç¡®è®¤å®æ", |
| | | cancelButtonText: "åæ¶", |
| | | }).then(({ value }) => { |
| | | completeExperimentStep({ stepId: s.id, stepResult: value }).then(() => { |
| | | ElMessage.success("æ¥éª¤å·²å®æ"); |
| | | openDetail({ id: s.experimentId }); |
| | | }); |
| | | }).catch(() => {}); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">æ°å¢æ ·å</el-button> |
| | | <el-button type="warning" plain @click="openOutStock()">åºåº</el-button> |
| | | <el-button @click="handleOut">导åº</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">å é¤</el-button> |
| | | </div> |
| | |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | |
| | | <!-- ===== æ ·å详æ
æ½å±ï¼åºå
¥åºæµæ°´ + ç¶æåæ´åå² ===== --> |
| | | <el-drawer v-model="detailVisible" title="æ ·å详æ
" size="820px" destroy-on-close> |
| | | <div v-if="detailData.sample" style="padding: 0 12px"> |
| | | <el-descriptions :column="3" border size="small" title="åºæ¬ä¿¡æ¯"> |
| | | <el-descriptions-item label="æ ·åç¼å·">{{ detailData.sample.sampleCode }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ ·ååç§°">{{ detailData.sample.sampleName }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç±»å">{{ detailData.sample.sampleType }}</el-descriptions-item> |
| | | <el-descriptions-item label="è§æ ¼">{{ detailData.sample.specification }}</el-descriptions-item> |
| | | <el-descriptions-item label="æ°é">{{ detailData.sample.quantity }} {{ detailData.sample.unit }}</el-descriptions-item> |
| | | <el-descriptions-item label="å½åç¶æ"> |
| | | <el-tag :type="statusTagType(detailData.sample.sampleStatus)" size="small">{{ statusText(detailData.sample.sampleStatus) }}</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="åå¨ä½ç½®">{{ detailData.sample.storageLocation }}</el-descriptions-item> |
| | | <el-descriptions-item label="å
¥åºæ¥æ">{{ detailData.sample.inStockDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="æææè³">{{ detailData.sample.validityDate }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <el-divider content-position="left">åºå
¥åºæµæ°´ï¼{{ (detailData.stockRecords || []).length }} æ¡ï¼</el-divider> |
| | | <el-table :data="detailData.stockRecords || []" border size="small" max-height="300"> |
| | | <el-table-column prop="operateTime" label="æ¶é´" width="160" /> |
| | | <el-table-column prop="recordType" label="ç±»å" width="80"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="row.recordType === 'in' ? 'success' : 'warning'" size="small">{{ recordTypeText(row.recordType) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="quantity" label="æ°é" width="90" /> |
| | | <el-table-column label="ç¶æåå" width="150"> |
| | | <template #default="{ row }">{{ statusText(row.beforeStatus) }} â {{ statusText(row.afterStatus) }}</template> |
| | | </el-table-column> |
| | | <el-table-column prop="operator" label="ç»æäºº" width="100" /> |
| | | <el-table-column prop="recipient" label="é¢ç¨æ¹" width="110" show-overflow-tooltip /> |
| | | <el-table-column prop="purpose" label="äºç±" show-overflow-tooltip /> |
| | | </el-table> |
| | | |
| | | <el-divider content-position="left">ç¶æåæ´åå²ï¼{{ (detailData.statusLogs || []).length }} æ¡ï¼</el-divider> |
| | | <el-timeline v-if="(detailData.statusLogs || []).length"> |
| | | <el-timeline-item |
| | | v-for="log in detailData.statusLogs" |
| | | :key="log.id" |
| | | :timestamp="log.operateTime" |
| | | placement="top" |
| | | > |
| | | <div>{{ statusText(log.fromStatus) }} <b>â</b> {{ statusText(log.toStatus) }}</div> |
| | | <div style="color: #909399; font-size: 12px">{{ log.changeReason || "â" }}ãæä½äººï¼{{ log.operator || "â" }}</div> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | <el-empty v-else description="ææ ç¶æåæ´è®°å½" :image-size="60" /> |
| | | </div> |
| | | </el-drawer> |
| | | |
| | | <!-- ===== æ ·ååºåºå¼¹çª ===== --> |
| | | <el-dialog v-model="outStockVisible" title="æ ·ååºåº" width="560px"> |
| | | <el-form :model="outForm" label-position="top"> |
| | | <el-form-item label="æ ·å"> |
| | | <el-input :model-value="outForm.sampleName + 'ï¼' + outForm.sampleCode + 'ï¼ç°æ ' + (outForm.stock ?? 0) + (outForm.unit || '') + 'ï¼'" disabled /> |
| | | </el-form-item> |
| | | <el-row :gutter="16"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åºåºæ°é"> |
| | | <el-input-number v-model="outForm.quantity" :precision="4" :min="0.0001" :max="outForm.stock || 999999" style="width: 100%" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¢ç¨æ¹/æ¥æ¶äºº"> |
| | | <el-input v-model="outForm.recipient" placeholder="请è¾å
¥" /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="å»å / æ°åå¨ä½ç½®"> |
| | | <el-input v-model="outForm.toLocation" placeholder="å¦ï¼æ£æµå®¤ A / 客æ·ç°åº" /> |
| | | </el-form-item> |
| | | <el-form-item label="åºåºäºç±"> |
| | | <el-input v-model="outForm.purpose" placeholder="å¦ï¼éæ£ãçæ ·è½¬ç§»ã鿝åå¤ç" /> |
| | | </el-form-item> |
| | | <el-form-item label="å
³èå®éªç¼å·ï¼å¯éï¼"> |
| | | <el-input v-model="outForm.relatedExperiment" placeholder="请è¾å
¥" /> |
| | | </el-form-item> |
| | | <el-form-item label="夿³¨"> |
| | | <el-input v-model="outForm.remark" type="textarea" :rows="2" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <el-button @click="outStockVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitOutStock">确认åºåº</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | </template> |
| | | |
| | | <script setup> |
| | |
| | | listSample, |
| | | updateSample, |
| | | } from "@/api/lims/sample.js"; |
| | | import { ElMessageBox } from "element-plus"; |
| | | import { ElMessage, ElMessageBox } from "element-plus"; |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import { getSampleDetail, outStockSample } from "@/api/lims/sample"; |
| | | import useUserStore from "@/store/modules/user"; |
| | | const { proxy } = getCurrentInstance(); |
| | | const userStore = useUserStore(); |
| | |
| | | width: 100, |
| | | }, |
| | | { |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | disabled: (row) => { |
| | | return row.maintainer !== userStore.nickName |
| | | } |
| | | }, |
| | | ], |
| | | dataType: "action", |
| | | label: "æä½", |
| | | align: "center", |
| | | fixed: 'right', |
| | | operation: [ |
| | | { |
| | | name: "详æ
", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openDetail(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "åºåº", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openOutStock(row); |
| | | }, |
| | | }, |
| | | { |
| | | name: "ç¼è¾", |
| | | type: "text", |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | disabled: (row) => { |
| | | return row.maintainer !== userStore.nickName |
| | | } |
| | | }, |
| | | ], |
| | | }, |
| | | ]); |
| | | const tableData = ref([]); |
| | |
| | | return `${year}-${month}-${day}`; |
| | | } |
| | | |
| | | // ==================== ç¬¬äºæ¹éæ±ï¼è¯¦æ
/ åºåº ==================== |
| | | const detailVisible = ref(false); |
| | | const detailData = ref({ sample: null, stockRecords: [], statusLogs: [] }); |
| | | const outStockVisible = ref(false); |
| | | const outForm = ref({ sampleId: null, sampleCode: "", sampleName: "", stock: 0, unit: "", quantity: null }); |
| | | |
| | | const STATUS_MAP = { |
| | | inStock: "å¨åº", |
| | | outStock: "åºåº", |
| | | testing: "æ£æµä¸", |
| | | destroyed: "已鿝", |
| | | }; |
| | | const RECORD_TYPE_MAP = { |
| | | in: "å
¥åº", |
| | | out: "åºåº", |
| | | destroy: "鿝", |
| | | return: "éåº", |
| | | transfer: "转移", |
| | | }; |
| | | const statusText = (s) => STATUS_MAP[s] || s || "â"; |
| | | const recordTypeText = (t) => RECORD_TYPE_MAP[t] || t || "â"; |
| | | const statusTagType = (s) => { |
| | | if (s === "inStock") return "success"; |
| | | if (s === "testing") return "warning"; |
| | | if (s === "destroyed") return "danger"; |
| | | return "info"; |
| | | }; |
| | | |
| | | // æå¼è¯¦æ
æ½å± |
| | | const openDetail = (row) => { |
| | | detailData.value = { sample: null, stockRecords: [], statusLogs: [] }; |
| | | detailVisible.value = true; |
| | | getSampleDetail(row.id).then((res) => { |
| | | detailData.value = res.data || { sample: null, stockRecords: [], statusLogs: [] }; |
| | | }); |
| | | }; |
| | | |
| | | // æå¼åºåºå¼¹çªï¼ä¸ä¼ row æ¶ç¨è¡¨æ ¼éä¸è¡ï¼ |
| | | const openOutStock = (row) => { |
| | | let target = row; |
| | | if (!target) { |
| | | if (!selectedRows.value.length) { |
| | | ElMessage.warning("请å
éæ©è¦åºåºçæ ·å"); |
| | | return; |
| | | } |
| | | if (selectedRows.value.length > 1) { |
| | | ElMessage.warning("䏿¬¡åªè½åºåºä¸ä¸ªæ ·åï¼è¯·åªé䏿¡"); |
| | | return; |
| | | } |
| | | target = selectedRows.value[0]; |
| | | } |
| | | if (target.sampleStatus === "destroyed") { |
| | | ElMessage.warning("æ ·å已鿝ï¼ä¸è½åºåº"); |
| | | return; |
| | | } |
| | | if (target.sampleStatus === "outStock") { |
| | | ElMessage.warning("æ ·åå·²æ¯åºåºç¶æ"); |
| | | return; |
| | | } |
| | | outForm.value = { |
| | | sampleId: target.id, |
| | | sampleCode: target.sampleCode, |
| | | sampleName: target.sampleName, |
| | | stock: Number(target.quantity || 0), |
| | | unit: target.unit || "", |
| | | quantity: null, |
| | | recipient: "", |
| | | toLocation: "", |
| | | purpose: "", |
| | | relatedExperiment: "", |
| | | remark: "", |
| | | }; |
| | | outStockVisible.value = true; |
| | | }; |
| | | |
| | | // æäº¤åºåº |
| | | const submitOutStock = () => { |
| | | const f = outForm.value; |
| | | if (!f.quantity || f.quantity <= 0) { |
| | | ElMessage.warning("请è¾å
¥åºåºæ°é"); |
| | | return; |
| | | } |
| | | if (f.quantity > f.stock) { |
| | | ElMessage.warning("åºåºæ°éè¶
è¿å½ååºåï¼ç°æ " + f.stock + (f.unit || "") + "ï¼"); |
| | | return; |
| | | } |
| | | outStockSample({ |
| | | sampleId: f.sampleId, |
| | | quantity: f.quantity, |
| | | recipient: f.recipient, |
| | | toLocation: f.toLocation, |
| | | purpose: f.purpose, |
| | | relatedExperiment: f.relatedExperiment, |
| | | remark: f.remark, |
| | | }).then((res) => { |
| | | ElMessage.success(res.msg || "åºåºæå"); |
| | | outStockVisible.value = false; |
| | | getList(); |
| | | }); |
| | | }; |
| | | |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |