index.vue 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085
  1. <template>
  2. <a-card size="small" class="explorer-monitor-line" :style="monitorLineCardStyle">
  3. <div slot="title" v-if="title">
  4. <a-row type="flex">
  5. <a-col>
  6. <a class="font-weight-bold h-100 d-block" style="margin-right: 6px;" @click="toggleShowTableLegend">
  7. <a-icon type="line-chart" style="font-size: 14px;" v-if="showTableLegend" />
  8. <a-icon type="credit-card" style="font-size: 14px;" v-if="!showTableLegend" />
  9. </a>
  10. </a-col>
  11. <a-col :span="21">
  12. {{ title }}
  13. </a-col>
  14. </a-row>
  15. </div>
  16. <div slot="extra" v-if="showTableExport && !isTemplate">
  17. <a-button v-if="showTableExport && curPager.total" type="link" :title="$t('monitor.full_export')" @click="exportTable">
  18. {{ $t('table.action.export') }}
  19. </a-button>
  20. <slot name="extra" />
  21. </div>
  22. <loader v-if="loading" :loading="true" />
  23. <template v-else>
  24. <div class="d-flex">
  25. <uchart :data="uChartData" :options="uChartOptions" :otherCursorMovePoint="otherCursorMovePoint" />
  26. <div v-if="alertHandlerShow && lineChartOptionsC.dataset.length" class="alert-handler-wrapper position-relative">
  27. <div class="position-absolute clearfix d-flex align-items-center" :style="{ top: `${topStyleRange[1]}px` }">
  28. <div class="alert-handler-line" />
  29. <div class="alert-handler"> {{ formatThreshold }}</div>
  30. </div>
  31. </div>
  32. </div>
  33. <vxe-grid
  34. v-if="tableData && tableData.length && showTable && showTableLegend"
  35. max-height="500"
  36. size="mini"
  37. border
  38. row-id="raw_name"
  39. ref="tableRef"
  40. highlight-hover-row
  41. class="mt-3"
  42. :columns="columns"
  43. :data="tableData"
  44. :row-style="getRowStyle"
  45. resizable
  46. :sort-config="sortConfig"
  47. @cell-click="cellClick"
  48. @sort-change="sortChange" />
  49. <div class="vxe-grid--pager-wrapper" v-if="showTableLegend">
  50. <div class="vxe-pager size--mini">
  51. <div class="vxe-pager--wrapper">
  52. <span class="vxe-pager--total" v-if="!pager || (pager && pager.total < 11)">{{ total }}</span>
  53. <vxe-pager
  54. v-else
  55. size="mini"
  56. @page-change="pageChange"
  57. :page-sizes="getPageSizes"
  58. :current-page.sync="curPager.page"
  59. :page-size.sync="curPager.limit"
  60. :total="curPager.total" />
  61. </div>
  62. </div>
  63. </div>
  64. </template>
  65. </a-card>
  66. </template>
  67. <script>
  68. import * as R from 'ramda'
  69. import _ from 'lodash'
  70. import XLSX from 'xlsx'
  71. import { metric_zh, tableColumnMaps } from '@Monitor/constants'
  72. import { ColorHash } from '@/utils/colorHash'
  73. import { transformUnit } from '@/utils/utils'
  74. import { getChartTooltipLabel } from '@Monitor/utils'
  75. const MAX_COLUMNS = 10
  76. export default {
  77. name: 'ExplorerMonitorLine',
  78. components: {
  79. },
  80. props: {
  81. isTemplate: {
  82. type: Boolean,
  83. default: false,
  84. },
  85. unit: {
  86. type: Object,
  87. default: () => ({}),
  88. },
  89. series: {
  90. type: Array,
  91. required: true,
  92. },
  93. reducedResult: {
  94. type: Object,
  95. default: () => ({}),
  96. },
  97. reducedResultOrder: {
  98. type: String,
  99. default: '',
  100. },
  101. pager: {
  102. type: Object,
  103. required: false,
  104. },
  105. timeFormatStr: {
  106. type: String,
  107. default: 'YYYY-MM-DD HH:mm',
  108. },
  109. lineChartOptions: {
  110. type: Object,
  111. default: () => ({
  112. legend: {
  113. show: false,
  114. selectedMode: 'multiple',
  115. selected: {},
  116. },
  117. }),
  118. },
  119. metricInfo: {
  120. type: Object,
  121. default: () => ({}),
  122. },
  123. loading: {
  124. type: Boolean,
  125. default: false,
  126. },
  127. description: {
  128. type: Object,
  129. },
  130. threshold: {
  131. },
  132. showTableExport: {
  133. type: Boolean,
  134. default: false,
  135. },
  136. showTableLegend: {
  137. type: Boolean,
  138. default: true,
  139. },
  140. monitorLineCardStyle: {
  141. type: Object,
  142. default: () => ({}),
  143. },
  144. otherCursorMovePoint: {
  145. type: Array,
  146. default: () => {
  147. return [-10, -10]
  148. },
  149. },
  150. },
  151. data () {
  152. return {
  153. chartInstanceOption: {
  154. series: [],
  155. },
  156. lineChartOptionsC: {},
  157. chartInstance: null,
  158. seriesOldClickName: null,
  159. highlight: {
  160. index: null,
  161. color: '',
  162. },
  163. highlights: [],
  164. colors: [],
  165. yMax: 0,
  166. alertHandlerShow: false,
  167. topStyleRange: [220, 20],
  168. curPager: Object.assign({}, this.pager || { seriesIndex: 0, total: 0, limit: 10, page: 0 }),
  169. }
  170. },
  171. computed: {
  172. fixedSeries () {
  173. if (this.series.length) {
  174. const cols = this.series[0].columns.filter(col => col !== 'time')
  175. if (cols.length > 1) {
  176. const series = []
  177. cols.forEach((col, idx) => {
  178. this.series.forEach(item => {
  179. const seriesItem = {
  180. columns: [col, 'time'],
  181. name: col,
  182. raw_name: col,
  183. points: item.points.map(point => [point[idx], point[point.length - 1]]),
  184. }
  185. series.push(seriesItem)
  186. })
  187. })
  188. return series
  189. } else {
  190. return this.series
  191. }
  192. }
  193. return this.series
  194. },
  195. groupBy () {
  196. const groupBy = _.get(this.metricInfo, 'model.group_by')
  197. if (groupBy && groupBy.length && groupBy[0].type !== 'time' && groupBy[0].type !== 'fill') {
  198. return groupBy
  199. }
  200. return null
  201. },
  202. resultReducer () {
  203. return _.get(this.metricInfo, 'result_reducer')
  204. },
  205. isSelectFunction () {
  206. const select = _.get(this.metricInfo, 'model.select') || []
  207. let ret = ''
  208. select.map(item => {
  209. if (R.is(Array, item)) {
  210. item.map(l => {
  211. if (['mean', 'sum', 'max', 'min'].includes(l.type)) {
  212. ret = l.type
  213. }
  214. })
  215. } else if (R.is(Object, item)) {
  216. if (['mean', 'sum', 'max', 'min'].includes(item.type)) {
  217. ret = item.type
  218. }
  219. }
  220. })
  221. return ret
  222. },
  223. showTable () {
  224. if (this.isTemplate) {
  225. return false
  226. }
  227. if (!this.groupBy && !this.resultReducer && this.isSelectFunction && this.columns.length < 2) {
  228. return false
  229. }
  230. return true
  231. },
  232. tableData () {
  233. return this.getTableData(this.fixedSeries, this.reducedResult)
  234. },
  235. total () {
  236. const total = this.tableData.length || 0
  237. return this.$t('monitor_metric_78', [total])
  238. },
  239. columns () {
  240. const that = this
  241. const columns = [
  242. {
  243. field: 'color',
  244. width: 50,
  245. slots: {
  246. default: ({ row, rowIndex }) => {
  247. if (this.highlights.some(item => item.index === rowIndex)) {
  248. return [<icon type="checkbox-fill" style={{ fontSize: '20px', color: (that.colors.length && that.colors[rowIndex]) || that.colorHash.hex(`${rowIndex * 1000}`), cursor: 'pointer', transform: 'translateY(3px)' }}></icon>]
  249. }
  250. return [<icon type="checkbox-empty" style="font-size:20px;cursor:pointer;transform:translateY(3px)"></icon>]
  251. },
  252. header: ({ column }, h) => {
  253. let type = 'checkbox-empty'
  254. if (this.isAllSelected) {
  255. type = 'checkbox-fill'
  256. } else if (this.highlights.length) {
  257. type = 'checkbox-some'
  258. }
  259. return [
  260. this.$createElement('icon', {
  261. props: {
  262. type,
  263. },
  264. style: {
  265. fontSize: '20px',
  266. cursor: 'pointer',
  267. transform: 'translateY(3px)',
  268. color: 'var(--antd-wave-shadow-color)',
  269. },
  270. on: {
  271. click: this.checkAllClick,
  272. },
  273. }),
  274. ]
  275. },
  276. },
  277. },
  278. ]
  279. if (this.tableData && this.tableData.length) {
  280. R.forEachObjIndexed((value, key) => {
  281. const isColumn = !R.isNil(this.tableData[0][key])
  282. if (isColumn) {
  283. const measurement = _.get(this.metricInfo, 'model.measurement')
  284. if ((this.description && this.description.metric_res_type === 'guest') || measurement.startsWith('vm_')) {
  285. if (value.field.startsWith('host')) {
  286. return
  287. }
  288. }
  289. columns.push({
  290. ...value,
  291. formatter: ({ cellValue }) => {
  292. if (cellValue === 'value') {
  293. const display_name = this.description.display_name
  294. const metric = this.metricInfo.model.measurement + '_' + this.description.name
  295. let label = metric_zh[display_name]
  296. if (label) {
  297. label += '/' + metric
  298. } else {
  299. label = metric
  300. }
  301. return `${this.description.label || label}${this.isSelectFunction ? `(${this.isSelectFunction.toUpperCase()})` : ''}` || cellValue
  302. }
  303. return cellValue
  304. },
  305. slots: {
  306. default: ({ row, rowIndex }) => {
  307. const cellValue = row[value.field]
  308. let val = cellValue
  309. if (cellValue === 'value') {
  310. const display_name = this.description.display_name
  311. const metric = this.metricInfo.model.measurement + '_' + this.description.name
  312. let label = metric_zh[display_name]
  313. if (label) {
  314. label += '/' + metric
  315. } else {
  316. label = metric
  317. }
  318. val = `${this.description.label || label}${this.isSelectFunction ? `(${this.isSelectFunction.toUpperCase()})` : ''}` || cellValue
  319. }
  320. return [<span>{val}</span>]
  321. },
  322. },
  323. })
  324. }
  325. }, tableColumnMaps)
  326. }
  327. const groupByFields = (this.groupBy || []).map(item => _.get(item, 'params[0]'))
  328. if (this.groupBy && groupByFields.length) {
  329. groupByFields.forEach(groupByField => {
  330. if (!columns.find(val => val.field === groupByField)) {
  331. const title = this.$te(`dictionary.${groupByField}`) ? this.$t(`dictionary.${groupByField}`) : groupByField
  332. columns.push({
  333. field: groupByField,
  334. title,
  335. formatter: ({ row }) => row[groupByField] || '-',
  336. slots: {
  337. default: ({ row, rowIndex }) => {
  338. const val = row[groupByField] || '-'
  339. return [<span>{val}</span>]
  340. },
  341. },
  342. })
  343. }
  344. })
  345. }
  346. if (this.reducedResult && this.reducedResult.reducer) {
  347. const { reducer = {} } = this.reducedResult
  348. const title = reducer.type === 'percentile' ? `P${reducer.params && reducer.params[0]}` : reducer.type === 'avg' ? 'MEAN' : reducer.type.toUpperCase()
  349. columns.push({
  350. field: 'result',
  351. title,
  352. formatter: ({ cellValue }) => {
  353. const unit = _.get(this.description, 'description.unit') || _.get(this.description, 'unit')
  354. const val = transformUnit(cellValue, unit)
  355. return val.text
  356. },
  357. slots: {
  358. default: ({ row, rowIndex }) => {
  359. const cellValue = row.result
  360. const unit = _.get(this.description, 'description.unit') || _.get(this.description, 'unit')
  361. const val = transformUnit(cellValue, unit)
  362. return [<span>{val.text}</span>]
  363. },
  364. },
  365. sortable: true,
  366. })
  367. }
  368. if (this.tableData && this.tableData.length && this.tableData.some(item => {
  369. return item.raw_name && item.raw_name.startsWith('{')
  370. })) {
  371. columns.push({
  372. field: 'raw_name',
  373. title: this.$t('cloudenv.text_237'),
  374. slots: {
  375. default: ({ row, rowIndex }) => {
  376. const val = row.raw_name || ''
  377. return [<span>{val}</span>]
  378. },
  379. },
  380. formatter: ({ row }) => {
  381. return row.raw_name || ''
  382. },
  383. })
  384. }
  385. // 只有raw_name 一列可以展示
  386. if (columns.length === 1) {
  387. columns.push({
  388. field: 'raw_name',
  389. title: this.$t('monitor.monitor_metric'),
  390. slots: {
  391. default: ({ row, rowIndex }) => {
  392. const val = (row.raw_name || '').replace('unknown-0-', '')
  393. return [<span>{val}</span>]
  394. },
  395. },
  396. formatter: ({ row }) => {
  397. return (row.raw_name || '').replace('unknown-0-', '')
  398. },
  399. })
  400. }
  401. return columns.slice(0, MAX_COLUMNS)
  402. },
  403. formatThreshold () {
  404. if (!this.threshold) return '0'
  405. const unit = _.get(this.description, 'description.unit') || _.get(this.description, 'unit')
  406. let formatStr = '0'
  407. if (unit === '%') { // 比如用户输入 2.22%,这里传入为 '0.00'
  408. const sLen = this.threshold.length
  409. const sArr = []
  410. for (let i = 0; i < sLen; i++) {
  411. const s = this.threshold[i]
  412. const v = /\d/.test(s) ? '0' : s
  413. sArr.push(v)
  414. }
  415. formatStr = sArr.join('')
  416. }
  417. const ret = transformUnit(this.threshold, unit, 1000, formatStr)
  418. return `${ret.value}${ret.unit}`
  419. },
  420. title () {
  421. let title = ''
  422. if (this.fixedSeries.length && this.description) {
  423. title = this.description.title || ''
  424. if (this.description.metric_res_type && this.description.metric_res_type === 'host') {
  425. if (this.$te(`dictionary.${this.description.metric_res_type}`)) {
  426. const hostPrefix = this.$t(`dictionary.${this.description.metric_res_type}`)
  427. if (!title.startsWith(hostPrefix)) {
  428. title = hostPrefix + title
  429. }
  430. }
  431. }
  432. }
  433. return title
  434. },
  435. sortConfig () {
  436. if (this.reducedResultOrder && this.columns.some(item => item.field === 'result')) {
  437. return {
  438. defaultSort: {
  439. field: 'result',
  440. order: this.reducedResultOrder,
  441. },
  442. }
  443. }
  444. return {}
  445. },
  446. getPageSizes () {
  447. const ret = [10, 20, 50, 100, 200]
  448. if (this.curPager.Total > ret[ret.length - 1]) {
  449. ret.push(this.curPager.Total)
  450. }
  451. return ret
  452. },
  453. isAllSelected () {
  454. return this.highlights.length === this.tableData.length
  455. },
  456. uChartData () {
  457. const ret = []
  458. if (this.fixedSeries.length) {
  459. const time = this.fixedSeries[0].points.map(item => item[item.length - 1])
  460. ret.push(time.map(item => item / 1000))
  461. this.fixedSeries.forEach((item, i) => {
  462. const row = []
  463. time.forEach(t => {
  464. const target = item.points.filter(p => p[1] === t)
  465. if (target.length) {
  466. row.push(target[0][0])
  467. } else {
  468. row.push(null)
  469. }
  470. })
  471. if (this.highlights.some(item => item.index === i)) {
  472. ret.push(row)
  473. }
  474. })
  475. }
  476. return ret
  477. },
  478. uChartOptions () {
  479. let unit = _.get(this.description, 'description.unit') || _.get(this.description, 'unit')
  480. // 检查数据是否为空(只有时间轴,没有数据系列)
  481. const isEmptyData = !this.uChartData || this.uChartData.length <= 1
  482. let yTicks
  483. let finalYMin
  484. let finalYMax
  485. if (isEmptyData) {
  486. // 如果数据为空,不显示刻度线,设置默认范围
  487. yTicks = []
  488. finalYMin = 0
  489. finalYMax = 100
  490. } else {
  491. // 检查单位是否为100%且数据都在0-100之间
  492. const isPercent100 = (unit === '%' || unit === '100%') && this.isDataInRange0To100(this.uChartData)
  493. if (isPercent100) {
  494. // 如果单位是100%且数据都在0-100之间,使用固定的刻度线
  495. yTicks = [0, 20, 40, 60, 80, 100]
  496. finalYMin = 0
  497. finalYMax = 100
  498. } else {
  499. // 检查数据是否都是整数
  500. const isInteger = this.isAllIntegers(this.uChartData)
  501. // 计算 Y 轴范围
  502. const minRange = isInteger ? 1 : 0.01
  503. const [yMin, yMax] = this.calculateYAxisRange(this.uChartData, minRange)
  504. // 生成 Y 轴刻度(会自动在数据范围外增加刻度线)
  505. yTicks = this.generateYTicks(yMin, yMax, 1, 10, isInteger)
  506. // 使用刻度线的最小值和最大值作为 Y 轴范围
  507. finalYMin = yTicks.length > 0 ? yTicks[0] : yMin
  508. finalYMax = yTicks.length > 0 ? yTicks[yTicks.length - 1] : yMax
  509. }
  510. }
  511. const ret = {
  512. width: '100%',
  513. height: 300,
  514. margin: {
  515. left: 100,
  516. },
  517. scales: {
  518. x: {
  519. time: true, // x轴为时间轴
  520. },
  521. y: {
  522. auto: false, // 禁用自动调整
  523. range: [finalYMin, finalYMax],
  524. },
  525. },
  526. axes: [
  527. {
  528. values: (self, ticks, space) => {
  529. return ticks.map(item => this.$moment(item * 1000).format('MM-DD') + '\n' + this.$moment(item * 1000).format('HH:mm'))
  530. },
  531. },
  532. {
  533. scale: 'y',
  534. size: 75,
  535. splits: (self, scaleMin, scaleMax) => {
  536. // 使用自定义生成的刻度
  537. return yTicks
  538. },
  539. values: (self, ticks, space) => {
  540. if (unit === 'NULL' || unit === 'count') {
  541. unit = ''
  542. }
  543. if (unit === 'ms') { // 时间类型的Y坐标,要取整 如 : 1小时10分钟30秒 -> 1小时
  544. unit = 'intms'
  545. }
  546. const list = ticks.map(item => {
  547. const val = transformUnit(item, unit, 1000, item < 100 ? '0.00' : '0.0')
  548. return val.text > 10000000 ? val.text / 10000 + 'w' : val.text
  549. })
  550. return list
  551. },
  552. },
  553. ],
  554. legend: {
  555. show: false,
  556. },
  557. grid: {
  558. show: false,
  559. },
  560. series: [
  561. {},
  562. ],
  563. tooltip: {
  564. valueFormatter: (value, unit) => {
  565. if (unit === 'NULL' || unit === 'count') {
  566. unit = ''
  567. }
  568. if (unit === 'ms') { // 时间类型的Y坐标,要取整 如 : 1小时10分钟30秒 -> 1小时
  569. unit = 'intms'
  570. }
  571. const val = transformUnit(value, unit, 1000, '0.0000')
  572. return val.text
  573. },
  574. },
  575. cursorMove: (x, y) => {
  576. this.$emit('cursorMove', [x, y])
  577. },
  578. }
  579. this.fixedSeries.forEach((item, i) => {
  580. const color = (this.colors && this.colors[i]) || this.colorHash.hex(`${i * 1000}`)
  581. if (this.highlights.some(item => item.index === i)) {
  582. const label = getChartTooltipLabel(item)
  583. ret.series.push({ label, width: 1, stroke: color, spanGaps: true, unit, color, points: { show: true, size: 3 } })
  584. }
  585. })
  586. return ret
  587. },
  588. },
  589. watch: {
  590. fixedSeries (val, oldV) {
  591. if (!R.equals(val, oldV)) {
  592. this.genColors()
  593. this.getMonitorLine()
  594. }
  595. },
  596. lineChartOptions: {
  597. deep: true,
  598. handler (val, oldV) {
  599. if (!R.equals(val, oldV)) {
  600. this.getMonitorLine()
  601. }
  602. },
  603. },
  604. description (val, oldV) {
  605. if (!R.equals(val, oldV)) {
  606. this.getMonitorLine()
  607. }
  608. },
  609. threshold () {
  610. this.showThreshold()
  611. },
  612. yMax () {
  613. this.showThreshold()
  614. },
  615. pager (val) {
  616. this.curPager = Object.assign({}, val || { seriesIndex: 0, total: 0, limit: 10, page: 0 })
  617. },
  618. tableData (val) {
  619. this.highlights = val.map((item, index) => ({ index, color: item.color }))
  620. },
  621. },
  622. created () {
  623. this.colorHash = new ColorHash({
  624. hue: [
  625. { min: 0, max: 360 },
  626. { min: 0, max: 360 },
  627. { min: 0, max: 360 },
  628. ],
  629. })
  630. this.getMonitorLine()
  631. },
  632. destroyed () {
  633. this.colorHash = null
  634. },
  635. methods: {
  636. // 检查数据是否都是整数
  637. isAllIntegers (uChartData) {
  638. if (!uChartData || uChartData.length <= 1) {
  639. return false
  640. }
  641. for (let i = 1; i < uChartData.length; i++) {
  642. if (Array.isArray(uChartData[i])) {
  643. for (let j = 0; j < uChartData[i].length; j++) {
  644. const val = uChartData[i][j]
  645. if (val !== null && val !== undefined && !isNaN(val)) {
  646. // 检查是否为整数
  647. if (!Number.isInteger(val)) {
  648. return false
  649. }
  650. }
  651. }
  652. }
  653. }
  654. return true
  655. },
  656. // 检查数据是否都在0-100之间
  657. isDataInRange0To100 (uChartData) {
  658. if (!uChartData || uChartData.length <= 1) {
  659. return false
  660. }
  661. // 收集所有有效数值(忽略 null)
  662. const allValues = []
  663. for (let i = 1; i < uChartData.length; i++) {
  664. if (Array.isArray(uChartData[i])) {
  665. uChartData[i].forEach(val => {
  666. if (val !== null && val !== undefined && !isNaN(val)) {
  667. allValues.push(val)
  668. }
  669. })
  670. }
  671. }
  672. if (allValues.length === 0) {
  673. return false
  674. }
  675. const min = Math.min(...allValues)
  676. const max = Math.max(...allValues)
  677. // 检查是否都在0-100之间
  678. return min >= 0 && max <= 100
  679. },
  680. // 计算 Y 轴范围,确保最小范围
  681. calculateYAxisRange (uChartData, minRange = 0.01) {
  682. if (!uChartData || uChartData.length <= 1) {
  683. return [0, 100]
  684. }
  685. // 收集所有有效数值(忽略 null)
  686. const allValues = []
  687. for (let i = 1; i < uChartData.length; i++) {
  688. if (Array.isArray(uChartData[i])) {
  689. uChartData[i].forEach(val => {
  690. if (val !== null && val !== undefined && !isNaN(val)) {
  691. allValues.push(val)
  692. }
  693. })
  694. }
  695. }
  696. if (allValues.length === 0) {
  697. return [0, 100]
  698. }
  699. let min = Math.min(...allValues)
  700. let max = Math.max(...allValues)
  701. const range = max - min
  702. // 如果数据中没有小于0的值,则最小值从0开始
  703. if (min >= 0) {
  704. min = 0
  705. }
  706. // 如果范围小于最小范围,使用固定范围
  707. if (range < minRange) {
  708. const center = (min + max) / 2
  709. // 如果最小值是0,确保不会小于0
  710. if (min === 0) {
  711. min = 0
  712. max = Math.max(max, minRange)
  713. } else {
  714. min = center - minRange / 2
  715. max = center + minRange / 2
  716. }
  717. } else {
  718. // 添加 10% 边距
  719. const padding = range * 0.1
  720. // 如果最小值是0,确保不会小于0
  721. if (min === 0) {
  722. min = 0
  723. } else {
  724. min = min - padding
  725. }
  726. max = max + padding
  727. }
  728. return [min, max]
  729. },
  730. // 生成 Y 轴刻度,确保间隔至少为 minStep,并在数据范围外自动增加刻度线,最少5条刻度线
  731. generateYTicks (min, max, minStep = 0.01, maxTicks = 10, isInteger = false) {
  732. const range = max - min
  733. const minTicks = 5 // 最少刻度线数量
  734. if (range <= 0) {
  735. // 如果最小值和最大值相同,生成至少5条刻度线
  736. const step = isInteger ? 1 : minStep
  737. const ticks = []
  738. // 如果最小值是0,从0开始生成刻度线
  739. if (min === 0) {
  740. for (let i = 0; i < minTicks; i++) {
  741. ticks.push(isInteger ? i * step : parseFloat((i * step).toFixed(10)))
  742. }
  743. } else {
  744. const center = isInteger ? Math.round(min) : min
  745. // 在中心上下各生成至少2条刻度线
  746. for (let i = -2; i <= 2; i++) {
  747. ticks.push(isInteger ? center + i * step : parseFloat((center + i * step).toFixed(10)))
  748. }
  749. }
  750. return ticks
  751. }
  752. // 计算理想步长(基于数据范围,确保至少生成 minTicks 条刻度线)
  753. let step = range / (minTicks - 1)
  754. if (isInteger) {
  755. // 如果数据都是整数,使用整数步长
  756. // 选择合适的整数步长:1, 2, 5, 10, 20, 50, 100, ...
  757. const niceSteps = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]
  758. step = Math.ceil(step)
  759. // 找到最接近的合适步长
  760. for (let i = 0; i < niceSteps.length; i++) {
  761. if (niceSteps[i] >= step) {
  762. step = niceSteps[i]
  763. break
  764. }
  765. }
  766. // 如果步长太大,使用计算出的步长向上取整
  767. if (step > niceSteps[niceSteps.length - 1]) {
  768. step = Math.ceil(step)
  769. }
  770. } else {
  771. // 确保步长不小于 minStep
  772. if (step < minStep) {
  773. step = minStep
  774. } else {
  775. // 将步长调整为 minStep 的倍数(向上取整)
  776. const multiplier = Math.ceil(step / minStep)
  777. step = multiplier * minStep
  778. }
  779. }
  780. // 计算起始刻度(向下取整到 step 的倍数,并在最小值之前至少一个步长)
  781. let startTick = Math.floor(min / step) * step
  782. // 如果最小值是0,确保起始刻度从0开始
  783. if (min === 0) {
  784. startTick = 0
  785. } else {
  786. // 确保起始刻度在最小值之前至少一个步长
  787. while (startTick >= min) {
  788. startTick -= step
  789. }
  790. }
  791. // 计算结束刻度(向上取整到 step 的倍数,并在最大值之后至少一个步长)
  792. let endTick = Math.ceil(max / step) * step
  793. // 确保结束刻度在最大值之后至少一个步长
  794. while (endTick <= max) {
  795. endTick += step
  796. }
  797. // 生成等间距的刻度数组
  798. const ticks = []
  799. let currentTick = startTick
  800. // 生成从起始到结束的所有刻度(包含边界)
  801. while (currentTick <= endTick + step * 0.001) {
  802. if (isInteger) {
  803. ticks.push(Math.round(currentTick))
  804. } else {
  805. ticks.push(parseFloat(currentTick.toFixed(10)))
  806. }
  807. currentTick += step
  808. }
  809. // 确保至少生成 minTicks 条刻度线
  810. if (ticks.length < minTicks) {
  811. // 如果刻度线太少,重新计算步长
  812. const totalRange = endTick - startTick
  813. step = totalRange / (minTicks - 1)
  814. if (isInteger) {
  815. // 整数步长:选择合适的整数步长
  816. const niceSteps = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000]
  817. step = Math.ceil(step)
  818. for (let i = 0; i < niceSteps.length; i++) {
  819. if (niceSteps[i] >= step) {
  820. step = niceSteps[i]
  821. break
  822. }
  823. }
  824. if (step > niceSteps[niceSteps.length - 1]) {
  825. step = Math.ceil(step)
  826. }
  827. } else {
  828. // 确保步长是 minStep 的倍数
  829. const multiplier = Math.ceil(step / minStep)
  830. step = multiplier * minStep
  831. }
  832. // 重新计算起始和结束刻度
  833. if (min === 0) {
  834. // 如果最小值是0,从0开始
  835. startTick = 0
  836. endTick = step * (minTicks - 1)
  837. } else {
  838. const center = (min + max) / 2
  839. const halfRange = (step * (minTicks - 1)) / 2
  840. startTick = Math.floor((center - halfRange) / step) * step
  841. endTick = startTick + step * (minTicks - 1)
  842. }
  843. // 重新生成刻度
  844. ticks.length = 0
  845. currentTick = startTick
  846. while (currentTick <= endTick + step * 0.001) {
  847. if (isInteger) {
  848. ticks.push(Math.round(currentTick))
  849. } else {
  850. ticks.push(parseFloat(currentTick.toFixed(10)))
  851. }
  852. currentTick += step
  853. }
  854. }
  855. return ticks
  856. },
  857. genColors () {
  858. this.colors = this.fixedSeries.map((item, i) => {
  859. return this.colors[i] || this.colorHash.hex(`${i * 1000}`)
  860. })
  861. },
  862. checkAllClick (val) {
  863. const list = []
  864. if (this.isAllSelected) {
  865. this.tableData.map((item, index) => {
  866. if (this.highlights.some(l => l.index === index)) {
  867. list.push({ row: item, rowIndex: index })
  868. }
  869. })
  870. } else {
  871. this.tableData.map((item, index) => {
  872. if (!this.highlights.some(l => l.index === index)) {
  873. list.push({ row: item, rowIndex: index })
  874. }
  875. })
  876. }
  877. this.tableSetHighlights(list)
  878. },
  879. getTableData (series, reducedResult) {
  880. return series.map((val, i) => {
  881. const ret = { ...val.tags, raw_name: val.raw_name }
  882. const showMetric = !!this.groupBy
  883. if (showMetric) {
  884. ret.__metric = val.name
  885. }
  886. const { series, color = [] } = this.chartInstanceOption
  887. const colors = series.map(val => val.itemStyle.color)
  888. const c = colors[i] || color[0]
  889. ret.__color = c
  890. if (reducedResult && reducedResult.result) {
  891. ret.result = reducedResult.result[i]
  892. }
  893. return ret
  894. })
  895. },
  896. pageChange ({ type, currentPage, pageSize, $event }) {
  897. this.$emit('pageChange', { seriesIndex: this.curPager.seriesIndex, total: this.curPager.total, limit: pageSize, page: currentPage })
  898. },
  899. sortChange ({ order }) {
  900. this.$emit('reducedResultOrderChange', order)
  901. },
  902. cellClick ({ row, rowIndex }) {
  903. this.tableSetHighlight({ row, rowIndex })
  904. },
  905. tableSetHighlight ({ row, rowIndex, click }) {
  906. let seriesName = _.get(this.chartInstanceOption, `series[${rowIndex}].name`)
  907. seriesName = seriesName || `series${rowIndex}`
  908. this.highlightSeries([{ seriesName, row, rowIndex }])
  909. },
  910. tableSetHighlights (list) {
  911. const highlights = list.map(item => {
  912. const { row, rowIndex } = item
  913. let seriesName = _.get(this.chartInstanceOption, `series[${rowIndex}].name`)
  914. seriesName = seriesName || `series${rowIndex}`
  915. return { seriesName, row, rowIndex }
  916. })
  917. this.highlightSeries(highlights)
  918. },
  919. setChartInstance (v) {
  920. this.chartInstanceOption = v.getOption()
  921. this.chartInstance = v
  922. this.$emit('chartInstance', v)
  923. this.chartInstance.on('click', params => {
  924. this._cancelHighlight()
  925. this.highlightSeries([{ seriesName: params.seriesName, row: this.tableData[params.seriesIndex], rowIndex: params.seriesIndex }])
  926. })
  927. if (this.chartInstance && R.is(Function, this.chartInstance.getModel)) {
  928. const model = this.chartInstance.getModel()
  929. if (model && R.is(Function, model.getComponent)) {
  930. const component = model.getComponent('yAxis')
  931. const yMax = _.get(component, 'axis.scale._extent[1]')
  932. this.yMax = yMax
  933. }
  934. }
  935. },
  936. highlightSeries (list, seriesName, row, rowIndex) {
  937. let highlights = [...this.highlights]
  938. list.forEach(item => {
  939. const { row, rowIndex } = item
  940. const target = this.highlights.filter(item => item.index === rowIndex)
  941. if (target.length) {
  942. highlights = highlights.filter(item => item.index !== rowIndex)
  943. } else {
  944. highlights = [...highlights, { index: rowIndex, color: row.__color }]
  945. }
  946. })
  947. this.highlights = highlights
  948. },
  949. _cancelHighlight () {
  950. const selected = {}
  951. const option = {
  952. data: this.lineChartOptionsC.series.map(s => { selected[s.name] = true; return s.name }),
  953. selectedMode: 'multiple',
  954. show: false,
  955. selected: selected,
  956. }
  957. this.chartInstance.setOption({
  958. legend: option,
  959. })
  960. },
  961. _setHighlights (seriesNames) {
  962. const selected = {}
  963. const option = {
  964. data: this.lineChartOptionsC.series.map(s => { selected[s.name] = seriesNames.includes(s.name); return s.name }),
  965. selectedMode: 'multiple',
  966. selected: selected,
  967. show: false,
  968. }
  969. this.chartInstance.setOption({
  970. legend: option,
  971. })
  972. },
  973. getMonitorLine () {
  974. this.highlights = this.fixedSeries.map((item, index) => {
  975. return {
  976. index,
  977. }
  978. })
  979. },
  980. getRowStyle ({ $rowIndex, column, columnIndex, $columnIndex }) {
  981. if ($rowIndex === this.highlight.index) {
  982. return {
  983. color: this.highlight.color,
  984. }
  985. }
  986. return null
  987. },
  988. showThreshold () {
  989. if (this.threshold > this.yMax) {
  990. this.alertHandlerShow = true
  991. } else {
  992. this.alertHandlerShow = false
  993. }
  994. },
  995. exportTable () {
  996. const { total, limit } = this.curPager
  997. if (total > limit) {
  998. // 导出全量
  999. this.$emit('exportTable', total)
  1000. } else {
  1001. this.exportData(this.tableData)
  1002. }
  1003. },
  1004. exportFullData (series, reducedResult) {
  1005. const tableData = this.getTableData(series, reducedResult)
  1006. this.exportData(tableData)
  1007. },
  1008. exportData (data) {
  1009. const columns = this.columns.filter(item => item.field !== 'color')
  1010. const list = [[...columns.map(item => item.title)]]
  1011. data.forEach(item => {
  1012. const row = []
  1013. columns.forEach(col => {
  1014. row.push(col.formatter ? col.formatter({ row: item, cellValue: item[col.field] }) : item[col.field])
  1015. })
  1016. list.push(row)
  1017. })
  1018. const filename = `${this.title || 'monitor table'}.xlsx`
  1019. const ws_name = 'Sheet1'
  1020. const wb = XLSX.utils.book_new()
  1021. const ws = XLSX.utils.aoa_to_sheet(list)
  1022. XLSX.utils.book_append_sheet(wb, ws, ws_name)
  1023. XLSX.writeFile(wb, filename)
  1024. },
  1025. toggleShowTableLegend () {
  1026. if (this.isTemplate) {
  1027. return
  1028. }
  1029. this.showTableLegend = !this.showTableLegend
  1030. },
  1031. },
  1032. }
  1033. </script>
  1034. <style lang="less" scoped>
  1035. .explorer-monitor-line {
  1036. ::v-deep .ant-card-body {
  1037. width: 100%;
  1038. }
  1039. .alert-handler-wrapper {
  1040. width: 50px;
  1041. .alert-handler-line {
  1042. background-color: red;
  1043. height: 2px;
  1044. z-index: 0;
  1045. position: relative;
  1046. width: 15px;
  1047. }
  1048. .alert-handler {
  1049. font-size: 12px;
  1050. color: red;
  1051. }
  1052. }
  1053. }
  1054. </style>