facilityData.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. <!--
  2. author:luoaoxuan
  3. subject:设施环境通用组件
  4. -->
  5. <template>
  6. <div class="ficily-data">
  7. <el-row type="flex">
  8. <el-col style="margin: 0 0 5px 0">
  9. <div class="button">
  10. <el-button
  11. v-if="!isCul"
  12. type="danger"
  13. size="mini"
  14. icon="ibps-icon-close"
  15. @click="goRemove"
  16. >删除</el-button
  17. >
  18. <el-button
  19. v-if="!isCul"
  20. type="success"
  21. size="mini"
  22. icon="ibps-icon-plus"
  23. @click="goAdd"
  24. >添加</el-button
  25. >
  26. </div>
  27. </el-col>
  28. </el-row>
  29. <el-row type="flex">
  30. <el-col>
  31. <el-table
  32. :data="forms"
  33. border
  34. @selection-change="handleSelectionChange"
  35. >
  36. <el-table-column width="50" type="selection" />
  37. <el-table-column label="参数名称" prop="label">
  38. <template slot-scope="{ row }">
  39. <el-input
  40. v-if="!readonly && !isCul"
  41. v-model="row.label"
  42. size="mini"
  43. placeholder="请输入"
  44. />
  45. <span v-else>{{ row.label || '/' }}</span>
  46. </template>
  47. </el-table-column>
  48. <el-table-column label="参数单位" prop="unit" width="120">
  49. <template slot-scope="{ row }">
  50. <el-autocomplete
  51. v-if="!readonly && !isCul"
  52. v-model="row.unit"
  53. class="inline-input"
  54. :fetch-suggestions="querySearch"
  55. placeholder="请输入"
  56. size="mini"
  57. />
  58. <span v-else>{{ row.unit || '/' }}</span>
  59. </template>
  60. </el-table-column>
  61. <el-table-column label="参数范围限值" prop="range" width="300">
  62. <template slot-scope="{ row }">
  63. <NumberRange
  64. v-model="row.range"
  65. :precision="2"
  66. :disabled="readonly || isCul"
  67. /></template>
  68. </el-table-column>
  69. <el-table-column label="参数修正值" prop="fixValue" width="120">
  70. <template slot-scope="{ row }">
  71. <el-input
  72. v-if="!readonly && !isCul"
  73. v-model="row.fixValue"
  74. size="mini"
  75. placeholder="请输入"
  76. type="number"
  77. />
  78. <span v-else>{{ row.fixValue || '/' }}</span>
  79. </template>
  80. </el-table-column>
  81. <el-table-column label="填写频率" prop="rate">
  82. <template slot="header" slot-scope="">
  83. <el-tooltip
  84. class="item"
  85. effect="dark"
  86. content="配置参数填写周期,适用于参数填写频率与监测周期不一致的情况。若为空则表示该参数每次推送都需填写。"
  87. placement="top-start"
  88. >
  89. <span>
  90. 填写频率
  91. <i class="el-icon-question" />
  92. </span>
  93. </el-tooltip>
  94. </template>
  95. <template slot-scope="{ row, $index }">
  96. <span>{{ row.rate || '/' }}</span>
  97. <el-button
  98. v-if="!readonly && !isCul"
  99. size="mini"
  100. icon="ibps-icon-edit"
  101. type="text"
  102. style="margin-left: 10px"
  103. @click="openDialog(row, $index)"
  104. >编辑</el-button
  105. >
  106. </template>
  107. </el-table-column>
  108. <el-table-column v-if="isCul" label="参数值" prop="value">
  109. <template slot-scope="{ row }">
  110. <el-input
  111. v-if="!readonly"
  112. v-model="row.value"
  113. size="mini"
  114. placeholder="请输入"
  115. type="number"
  116. :readonly="readonly"
  117. />
  118. <span v-else>{{ row.value || '/' }}</span>
  119. </template>
  120. </el-table-column>
  121. <el-table-column v-if="isCul" label="最终值" prop="result">
  122. <template slot-scope="{ row }">
  123. <span>{{ formatResult(row.result) }}</span>
  124. </template>
  125. </el-table-column>
  126. <el-table-column v-if="isCul" label="状态" prop="status">
  127. <template slot-scope="{ row }">
  128. <span>{{ row.status || '/' }}</span>
  129. </template>
  130. </el-table-column>
  131. </el-table>
  132. </el-col>
  133. </el-row>
  134. <FacilityDataDialog
  135. v-if="dialogShow"
  136. :dialog-visible.sync="dialogShow"
  137. :params="dialogParams"
  138. @submit="dialogSubmit"
  139. />
  140. </div>
  141. </template>
  142. <script>
  143. import NumberRange from '@/views/component/xcomponent/numberRange.vue'
  144. import FacilityDataDialog from './facilityDataDialog.vue'
  145. import { getSetting } from '@/utils/query'
  146. export default {
  147. components: {
  148. NumberRange,
  149. FacilityDataDialog
  150. },
  151. props: {
  152. formData: {
  153. type: Object,
  154. default: () => {}
  155. },
  156. readonly: {
  157. type: Boolean,
  158. default: false
  159. },
  160. isCul: {
  161. type: Boolean,
  162. default: true
  163. }
  164. },
  165. data() {
  166. return {
  167. dialogParams: {},
  168. dialogShow: false,
  169. isFirst: true,
  170. forms: [],
  171. multipleSelection: [],
  172. baoliuxiaoshu: true,
  173. config: 2,
  174. isUpdatingFromParent: false // 标记是否来自父组件的更新
  175. }
  176. },
  177. computed: {},
  178. watch: {
  179. formData: {
  180. /*
  181. handler(val) {
  182. console.log('formData', val)
  183. if (val.lieBiaoShuJu) {
  184. if (!this.isCul) {
  185. this.forms = this.initData(val.lieBiaoShuJu)
  186. } else {
  187. if (this.isFirst) {
  188. this.forms = this.initData(val.lieBiaoShuJu)
  189. this.isFirst = false
  190. }
  191. }
  192. }
  193. this.culXiuZheng()
  194. },
  195. */
  196. handler(val) {
  197. if (val.lieBiaoShuJu) {
  198. // 标记开始内部更新,避免无限循环调用initData导致卡死(控制forms里的this.$emit的循环调用)
  199. this.isUpdatingFromParent = true
  200. //现在每一次watch到数据变化,都会执行initData方法
  201. if (!this.isCul) {
  202. this.forms = this.initData(val.lieBiaoShuJu)
  203. } else {
  204. this.forms = this.initData(val.lieBiaoShuJu)
  205. }
  206. }
  207. this.culXiuZheng()
  208. },
  209. deep: true,
  210. immediate: true
  211. },
  212. forms: {
  213. /*
  214. handler(val) {
  215. // console.log('forms', val)
  216. this.culXiuZheng()
  217. this.$emit('change-data', 'lieBiaoShuJu', JSON.stringify(val))
  218. },
  219. */
  220. handler(val) {
  221. this.culXiuZheng()
  222. //只有非内部更新时才向父组件发送数据(避免无限循环的调用导致卡死,由formData里的方法对该变量控制)
  223. if (!this.isUpdatingFromParent) {
  224. this.$emit('change-data', 'lieBiaoShuJu', JSON.stringify(val))
  225. } else {
  226. this.isUpdatingFromParent = false // 重置标记
  227. }
  228. },
  229. deep: true
  230. }
  231. },
  232. async mounted() {
  233. const { setting = {} } = this.$store.getters
  234. const { baoliuxiaoshu = true } = (await getSetting('facilityEnv')) || {}
  235. this.baoliuxiaoshu = baoliuxiaoshu
  236. if (
  237. this.$utils.isNotEmpty(setting.facilityEnv) &&
  238. this.$utils.isNotEmpty(setting.facilityEnv.precision)
  239. ) {
  240. this.config = setting.facilityEnv.precision
  241. }
  242. },
  243. methods: {
  244. // 弹窗的提交事件
  245. dialogSubmit(data, index) {
  246. this.$set(this.forms, index, data)
  247. // console.log('form', this.forms)
  248. },
  249. openDialog(row, $index) {
  250. this.dialogParams = {
  251. ...row,
  252. $index
  253. }
  254. this.dialogShow = true
  255. },
  256. // 计算时间差
  257. getDayDifference(date1, date2) {
  258. // 将两个日期转换为Date对象
  259. const d1 = new Date(date1)
  260. const d2 = new Date(date2)
  261. // 计算两个日期之间的毫秒差
  262. const timeDiff = d2.getTime() - d1.getTime()
  263. // 将毫秒差转换为天数并返回
  264. const dayDiff = Math.ceil(timeDiff / (1000 * 3600 * 24))
  265. // console.log(dayDiff)
  266. return dayDiff
  267. },
  268. // 初始化数据
  269. initData(data) {
  270. data = data.replace(/\n/g, '')
  271. const ans = JSON.parse(data)
  272. const result = []
  273. if (this.isFirst) {
  274. console.log(ans)
  275. console.log('formData', this.formData)
  276. }
  277. ans.forEach((item) => {
  278. if (!Object.hasOwn(item, 'label')) {
  279. item.label = ''
  280. }
  281. if (!Object.hasOwn(item, 'unit')) {
  282. item.unit = '℃'
  283. }
  284. if (!Object.hasOwn(item, 'range')) {
  285. item.range = [null, null]
  286. }
  287. if (!Object.hasOwn(item, 'fixValue')) {
  288. item.fixValue = ''
  289. }
  290. if (!Object.hasOwn(item, 'value')) {
  291. item.value = ''
  292. }
  293. if (!Object.hasOwn(item, 'result')) {
  294. item.result = ''
  295. }
  296. if (!Object.hasOwn(item, 'rate')) {
  297. item.rate = ''
  298. }
  299. if (!Object.hasOwn(item, 'status')) {
  300. item.status = ''
  301. }
  302. if (!Object.hasOwn(item, 'period')) {
  303. item.period = ''
  304. }
  305. if (!Object.hasOwn(item, 'date')) {
  306. item.date = ''
  307. }
  308. if (!Object.hasOwn(item, 'startTime')) {
  309. item.startTime = ''
  310. }
  311. // this.formData.jianCeShiJian = '2025-03-01' // 测试用
  312. if (
  313. !item.result &&
  314. item.rate &&
  315. (this.formData.jianCeShiJian ||
  316. this.formData.bianZhiShiJian ||
  317. this.readonly)
  318. ) {
  319. let jianCeShiJian = ''
  320. if (this.readonly || !this.formData.jianCeShiJian) {
  321. jianCeShiJian = this.formData.bianZhiShiJian?.slice(0, 10)
  322. } else {
  323. jianCeShiJian = this.formData.jianCeShiJian?.slice(0, 10)
  324. }
  325. const thatDate = new Date(jianCeShiJian)
  326. let day = thatDate.getDay() // 周几
  327. if (day === 0) {
  328. // 周日得到结果为0 需要特判
  329. day = 7
  330. }
  331. const dayofMonth = thatDate.getDate() // 多少号
  332. const monthofYear = thatDate.getMonth() + 1 // 多少月
  333. const monthInQuarter = monthofYear % 3 === 0 ? 3 : monthofYear % 3 // 计算当前月在当前季度中的第几个月
  334. let dayDiff = ''
  335. console.log(`监测时间:${jianCeShiJian},周${day}`)
  336. switch (item.period) {
  337. case 'day':
  338. if (item.date.indexOf(day) > -1) {
  339. result.push(item)
  340. }
  341. break
  342. case 'week':
  343. if (item.date === day) {
  344. result.push(item)
  345. }
  346. break
  347. case 'month':
  348. if (item.date === dayofMonth) {
  349. result.push(item)
  350. }
  351. break
  352. case 'quarter':
  353. if (item.date === monthInQuarter && dayofMonth === 1) {
  354. result.push(item)
  355. }
  356. break
  357. case 'repeat':
  358. dayDiff = this.getDayDifference(item.startTime, jianCeShiJian)
  359. if (dayDiff >= 0 && dayDiff % (item.date + 1) === 0) {
  360. result.push(item)
  361. }
  362. break
  363. default:
  364. break
  365. }
  366. } else {
  367. result.push(item)
  368. }
  369. })
  370. return result
  371. },
  372. // 默认单位
  373. querySearch(queryString, cb) {
  374. const units = [
  375. { value: '℃' },
  376. { value: '%' },
  377. { value: 'Pa' },
  378. { value: 'MΩ·CM' },
  379. { value: 'uS/cm' },
  380. { value: 'cfu/ml' }
  381. ]
  382. // 调用 callback 返回建议列表的数据
  383. cb(units)
  384. },
  385. // 计算状态
  386. getStatus(range, result) {
  387. let [min, max] = range
  388. if (min === null || min === '' || typeof min === 'undefined')
  389. min = Number.NEGATIVE_INFINITY
  390. if (max === null || max === '' || typeof max === 'undefined')
  391. max = Number.POSITIVE_INFINITY
  392. if (+min === 0 && +max === 0) {
  393. return '正常'
  394. }
  395. if (+result <= +max && +result >= +min) {
  396. return '正常'
  397. }
  398. return '失控'
  399. },
  400. // 计算修正值
  401. culXiuZheng() {
  402. if (!this.isCul) return
  403. if (this.readonly) return
  404. const self = this
  405. this.forms.forEach((item) => {
  406. if (item.value) {
  407. if (!self.baoliuxiaoshu && self.formData.leiXing === '纯水机') {
  408. //绍兴环境,纯水机不需要保留小数
  409. if (item.fixValue) {
  410. item.result = self.addStringDecimals(item.fixValue, item.value)
  411. } else {
  412. item.result = +item.value
  413. }
  414. } else {
  415. //其他环境、其他页面
  416. if (item.fixValue) {
  417. item.result = (+item.fixValue + +item.value).toFixed(this.config)
  418. } else {
  419. item.result = +item.value //无修正值时不处理小数位
  420. }
  421. }
  422. item.status = this.getStatus(item.range, item.result)
  423. } else {
  424. item.result = ''
  425. item.status = ''
  426. }
  427. })
  428. },
  429. // 展示用格式化:保留两位小数(向下取值),末尾可省略的0不展示
  430. formatResult(val) {
  431. if (val === '' || val === null || val === undefined) return '/'
  432. const num = Number(val)
  433. if (Number.isNaN(num)) return val
  434. // 向下取值保留两位小数
  435. const floored = Math.floor(num * 100) / 100
  436. let str = String(floored)
  437. if (str.includes('.')) {
  438. str = str.replace(/0+$/, '').replace(/\.$/, '')
  439. }
  440. return str
  441. },
  442. formatData(val) {
  443. return JSON.stringify(val)
  444. },
  445. goAdd() {
  446. if (this.forms.length >= 10) {
  447. return this.$message({
  448. message: '超过最大限制!',
  449. type: 'warning'
  450. })
  451. }
  452. this.forms.push({
  453. label: '',
  454. range: [],
  455. fixValue: '',
  456. value: '',
  457. result: '',
  458. status: '',
  459. unit: '',
  460. period: '',
  461. date: ''
  462. })
  463. },
  464. addStringDecimals(a, b) {
  465. // 分割整数和小数部分
  466. const [int1, dec1 = ''] = a.split('.')
  467. const [int2, dec2 = ''] = b.split('.')
  468. // 计算最大小数位数
  469. const maxDecimal = Math.max(dec1.length, dec2.length)
  470. // 小数部分补零对齐
  471. const paddedDec1 = dec1.padEnd(maxDecimal, '0')
  472. const paddedDec2 = dec2.padEnd(maxDecimal, '0')
  473. // 拼接完整整数(原值 × 10^maxDecimal)
  474. const fullInt1 = int1 + paddedDec1
  475. const fullInt2 = int2 + paddedDec2
  476. // 大整数相加
  477. let carry = 0
  478. let result = ''
  479. let i = fullInt1.length - 1
  480. let j = fullInt2.length - 1
  481. while (i >= 0 || j >= 0 || carry) {
  482. const digit1 = i >= 0 ? parseInt(fullInt1[i--], 10) : 0
  483. const digit2 = j >= 0 ? parseInt(fullInt2[j--], 10) : 0
  484. const sum = digit1 + digit2 + carry
  485. carry = Math.floor(sum / 10)
  486. result = (sum % 10) + result
  487. }
  488. // 处理前导零
  489. result = result.replace(/^0+/, '') || '0'
  490. // 插入小数点
  491. if (maxDecimal > 0) {
  492. const pos = result.length - maxDecimal
  493. if (pos <= 0) {
  494. // 整数部分为零的情况
  495. result = '0.' + result.padStart(maxDecimal, '0')
  496. } else {
  497. // 正常插入小数点
  498. result = result.slice(0, pos) + '.' + result.slice(pos)
  499. }
  500. }
  501. return result
  502. },
  503. goRemove() {
  504. this.forms = this.forms.filter(
  505. (item) => !this.multipleSelection.includes(item)
  506. )
  507. },
  508. handleSelectionChange(val) {
  509. this.multipleSelection = val
  510. }
  511. }
  512. }
  513. </script>
  514. <style lang="scss" scoped>
  515. .ficily-data {
  516. margin-bottom: 20px;
  517. .button {
  518. display: flex;
  519. flex-direction: row-reverse;
  520. .el-button {
  521. margin-left: 5px;
  522. }
  523. }
  524. }
  525. </style>