index.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. <template>
  2. <div>
  3. <div v-if="show" class="reagentChange">
  4. <el-row type="flex">
  5. <el-col class="button">
  6. <div class="title">仪器设备维修后验证记录(定量)</div>
  7. <div v-if="nodeId === 'Activity_0b188bo' || readonly" />
  8. <div v-else>
  9. <el-button type="primary" size="mini" icon="ibps-icon-add" @click="handleAdd"> 添加</el-button>
  10. <el-button type="danger" size="mini" icon="ibps-icon-remove" @click="handleDelete"> 删除</el-button>
  11. <el-button type="warning" size="mini" icon="ibps-icon-calculator" @click="computedResult">计算</el-button>
  12. </div>
  13. </el-col>
  14. </el-row>
  15. <el-row type="flex">
  16. <el-col>
  17. <el-table ref="reagent" :data="reagentDataFilter" :span-method="spanMethod" @selection-change="handleSelectionChange">
  18. <el-table-column type="selection" width="55" />
  19. <el-table-column
  20. label="检验项目"
  21. prop="jianYanXiangMu"
  22. width="150"
  23. >
  24. <template slot-scope="{row}">
  25. <el-input v-if="!disabled" v-model="row.jianYanXiangMu" type="textarea" size="mini" placeholder="请输入" />
  26. <span v-else>{{ row.jianYanXiangMu|| '/' }}</span>
  27. </template>
  28. </el-table-column>
  29. <el-table-column label="浓度" prop="nongDu" width="100">
  30. <template slot-scope="{row}">
  31. <el-input v-if="!disabled" v-model="row.nongDu" size="mini" placeholder="请输入" />
  32. <span v-else>{{ row.nongDu|| '/' }}</span>
  33. </template>
  34. </el-table-column>
  35. <el-table-column label="条码号或标本号" prop="tiaoBiaoHao" width="150">
  36. <template slot-scope="{row}">
  37. <el-input v-if="!disabled" v-model="row.tiaoBiaoHao" size="mini" placeholder="请输入" />
  38. <span v-else>{{ row.tiaoBiaoHao|| '/' }}</span>
  39. </template>
  40. </el-table-column>
  41. <el-table-column label="故障前结果(X)" prop="qianJieGuo" width="130">
  42. <template slot-scope="{row}">
  43. <el-input v-if="!disabled" v-model="row.qianJieGuo" :min="0" size="mini" placeholder="请输入" />
  44. <span v-else>{{ row.qianJieGuo|| '/' }}</span>
  45. </template>
  46. </el-table-column>
  47. <el-table-column label="故障后结果(Y)" prop="houJieGuo" width="130">
  48. <template slot-scope="{row}">
  49. <el-input v-if="!disabled" v-model="row.houJieGuo" :min="0" size="mini" placeholder="请输入" />
  50. <span v-else>{{ row.houJieGuo|| '/' }}</span>
  51. </template>
  52. </el-table-column>
  53. <el-table-column label="计算方式" prop="jiSuanGongShi" width="140">
  54. <template slot-scope="{row}">
  55. <el-select v-if="!disabled" v-model="row.jiSuanGongShi" size="mini" placeholder="请选择">
  56. <el-option
  57. label="|Y-X|"
  58. value="|Y-X|"
  59. />
  60. <el-option
  61. label="|Y-X|/X(%)"
  62. value="|Y-X|/X(%)"
  63. />
  64. </el-select>
  65. <span v-else>{{ row.jiSuanGongShi|| '/' }}</span>
  66. </template>
  67. </el-table-column>
  68. <el-table-column label="实际差值" prop="shiJiChaZhi" />
  69. <el-table-column label="实际偏倚" prop="shiJiPianYi" />
  70. <el-table-column label="限定范围" prop="xianDingFanWei" width="110">
  71. <template slot-scope="{row}">
  72. <el-input v-if="!disabled" v-model="row.xianDingFanWei" size="mini" placeholder="请输入" type="number" />
  73. <span v-else>{{ row.xianDingFanWei|| '/' }}</span>
  74. </template>
  75. </el-table-column>
  76. <el-table-column label="允许偏倚" prop="yunXuPianYi" width="100">
  77. <template slot-scope="{row}">
  78. <el-input v-if="!disabled" v-model="row.yunXuPianYi" size="mini" placeholder="请输入" />
  79. <span v-else>{{ row.yunXuPianYi|| '/' }}</span>
  80. </template>
  81. </el-table-column>
  82. <el-table-column label="是否相符" prop="shiFouXiangFu" width="80" />
  83. <el-table-column label="符合率" prop="fuHeLv" />
  84. <el-table-column label="符合率可接受标准" prop="fuHeBiaoZhun" width="140">
  85. <template slot-scope="{row}">
  86. <el-select v-if="!disabled" v-model="row.fuHeBiaoZhun" size="mini" placeholder="请选择" @change="handelChange($event,row.jianYanXiangMu)">
  87. <el-option
  88. label="≥80%"
  89. value="≥80%"
  90. />
  91. <el-option
  92. label="=100%"
  93. value="=100%"
  94. />
  95. </el-select>
  96. <span v-else>{{ row.fuHeBiaoZhun|| '/' }}</span>
  97. </template>
  98. </el-table-column>
  99. <el-table-column label="结论" prop="jieLun" />
  100. </el-table>
  101. <el-pagination
  102. layout="total,sizes,prev, pager, next,jumper"
  103. :current-page="requestPage.pageNo"
  104. :page-size="requestPage.limit"
  105. :page-sizes="[10,15,20,30,50,100]"
  106. :total="reagentData.length"
  107. @size-change="handleSizeChange"
  108. @current-change="handleCurrentChange"
  109. />
  110. </el-col>
  111. </el-row>
  112. </div>
  113. </div>
  114. </template>
  115. <script>
  116. export default {
  117. props: {
  118. formData: {
  119. type: Object,
  120. default: () => {}
  121. },
  122. readonly: {
  123. type: Boolean,
  124. default: false
  125. },
  126. params: {
  127. type: Object,
  128. default: () => {}
  129. }
  130. },
  131. data () {
  132. return {
  133. reagentData: [],
  134. copyDialogData: [],
  135. disabled: false,
  136. ypData: [],
  137. nodeId: '',
  138. show: false,
  139. requestPage: {
  140. limit: 20,
  141. pageNo: 1
  142. },
  143. importTableDialogVisible: false,
  144. multipleSelection: []
  145. }
  146. },
  147. computed: {
  148. reagentDataFilter () {
  149. return this.reagentData.slice((this.requestPage.pageNo - 1) * (this.requestPage.limit), (this.requestPage.pageNo - 1) * (this.requestPage.limit) + this.requestPage.limit)
  150. }
  151. },
  152. watch: {
  153. 'formData.yqsbwxhlybdjlbzbdl': {
  154. handler (value, old) {
  155. if (value && value.length) {
  156. this.reagentData = value
  157. }
  158. },
  159. immediate: true
  160. },
  161. reagentData: {
  162. handler (value, old) {
  163. this.$emit('change-data', 'yqsbwxhlybdjlbzbdl', value)
  164. },
  165. deep: true
  166. },
  167. 'formData.xiangMuLeiXing': {
  168. handler (val) {
  169. if (val === '定量') { this.show = true } else { this.show = false; this.reagentData = [] }
  170. },
  171. immediate: true
  172. },
  173. 'formData.piLiangYunXu': {
  174. handler (val) {
  175. this.reagentData.forEach(item => {
  176. const { jianYanXiangMu } = this.formData
  177. if (item.jianYanXiangMu === jianYanXiangMu) {
  178. item.yunXuPianYi = val
  179. }
  180. })
  181. },
  182. immediate: true
  183. },
  184. 'formData.piLiangXianDing': {
  185. handler (val) {
  186. this.reagentData.forEach(item => {
  187. const { jianYanXiangMu } = this.formData
  188. if (item.jianYanXiangMu === jianYanXiangMu) {
  189. item.xianDingFanWei = val
  190. }
  191. })
  192. }
  193. }
  194. },
  195. mounted () {
  196. const { first = '' } = this.$store.getters.level
  197. const { deptList = [] } = this.$store.getters || {}
  198. const t1 = deptList.find(i => i.positionId === first) || {}
  199. console.log(t1.positionName)
  200. this.nodeId = this.params ? this.params.nodeId : ''
  201. this.disabled = this.readonly || this.nodeId === 'Activity_0b188bo'
  202. },
  203. methods: {
  204. handelChange (event, jianYanXiangMu) {
  205. this.reagentData.forEach(item => {
  206. if (item.jianYanXiangMu === jianYanXiangMu) {
  207. item.fuHeBiaoZhun = event
  208. }
  209. })
  210. },
  211. // 新增
  212. handleAdd () {
  213. const { xMMingCheng, piLiangYunXu, piLiangXianDing, piLiangNongDu } = this.formData
  214. if (this.disabled && this.reagentData?.length > 0) {
  215. return this.$message.warning('导入与手动添加功能不可混用!')
  216. }
  217. if (!this.formData.xMMingCheng) {
  218. return this.$message.warning('请选择或输入检验项目')
  219. }
  220. this.reagentData.push({ jianYanXiangMu: xMMingCheng, nongDu: piLiangNongDu, tiaoBiaoHao: '', qianJieGuo: '', houJieGuo: '', jiSuanGongShi: '|Y-X|/X(%)', shiJiChaZhi: '', shiJiPianYi: '', xianDingFanWei: piLiangXianDing, yunXuPianYi: piLiangYunXu, shiFouXiangFu: '', fuHeLv: '', fuHeBiaoZhun: '≥80%', jieLun: '' })
  221. const grouped = this.reagentData.reduce((acc, item) => {
  222. const key = item.jianYanXiangMu
  223. if (!acc[key]) acc[key] = []
  224. acc[key].push(item)
  225. return acc
  226. }, {})
  227. // 先清空数组,再填充新数据(确保 Vue 检测到变化)
  228. this.reagentData.splice(0, this.reagentData.length, ...Object.values(grouped).flat())
  229. // console.log(this.reagentData)
  230. },
  231. // 删除
  232. handleDelete () {
  233. this.$confirm('确定删除当前选中数据?', '提示', {
  234. confirmButtonText: '确定',
  235. cancelButtonText: '取消',
  236. type: 'warning'
  237. }).then(() => {
  238. if (this.multipleSelection.length > 0) {
  239. this.reagentData = this.reagentData.filter(row => !this.multipleSelection.includes(row))
  240. } else {
  241. this.$message.warning('请选择数据')
  242. }
  243. })
  244. },
  245. handleSelectionChange (val) {
  246. this.multipleSelection = val
  247. },
  248. getColumns () {
  249. return [{
  250. field_name: 'shiFouXiangFu',
  251. label: '是否相符',
  252. name: 'shiFouXiangFu'
  253. },
  254. {
  255. field_name: 'jianYanXiangMu',
  256. label: '检验项目',
  257. name: 'jianYanXiangMu'
  258. },
  259. {
  260. field_name: 'nongDu',
  261. label: '浓度',
  262. name: 'nongDu'
  263. },
  264. {
  265. field_name: 'tiaoBiaoHao',
  266. label: '条码号或标本号',
  267. name: 'tiaoBiaoHao'
  268. },
  269. {
  270. field_name: 'qianJieGuo',
  271. label: '故障前结果(X)',
  272. name: 'qianJieGuo'
  273. },
  274. {
  275. field_name: 'houJieGuo',
  276. label: '故障后结果(Y)',
  277. name: 'houJieGuo'
  278. },
  279. {
  280. field_name: 'shiJiChaZhi',
  281. label: '实际差值',
  282. name: 'shiJiChaZhi'
  283. },
  284. {
  285. field_name: 'shiJiPianYi',
  286. label: '实际偏倚',
  287. name: 'shiJiPianYi'
  288. },
  289. {
  290. field_name: 'xianDingFanWei',
  291. label: '限定范围',
  292. name: 'xianDingFanWei'
  293. },
  294. {
  295. field_name: 'yunXuPianYi',
  296. label: '允许偏倚',
  297. name: 'yunXuPianYi'
  298. },
  299. {
  300. field_name: 'fuHeLv',
  301. label: '符合率',
  302. name: 'fuHeLv'
  303. }, {
  304. field_name: 'jieLun',
  305. label: '结论',
  306. name: 'jieLun'
  307. },
  308. {
  309. field_name: 'fuHeBiaoZhun',
  310. label: '符合率可接受标准',
  311. name: 'fuHeBiaoZhun'
  312. }]
  313. },
  314. // 当前页码改变
  315. handleCurrentChange (val) {
  316. this.requestPage.pageNo = val
  317. },
  318. // 页码选择器改变
  319. handleSizeChange (val) {
  320. this.requestPage.limit = val
  321. this.requestPage.pageNo = 1
  322. },
  323. // 计算结果
  324. computedResult () {
  325. if (this.disabled) {
  326. this.$message.warning('导入数据请勿使用计算功能!')
  327. return
  328. }
  329. const hasInvalidData = this.reagentData.some(item =>
  330. !item.qianJieGuo || item.qianJieGuo <= 0 || !item.houJieGuo || item.houJieGuo <= 0
  331. )
  332. if (hasInvalidData || this.reagentData.length === 0) {
  333. this.$message.warning('故障前结果(X)必须大于0且不能为空!')
  334. return
  335. }
  336. const hasPyOrCz = this.reagentData.some(item =>
  337. (item.jiSuanGongShi === '|Y-X|/X(%)' && !item.yunXuPianYi) || (item.jiSuanGongShi === '|Y-X|' && !item.xianDingFanWei)
  338. )
  339. if (hasPyOrCz) {
  340. this.$message.warning(`计算方式为|Y-X|/X(%)请填写允许偏倚,计算方式为|Y-X|请填写限定范围`)
  341. return
  342. }
  343. // 预处理函数:统一处理百分比数值
  344. const normalizePercent = val => Number(String(val).replace('%', '')) || 0
  345. // 使用 Map 存储统计信息
  346. const projectStats = new Map()
  347. this.reagentData.forEach(item => {
  348. const { jiSuanGongShi, houJieGuo, qianJieGuo, xianDingFanWei, jianYanXiangMu } = item
  349. // 计算差值/偏倚
  350. if (!isNaN(houJieGuo) && !isNaN(qianJieGuo)) {
  351. if (jiSuanGongShi === '|Y-X|') {
  352. item.shiJiChaZhi = Math.abs((houJieGuo * 100000 - qianJieGuo * 100000) / 100000).toFixed(2)
  353. item.shiFouXiangFu = normalizePercent(item.shiJiChaZhi) <= normalizePercent(xianDingFanWei) ? '相符' : '不相符'
  354. item.shiJiPianYi = ''
  355. } else {
  356. item.shiJiPianYi = this.deleteAccuracy(Math.abs(houJieGuo - qianJieGuo) / qianJieGuo)
  357. item.shiFouXiangFu = normalizePercent(item.shiJiPianYi) <= normalizePercent(item.yunXuPianYi) ? '相符' : '不相符'
  358. item.shiJiChaZhi = ''
  359. }
  360. }
  361. // 统计符合率
  362. const stats = projectStats.get(jianYanXiangMu) || { total: 0, yes: 0 }
  363. stats.total++
  364. if (item.shiFouXiangFu === '相符') stats.yes++
  365. projectStats.set(jianYanXiangMu, stats)
  366. })
  367. // 填充符合率标准,保留小数点后两位
  368. this.reagentData.forEach(item => {
  369. const stats = projectStats.get(item.jianYanXiangMu)
  370. item.fuHeLv = this.deleteAccuracy(stats.yes / stats.total)
  371. console.log(parseFloat(item.fuHeLv), item.fuHeBiaoZhun)
  372. item.jieLun = parseFloat(item.fuHeLv) >= parseFloat(item.fuHeBiaoZhun.replace(/[≥=%]/g, '')) ? '可接受' : '不可接受'
  373. })
  374. },
  375. spanMethod ({ row, column, rowIndex, columnIndex }) {
  376. if (columnIndex === 1 || columnIndex === 12 || columnIndex === 13 || columnIndex === 14) {
  377. const currentValue = row[column.property]
  378. const preRow = this.reagentData[rowIndex - 1]
  379. // 上一行这一列的数据
  380. const preValue = preRow ? preRow[column.property] : null
  381. // 如果当前值和上一行的值相同,则将当前单元格隐藏
  382. // 给第0,10,11,12列对数值相同且是同一个'jianYanXiangMu'进行表格合并
  383. if (currentValue === preValue && row['jianYanXiangMu'] === preRow['jianYanXiangMu']) {
  384. return { rowspan: 0, colspan: 0 }
  385. } else {
  386. let rowspan = 1
  387. // 计算应该合并的行数
  388. for (let i = rowIndex + 1; i < this.reagentData.length; i++) {
  389. const nextRow = this.reagentData[i]
  390. const nextValue = nextRow[column.property]
  391. if (nextValue === currentValue && nextRow['jianYanXiangMu'] === row['jianYanXiangMu']) {
  392. rowspan++
  393. } else {
  394. break
  395. }
  396. }
  397. return { rowspan, colspan: 1 }
  398. }
  399. // if (rowIndex % rowspan === 0) {
  400. // return {
  401. // rowspan,
  402. // colspan: 1
  403. // }
  404. // } else {
  405. // return {
  406. // rowspan: 0,
  407. // colspan: 0
  408. // }
  409. // }
  410. }
  411. },
  412. // 去除小数*100精度方法
  413. deleteAccuracy (num) {
  414. // 是否带小数点
  415. if ((num.toString()).includes('.')) {
  416. // 保留小数点后面3位
  417. const numArry = num.toFixed(3).toString().split('.')
  418. // 整数位是否大于0
  419. if (numArry[0] > 0) {
  420. return Number(numArry[0] + numArry[1].substring(0, 2) + '.' + numArry[1].substring(2, 3)) + '%'
  421. } else {
  422. // 小数位第一位是否大于0
  423. if (numArry[1][0] > 0) {
  424. return Number(numArry[1].substring(0, 2) + '.' + numArry[1].substring(2, 3)) + '%'
  425. } else {
  426. // 小数位第二位是否大于0
  427. if (numArry[1][1] > 0) {
  428. return Number(numArry[1].substring(1, 2) + '.' + numArry[1].substring(2, 3)) + '%'
  429. } else {
  430. return Number(0 + '.' + numArry[1].substring(2, 3)) + '%'
  431. }
  432. }
  433. }
  434. } else {
  435. return num * 100 + '%'
  436. }
  437. }
  438. }
  439. }
  440. </script>
  441. <style lang="scss" scoped>
  442. .reagentChange{
  443. margin-bottom: 20px;
  444. .button{
  445. display: flex;
  446. justify-content: space-between;
  447. padding: 0px 0px 0px 15px;
  448. background: #f0ffff;
  449. .title {
  450. color: #999;
  451. font-size: 12px;
  452. font-weight: bold;
  453. margin-bottom: 0;
  454. }
  455. .el-button {
  456. margin: 0;
  457. }
  458. }
  459. }
  460. </style>