spectrogramchartsNew.vue 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156
  1. <template>
  2. <div v-loading="loading">
  3. <!-- ECharts 图表容器 -->
  4. <div class="pannel" v-if="isshow">
  5. 光标数据
  6. <div class="pannel-row" v-show="showReadoutSingle">
  7. <span class="pannel-key">单指针</span>
  8. <span class="pannel-val">{{ cursorReadoutSingle }}</span>
  9. </div>
  10. <div class="pannel-row" v-show="showReadoutSideband">
  11. <span class="pannel-key">边带</span>
  12. <span class="pannel-val">{{ cursorReadoutSideband }}</span>
  13. </div>
  14. <div class="pannel-row" v-show="showReadoutPeak">
  15. <span class="pannel-key">峰值</span>
  16. <span class="pannel-val">{{ cursorReadoutPeak }}</span>
  17. </div>
  18. <div class="pannel-row" v-show="showReadoutHarmonic">
  19. <span class="pannel-key">谐波</span>
  20. <span class="pannel-val">{{ cursorReadoutHarmonic }}</span>
  21. </div>
  22. </div>
  23. <div class="line-chart" ref="chart"></div>
  24. <div class="control-panel">
  25. <!-- 频率范围 -->
  26. <div class="box full-row">
  27. <div class="panel-block">
  28. <span class="label1">频率</span>
  29. <el-input v-model="freqMin" size="mini" placeholder="下限" />
  30. <span>~</span>
  31. <el-input v-model="freqMax" size="mini" placeholder="上限" />
  32. <el-button size="mini" type="primary" @click="handleFreqRange">
  33. 应用
  34. </el-button>
  35. </div>
  36. </div>
  37. <!-- 手动标注 -->
  38. <div class="panel-block">
  39. <span class="label1">标注</span>
  40. <el-input v-model="manualFreq" size="mini" placeholder="频率" />
  41. <el-input v-model="multiple" size="mini" placeholder="倍频" />
  42. <el-button size="mini" type="success" @click="handleMark"
  43. >标注</el-button
  44. >
  45. </div>
  46. <!-- 特征值(多选) -->
  47. <div class="panel-block">
  48. <span class="label">特征 </span>
  49. <el-cascader
  50. v-model="selectedFeatures"
  51. :options="cascaderOptions"
  52. :props="cascaderProps"
  53. collapse-tags
  54. clearable
  55. placeholder="请选择特征"
  56. size="small"
  57. @change="handleCascaderChange"
  58. />
  59. </div>
  60. <!-- 光标(单选) -->
  61. <div class="panel-block">
  62. <span class="label">光标</span>
  63. <div class="btn-group">
  64. <span
  65. v-for="item in GBcheckList"
  66. :key="item.val"
  67. :class="['btn', checkedGB.includes(item.val) ? 'active' : '']"
  68. @click="selectCursor(item.val)"
  69. >
  70. {{ item.val }}
  71. </span>
  72. </div>
  73. </div>
  74. </div>
  75. </div>
  76. </template>
  77. <script>
  78. import axios from "axios";
  79. import * as echarts from "echarts";
  80. import cursorReferenceMixin from "./spectrogramcharts/cursorReferenceMixin";
  81. import Bdgb from "./spectrogramcharts/Bdgb";
  82. import Xdgb from "./spectrogramcharts/Xdgb";
  83. import Tjgb from "./spectrogramcharts/Tjgb";
  84. import failureFrequency from "./data/failureFrequency.json";
  85. export default {
  86. name: "TimedomainCharts",
  87. mixins: [cursorReferenceMixin, Bdgb, Xdgb, Tjgb],
  88. props: {
  89. isshow: {
  90. type: Boolean,
  91. default: false,
  92. },
  93. currentIndex: {
  94. type: Number,
  95. default: 0,
  96. },
  97. activeIndex: {
  98. type: Number,
  99. default: 0,
  100. },
  101. ids: {
  102. type: Array,
  103. default: () => [],
  104. },
  105. spectrumListTwo: {
  106. type: Object,
  107. default: () => ({}),
  108. },
  109. currentRow: {
  110. type: Object,
  111. default: () => ({}),
  112. },
  113. windCode: {
  114. type: String,
  115. default: "",
  116. },
  117. loading: {
  118. type: Boolean,
  119. default: false,
  120. },
  121. },
  122. data() {
  123. return {
  124. freqMin: "",
  125. freqMax: "",
  126. manualFreq: "",
  127. multiple: 1,
  128. manualMarks: [],
  129. chartInstance: null,
  130. option: null,
  131. // TZshow: false,
  132. BGshow: false,
  133. PXshow: false,
  134. spectrumList: {},
  135. GBcheckList: [
  136. { val: "单指针", checked: false, disabled: false },
  137. { val: "谐波光标", checked: false, disabled: false },
  138. { val: "边带光标", checked: false, disabled: false },
  139. { val: "移动峰值", checked: false, disabled: false },
  140. ],
  141. featureGroups: [
  142. {
  143. label: "轴承故障",
  144. type: "bearing",
  145. children: [
  146. {
  147. // GEN
  148. label: "发电机轴承",
  149. type: "bearing",
  150. children: [
  151. "GEN NDE BPFO",
  152. "GEN NDE BPFI",
  153. "GEN NDE BSF",
  154. "GEN NDE FTF",
  155. "GEN-DE BPFO",
  156. "GEN-DE BPFI",
  157. "GEN-DE BSF",
  158. "GEN-DE FTF",
  159. ],
  160. },
  161. {
  162. label: "主轴轴承",
  163. type: "bearing",
  164. children: [
  165. // 主轴轴承
  166. "MB01 BPFO",
  167. "MB01 BPFI",
  168. "MB01 BSF",
  169. "MB01 FTF",
  170. "MB02 BPFO",
  171. "MB02 BPFI",
  172. "MB02 BSF",
  173. "MB02 FTF",
  174. ],
  175. },
  176. {
  177. label: "LSS",
  178. type: "bearing",
  179. children: [
  180. // 行星啮合频率(本质GMF)
  181. "lSS NNCF5048 BPFO",
  182. "lSS NNCF5048 BPFI",
  183. "lSS NNCF5048 BSF",
  184. "lSS NNCF5048 FTF",
  185. ],
  186. },
  187. {
  188. label: "HSS",
  189. type: "bearing",
  190. children: [
  191. // HSS
  192. "HSS-NU2228 BPFO",
  193. "HSS-NU2228 BPFI",
  194. "HSS-NU2228 BSF",
  195. "HSS-NU2228 FTF",
  196. "HSS-QJ328 BPFO",
  197. "HSS-QJ328 BPFI",
  198. "HSS-QJ328 BSF",
  199. "HSS-QJ328 FTF",
  200. ],
  201. },
  202. {
  203. label: "IMS",
  204. type: "bearing",
  205. children: [
  206. // IMS
  207. "IMS-32956 BPFO",
  208. "IMS-32956 BPFI",
  209. "IMS-32956 BSF",
  210. "IMS-32956 FTF",
  211. "IMS-NU1052 BPFO",
  212. "IMS-NU1052 BPFI",
  213. "IMS-NU1052 BSF",
  214. "IMS-NU1052 FTF",
  215. "IMS-NU2324 BPFO",
  216. "IMS-NU2324 BPFI",
  217. "IMS-NU2324 BSF",
  218. "IMS-NU2324 FTF",
  219. ],
  220. },
  221. {
  222. label: "行星轴承(LSS/IMS)",
  223. type: "bearing",
  224. children: [
  225. // 行星轴承(LSS/IMS)
  226. "LSS BPFO",
  227. "LSS BPFI",
  228. "LSS BSF",
  229. "LSS FTF",
  230. "IMS BPFO", //中间轴
  231. "IMS BPFI",
  232. "IMS BSF",
  233. "IMS FTF",
  234. ],
  235. },
  236. ],
  237. },
  238. {
  239. label: "转动基频",
  240. type: "rotation",
  241. children: [
  242. "Speed",
  243. "MainShaft",
  244. "LSS Planet shaft",
  245. "IMS Planet shaft",
  246. "Rotor, Rotor bars",
  247. "固定联轴节HHS",
  248. ],
  249. },
  250. {
  251. label: "结构频率",
  252. type: "structure",
  253. children: [
  254. "Blade",
  255. "Stator, Pole pass freq.",
  256. "Stator, Grid freq.",
  257. "Rotor, Grid freq.",
  258. ],
  259. },
  260. {
  261. label: "齿轮特征",
  262. type: "gear",
  263. children: [
  264. "GearIMS",
  265. "GearHHS",
  266. "LSS", //小齿轮
  267. "IMS", //中间轴
  268. ],
  269. },
  270. ],
  271. // 每个分类单独存
  272. selectedMap: {
  273. bearing: [],
  274. rotation: [],
  275. structure: [],
  276. gear: [],
  277. },
  278. // 最终统一给图表用
  279. checkedFeatures: [],
  280. selectedFeatures: [],
  281. cascaderProps: {
  282. multiple: true,
  283. checkStrictly: false,
  284. emitPath: true, // 保留(你需要路径)
  285. value: "value",
  286. label: "label",
  287. },
  288. Fr: [],
  289. BPFI: [],
  290. BPFO: [],
  291. BSF: [],
  292. FTF: [],
  293. B3P: [],
  294. checkedGB: [],
  295. checkedValues: [],
  296. featureMultipleMap: {},
  297. };
  298. },
  299. computed: {
  300. cascaderOptions() {
  301. return this.featureGroups.map((group) => ({
  302. label: group.label,
  303. value: group.label,
  304. children: group.children.map((item) => {
  305. // 其它分组:子项为字符串;「轴承故障」为 { label, children: string[] } 多一级
  306. if (typeof item === "string") {
  307. return { label: item, value: item };
  308. }
  309. if (item && Array.isArray(item.children)) {
  310. return {
  311. label: item.label,
  312. value: item.label,
  313. children: item.children.map((leaf) => ({
  314. label: leaf,
  315. value: leaf,
  316. })),
  317. };
  318. }
  319. return {
  320. label: item.label,
  321. value: item.label,
  322. };
  323. }),
  324. }));
  325. },
  326. spectrumXLabel() {
  327. const t = this.spectrumList?.xaxis;
  328. return t && String(t).trim() ? String(t).trim() : "Hz";
  329. },
  330. spectrumYLabel() {
  331. const t = this.spectrumList?.yaxis;
  332. return t && String(t).trim() ? String(t).trim() : "m/s²";
  333. },
  334. /** 单指针:点击后的频率与幅值(来自 mixin singlePointerPoint) */
  335. cursorReadoutSingle() {
  336. const p = this.singlePointerPoint;
  337. if (!p || p.xAxis == null) return this.formatCursorPair(null, null);
  338. return this.formatCursorPair(p.xAxis, p.val);
  339. },
  340. /** 边带:展示 +1 边带线处的频率与谱幅(与图上「+1」标注一致) */
  341. cursorReadoutSideband() {
  342. if (!this.sidebandCursorPoints?.length || !this.singlePointerPoint) {
  343. return this.formatCursorPair(null, null);
  344. }
  345. const plusOne = this.sidebandCursorPoints.find((p) => p.val === "+1");
  346. if (!plusOne || plusOne.xAxis == null) {
  347. return this.formatCursorPair(null, null);
  348. }
  349. const xData = this.spectrumList?.x;
  350. const yData = this.spectrumList?.y;
  351. if (!xData?.length || !yData?.length) {
  352. return this.formatCursorPair(plusOne.xAxis, null);
  353. }
  354. const idx = this.findClosestIndex(Number(plusOne.xAxis), xData);
  355. const yVal = yData[idx];
  356. const amp =
  357. yVal != null && !Number.isNaN(Number(yVal))
  358. ? Number(yVal).toFixed(6)
  359. : null;
  360. return this.formatCursorPair(plusOne.xAxis, amp);
  361. },
  362. /** 移动峰值:当前峰值光标(cursorHightPoints) */
  363. cursorReadoutPeak() {
  364. const pts = this.cursorHightPoints;
  365. if (!pts?.length) return this.formatCursorPair(null, null);
  366. const p = pts[0];
  367. return this.formatCursorPair(p.xAxis, p.val);
  368. },
  369. /** 谐波:基频(1×)处频率与插值幅值(harmonicCursorPoints[0]) */
  370. cursorReadoutHarmonic() {
  371. const pts = this.harmonicCursorPoints;
  372. if (!pts?.length) return this.formatCursorPair(null, null);
  373. const p = pts[0];
  374. return this.formatCursorPair(p.xAxis, p.yValue);
  375. },
  376. /** v-show 用:字符串恒为真,不能写 v-show="cursorReadoutPeak",需按数据源判断 */
  377. showReadoutSingle() {
  378. return (
  379. this.singlePointerPoint != null && this.singlePointerPoint.xAxis != null
  380. );
  381. },
  382. showReadoutSideband() {
  383. return (
  384. !!this.sidebandCursorPoints?.length &&
  385. this.singlePointerPoint != null &&
  386. this.singlePointerPoint.xAxis != null
  387. );
  388. },
  389. showReadoutPeak() {
  390. return !!(this.cursorHightPoints && this.cursorHightPoints.length);
  391. },
  392. showReadoutHarmonic() {
  393. return !!(this.harmonicCursorPoints && this.harmonicCursorPoints.length);
  394. },
  395. },
  396. watch: {
  397. // immediate:开发热更新重挂载时 props 引用未变,普通 watch 不触发,会导致 spectrumList 仍是 {}、图表空白
  398. spectrumListTwo: {
  399. handler(newValue) {
  400. this.spectrumList =
  401. newValue && typeof newValue === "object" ? newValue : {};
  402. if (
  403. this.chartInstance &&
  404. Array.isArray(this.spectrumList?.y) &&
  405. Array.isArray(this.spectrumList?.x) &&
  406. this.spectrumList.y.length === this.spectrumList.x.length
  407. ) {
  408. this.updateChart(this.spectrumList.y, this.spectrumList.x);
  409. }
  410. },
  411. immediate: true,
  412. deep: true,
  413. },
  414. spectrumList(newVal) {
  415. if (!newVal) return;
  416. this.$nextTick(() => {
  417. if (this.checkedFeatures.length) {
  418. this.updateFeatureLinesByGroup();
  419. }
  420. });
  421. },
  422. },
  423. mounted() {
  424. this.$nextTick(() => {
  425. this.initializeChart();
  426. window.addEventListener("resize", this.handleResize);
  427. // 数据由父组件统一刷新(props: spectrumListTwo),同步依赖 watch immediate
  428. });
  429. },
  430. beforeDestroy() {
  431. window.removeEventListener("resize", this.handleResize);
  432. if (this.chartInstance) {
  433. this.disableSinglePointer?.();
  434. this.chartInstance.getZr().off("dblclick", this.handleDoubleClick);
  435. this.chartInstance.dispose();
  436. this.chartInstance = null;
  437. }
  438. },
  439. methods: {
  440. formatNumericFreq(v) {
  441. const n = Number(v);
  442. if (Number.isNaN(n)) return null;
  443. return Math.abs(n) >= 1000 ? n.toFixed(2) : n.toFixed(4);
  444. },
  445. formatCursorPair(freqNum, ampVal) {
  446. const xl = this.spectrumXLabel;
  447. const yl = this.spectrumYLabel;
  448. const numStr = this.formatNumericFreq(freqNum);
  449. const left = numStr == null ? "—" : `${numStr} ${xl}`;
  450. const amp =
  451. ampVal === null || ampVal === undefined || ampVal === ""
  452. ? null
  453. : String(ampVal);
  454. const right = amp == null ? "—" : `${amp} ${yl}`;
  455. return `${left};${right}`;
  456. },
  457. normalizeFeatureName(name) {
  458. if (!name) return "";
  459. return String(name)
  460. .replace(/GEN-NDE/g, "GEN NDE")
  461. .trim();
  462. },
  463. buildFeatureMultipleMap() {
  464. const map = {};
  465. (failureFrequency || []).forEach((part) => {
  466. (part.faultFrequencies || []).forEach((item) => {
  467. if (!item?.name) return;
  468. const key = this.normalizeFeatureName(item.name);
  469. map[key] = Number(item.multiple);
  470. });
  471. });
  472. // 兼容页面中的中文别名
  473. if (map.Blade != null) {
  474. map.叶片频率 = map.Blade;
  475. }
  476. map.转速频率 = 1;
  477. this.featureMultipleMap = map;
  478. },
  479. getFeatureMultipleByName(name) {
  480. const normalized = this.normalizeFeatureName(name);
  481. if (this.featureMultipleMap[normalized] != null) {
  482. return this.featureMultipleMap[normalized];
  483. }
  484. // 兼容 GEN-NDE / GEN NDE 两种写法
  485. const swapped = normalized.includes("GEN NDE")
  486. ? normalized.replace("GEN NDE", "GEN-NDE")
  487. : normalized.replace("GEN-NDE", "GEN NDE");
  488. if (this.featureMultipleMap[swapped] != null) {
  489. return this.featureMultipleMap[swapped];
  490. }
  491. return null;
  492. },
  493. handleResize() {
  494. this.chartInstance?.resize();
  495. },
  496. handleFeatureChange() {
  497. // 1️⃣ 汇总所有选中的特征
  498. const allSelected = Object.values(this.selectedMap).flat();
  499. this.checkedFeatures = allSelected;
  500. // 2️⃣ 更新图表
  501. this.updateFeatureLinesByGroup();
  502. },
  503. handleCascaderChange(val) {
  504. console.log(this.spectrumListTwo, "spectrumListTwo", this.spectrumList);
  505. this.manualFreq = "";
  506. this.multiple = 1;
  507. if (!val || val.length === 0) {
  508. this.checkedFeatures = [];
  509. this.renderFeatureSeries([]);
  510. return;
  511. }
  512. // 👉 1. 提取最后一层(真正特征)
  513. this.checkedFeatures = val
  514. .map((item) => item[item.length - 1])
  515. .filter(Boolean);
  516. // 👉 3. 更新图表
  517. this.updateFeatureLinesByGroup();
  518. },
  519. selectCursor(val) {
  520. // 光标选择逻辑:
  521. // - 「单指针」与「边带光标」允许同时选中(边带依赖单指针中心)
  522. // - 其它光标(谐波光标 / 移动峰值)保持单选互斥
  523. const isAlreadyChecked = this.checkedGB.includes(val);
  524. const isSinglePointerVal = val === "单指针";
  525. const isSidebandVal = val === "边带光标";
  526. const isHarmonicVal = val === "谐波光标";
  527. const isMovePeakVal = val === "移动峰值";
  528. if (isAlreadyChecked) {
  529. // 取消选中
  530. if (isSinglePointerVal) {
  531. // 单指针被取消时:边带依赖单指针中心,直接级联取消边带并清除边带设置
  532. this.checkedGB = this.checkedGB.filter(
  533. (v) => v !== "单指针" && v !== "边带光标",
  534. );
  535. } else {
  536. // 其它:仅移除当前项
  537. this.checkedGB = this.checkedGB.filter((v) => v !== val);
  538. }
  539. } else if (isSinglePointerVal || isSidebandVal) {
  540. // 单指针/边带:在现有基础上追加(保留对方)
  541. const next = new Set(this.checkedGB);
  542. next.add(val);
  543. // 但如果此时已经选了「谐波光标」或「移动峰值」,需要清掉(它们保持单选)
  544. next.delete("谐波光标");
  545. next.delete("移动峰值");
  546. this.checkedGB = Array.from(next);
  547. } else if (isHarmonicVal || isMovePeakVal) {
  548. // 其它:单选互斥(会清掉单指针/边带)
  549. this.checkedGB = [val];
  550. } else {
  551. this.checkedGB = [val];
  552. }
  553. console.log(this.checkedGB, "this.checkedGB");
  554. const isMoveChecked = this.checkedGB.includes("移动峰值");
  555. const isSidebandChecked = this.checkedGB.includes("边带光标");
  556. const isHarmonicChecked = this.checkedGB.includes("谐波光标");
  557. const isSinglePointer = this.checkedGB.includes("单指针");
  558. // 峰值 / 谐波 与 单指针+边带 互斥:先清边带与单指针线,再画峰值或谐波(避免只清状态、线残留)
  559. if (isMoveChecked || isHarmonicChecked) {
  560. this.disableSidebandCursor();
  561. this.disableSinglePointer();
  562. }
  563. isMoveChecked ? this.handleMoveCursor() : this.removeCursor();
  564. isSidebandChecked
  565. ? this.enableSidebandCursor()
  566. : this.disableSidebandCursor();
  567. isHarmonicChecked
  568. ? this.enableHarmonicCursor()
  569. : this.disableHarmonicCursor();
  570. // 单指针:
  571. // - 只要没选中「单指针」→ 移除单指针线+解绑事件
  572. // - 选中「单指针」且未启用「边带光标」→ 允许点击移动单指针
  573. // - 选中「单指针」且启用「边带光标」→ 冻结单指针(保留线,不允许点击移动)
  574. if (!isSinglePointer) {
  575. this.disableSinglePointer();
  576. } else if (isSidebandChecked) {
  577. // 冻结:仅解绑 click,不删除单指针线
  578. this.unbindSinglePointerClick?.();
  579. } else {
  580. this.enableSinglePointer();
  581. }
  582. },
  583. initializeChart() {
  584. const chartDom = this.$refs.chart;
  585. if (chartDom && !this.chartInstance) {
  586. this.chartInstance = echarts.init(chartDom);
  587. this.chartInstance.getZr().on("dblclick", this.handleDoubleClick);
  588. }
  589. this.$nextTick(() => {
  590. if (this.chartInstance && this.spectrumList.y && this.spectrumList.x) {
  591. this.updateChart(this.spectrumList.y, this.spectrumList.x);
  592. }
  593. });
  594. },
  595. handleFreqRange() {
  596. const min = Number(this.freqMin);
  597. const max = Number(this.freqMax);
  598. if (isNaN(min) || isNaN(max)) return;
  599. this.chartInstance.setOption({
  600. xAxis: { min, max },
  601. });
  602. },
  603. handleMark() {
  604. const freq = Number(this.manualFreq);
  605. const multiple = Number(this.multiple) || 1;
  606. if (isNaN(freq)) return;
  607. const marks = [];
  608. for (let i = 1; i <= multiple; i++) {
  609. marks.push({
  610. xAxis: freq * i,
  611. val: `${i}x`,
  612. });
  613. }
  614. this.manualMarks = marks;
  615. this.renderManualMarks();
  616. },
  617. renderManualMarks() {
  618. const option = this.chartInstance.getOption();
  619. const manualSeries = {
  620. id: "MANUAL_MARK",
  621. type: "line",
  622. markLine: {
  623. symbol: ["none", "none"],
  624. lineStyle: {
  625. color: "#ff0000",
  626. width: 2,
  627. type: "dashed",
  628. },
  629. label: {
  630. formatter: ({ data }) => data.val,
  631. },
  632. data: this.manualMarks,
  633. },
  634. };
  635. this.chartInstance.setOption({
  636. series: [
  637. ...(option.series || []).filter((s) => s && s.id !== "MANUAL_MARK"),
  638. manualSeries,
  639. ],
  640. });
  641. },
  642. updateFeatureLinesByGroup() {
  643. if (!this.spectrumList) return;
  644. console.log(
  645. this.spectrumListTwo,
  646. "updateFeatureLinesByGroup",
  647. this.spectrumList,
  648. );
  649. console.log(this.currentRow, "currentRow");
  650. const featureLines = this.checkedFeatures
  651. .map((name) => {
  652. const multiple = this.getFeatureMultipleByName(name);
  653. if (multiple == null || Number.isNaN(multiple)) return null;
  654. return {
  655. // Xaxis: Number(multiple),
  656. Xaxis:
  657. (Number(this.currentRow.otherData.RPM) / 60) * Number(multiple),
  658. val: `${name}: ${multiple}`,
  659. };
  660. })
  661. .filter(Boolean);
  662. this.renderFeatureSeries(featureLines);
  663. },
  664. updateChart(data, labels) {
  665. if (
  666. !this.chartInstance ||
  667. !Array.isArray(labels) ||
  668. !Array.isArray(data) ||
  669. labels.length !== data.length
  670. ) {
  671. console.error("Invalid data or labels");
  672. return;
  673. }
  674. console.log(this.spectrumList, "updataChart 频谱图");
  675. const createMarkLine = (dataSource, color) => ({
  676. type: "line",
  677. markLine: {
  678. silent: false,
  679. lineStyle: { color, type: "dashed", width: 2 },
  680. symbol: ["none", "none"],
  681. label: {
  682. show: true,
  683. position: "end",
  684. formatter: ({ data }) => data.val,
  685. },
  686. emphasis: {
  687. lineStyle: { color: "#FF6A00", width: 4, type: "dashed" },
  688. label: {
  689. show: true,
  690. formatter: ({ value }) => `特征值: ${value}`,
  691. color: "#000",
  692. backgroundColor: "#FFF",
  693. padding: [2, 4],
  694. borderRadius: 3,
  695. fontSize: 12,
  696. },
  697. },
  698. data: dataSource.map(({ Xaxis, val }) => ({ xAxis: Xaxis, val })),
  699. },
  700. });
  701. const markLines = [
  702. { data: this.Fr, color: "#A633FF" },
  703. { data: this.BPFI, color: "#23357e" },
  704. { data: this.BPFO, color: "#42a0ae" },
  705. { data: this.BSF, color: "#008080" },
  706. { data: this.FTF, color: "#af254f" },
  707. { data: this.B3P, color: "#FFD700" },
  708. ].map(({ data, color }) => createMarkLine(data, color));
  709. const option = {
  710. grid: {
  711. left: 60, // 原来是100,适当缩小左右边距
  712. right: 20,
  713. // top: 90, // 给推拽条和坐标轴腾出空间
  714. top: 30,
  715. bottom: 50,
  716. },
  717. // title: {
  718. // text: this.spectrumList.title || "",
  719. // left: "center",
  720. // top: 0,
  721. // textStyle: {
  722. // fontSize: 15,
  723. // fontWeight: 600,
  724. // color: "#303133",
  725. // },
  726. // padding: [0, 12, 6, 12],
  727. // },
  728. toolbox: {
  729. right: 10,
  730. // top: 55, // 👈 工具栏在最上面
  731. feature: {
  732. dataZoom: { yAxisIndex: "none" },
  733. restore: {},
  734. saveAsImage: {},
  735. },
  736. },
  737. xAxis: {
  738. type: "value",
  739. name: this.spectrumList.xaxis,
  740. nameLocation: "center",
  741. nameTextStyle: {
  742. fontSize: 12,
  743. color: "#333",
  744. padding: [10, 0, 0, 0], // 增加X轴标题和轴线的距离
  745. },
  746. axisLabel: {
  747. margin: 1, // 增加数值标签和轴线的间距
  748. formatter: (value) => value,
  749. },
  750. },
  751. yAxis: {
  752. type: "value",
  753. name: this.spectrumList.yaxis,
  754. nameTextStyle: {
  755. fontSize: 12,
  756. color: "#333",
  757. padding: [10, 0, 0, 0],
  758. },
  759. },
  760. tooltip: {
  761. trigger: "axis",
  762. formatter: ([
  763. {
  764. value: [x, y],
  765. },
  766. ]) => `X: ${x}<br/>Y: ${y}`,
  767. axisPointer: { type: "line" },
  768. },
  769. // 无滑块;工具栏「区域缩放」需 dataZoom 组件:用不可见 inside,关闭滚轮
  770. dataZoom: [
  771. {
  772. type: "inside",
  773. xAxisIndex: 0,
  774. filterMode: "none",
  775. zoomOnMouseWheel: false,
  776. moveOnMouseMove: false,
  777. moveOnMouseWheel: false,
  778. },
  779. ],
  780. series: [
  781. {
  782. name: "数据系列",
  783. type: "line",
  784. data: labels.map((x, i) => [x, data[i]]),
  785. symbol: "none",
  786. lineStyle: { color: "#162961", width: 1 },
  787. itemStyle: {
  788. color: "#162961",
  789. borderColor: "#fff",
  790. borderWidth: 1,
  791. },
  792. large: true,
  793. progressive: 2000,
  794. },
  795. ...markLines,
  796. ],
  797. };
  798. this.chartInstance.setOption(option, true);
  799. this.$nextTick(() => {
  800. if (this.singlePointerPoint) {
  801. this.applySinglePointerSeries();
  802. }
  803. });
  804. },
  805. // ✅ 生成特征线 series(安全版)
  806. generateSeries(featureLines) {
  807. // 👉 安全创建 markLine
  808. const createMarkLine = (dataSource, color) => {
  809. const validData = (dataSource || [])
  810. .filter((item) => item && item.Xaxis != null && !isNaN(item.Xaxis))
  811. .map(({ Xaxis, val }) => ({
  812. xAxis: Number(Xaxis),
  813. val,
  814. }));
  815. // ❗ 没数据直接返回 null(后面会过滤)
  816. if (!validData.length) return null;
  817. return {
  818. type: "line",
  819. markLine: {
  820. silent: false,
  821. lineStyle: {
  822. color,
  823. type: "dashed",
  824. width: 2,
  825. },
  826. symbol: ["none", "none"],
  827. label: {
  828. show: true,
  829. position: "end",
  830. formatter: ({ data }) => data.val,
  831. },
  832. data: validData,
  833. },
  834. };
  835. };
  836. const colors = [
  837. "#A633FF",
  838. "#23357e",
  839. "#42a0ae",
  840. "#008080",
  841. "#af254f",
  842. "#FFD700",
  843. "#ff7f50",
  844. "#00bcd4",
  845. ];
  846. // 每个选中特征生成一条标注线
  847. const markLines = (featureLines || [])
  848. .map((line, idx) => createMarkLine([line], colors[idx % colors.length]))
  849. .filter(Boolean); // ✅ 关键:过滤 null
  850. return [
  851. {
  852. name: "数据系列",
  853. type: "line",
  854. data: [],
  855. },
  856. ...markLines,
  857. ];
  858. },
  859. // ✅ 渲染特征线(最终稳定版)
  860. renderFeatureSeries(featureLines) {
  861. if (!this.chartInstance) return;
  862. const currentOption = this.chartInstance.getOption();
  863. const seriesArr = currentOption.series || [];
  864. // 👉 基础数据线(getOption 可能在首位出现 null 占位,勿用 [0])
  865. const baseSeries =
  866. seriesArr.find(
  867. (s) => s && s.name === "数据系列" && s.type === "line",
  868. ) || seriesArr.find((s) => s);
  869. // 👉 光标相关 series(不要写 || {} ❗)
  870. const cursorLineSeries = seriesArr.find(
  871. (s) => s && s.id === "CURSOR_LINE_SERIES",
  872. );
  873. const cursorPointSeries = seriesArr.find(
  874. (s) => s && s.id === "CURSOR_POINT_SERIES",
  875. );
  876. const cursorHighLineSeries = seriesArr.find(
  877. (s) => s && s.id === "PEAK_REFERENCE_LINE",
  878. );
  879. const singlePointerSeries = seriesArr.find(
  880. (s) => s && s.id === "SINGLE_POINTER_LINE",
  881. );
  882. // 👉 生成特征线
  883. const featureSeries = this.generateSeries(featureLines);
  884. // 👉 最终更新(核心:必须过滤)
  885. this.chartInstance.setOption(
  886. {
  887. series: [
  888. baseSeries,
  889. ...featureSeries.slice(1), // 跳过第一个占位 line
  890. cursorLineSeries,
  891. cursorPointSeries,
  892. cursorHighLineSeries,
  893. singlePointerSeries,
  894. ].filter((s) => s && s.type), // ✅ 终极保险(必须有)
  895. },
  896. {
  897. replaceMerge: ["series"],
  898. },
  899. );
  900. },
  901. // 获取数据
  902. getTime() {
  903. this.$emit("handleLoading", {
  904. id: this.chartId,
  905. currentRow: this.currentRow,
  906. loading: true,
  907. });
  908. const params = {
  909. ids: this.ids,
  910. windCode: this.windCode,
  911. analysisType: "frequency",
  912. };
  913. axios
  914. .post("/AnalysisMulti/analysis/frequency", params)
  915. .then((res) => {
  916. this.spectrumList = { ...JSON.parse(res.data) };
  917. console.log(this.spectrumList, "频谱图数据1");
  918. const XrmsValue = this.spectrumList?.Xrms;
  919. this.$emit("updateXrms", XrmsValue);
  920. // 拉完数据后,如果已有选中项,自动渲染
  921. this.$nextTick(() => {
  922. this.updateFeatureLinesByGroup();
  923. });
  924. })
  925. .catch((error) => {
  926. console.error(error);
  927. })
  928. .finally(() => {
  929. this.$emit("handleLoading", {
  930. id: this.chartId,
  931. currentRow: this.currentRow,
  932. loading: false,
  933. });
  934. });
  935. },
  936. Show(value) {
  937. const stateMap = {
  938. 1: { TZshow: true, BGshow: false, PXshow: false },
  939. 2: { TZshow: false, BGshow: true, PXshow: false },
  940. 3: { TZshow: false, BGshow: false, PXshow: true },
  941. };
  942. if (stateMap[value]) {
  943. this.TZshow = value === "1" ? !this.TZshow : false;
  944. this.BGshow = value === "2" ? !this.BGshow : false;
  945. this.PXshow = value === "3" ? !this.PXshow : false;
  946. }
  947. },
  948. },
  949. created() {
  950. this.buildFeatureMultipleMap();
  951. },
  952. };
  953. </script>
  954. <style lang="scss" scoped>
  955. .line-chart {
  956. width: 100%;
  957. height: 400px;
  958. }
  959. .FD {
  960. width: 100%;
  961. height: 1px;
  962. position: relative;
  963. }
  964. .eigenvalue {
  965. position: absolute;
  966. top: 60px;
  967. right: 0;
  968. font-size: 10px;
  969. width: 146px;
  970. border: 1px solid rgb(182, 182, 182);
  971. padding: 5px;
  972. background: #fff;
  973. z-index: 99;
  974. border-radius: 5px;
  975. h5 {
  976. line-height: 16px;
  977. height: 16px;
  978. }
  979. }
  980. .eigenvalue--first {
  981. width: 100px;
  982. }
  983. .control-panel {
  984. /* 按控件区域实际宽度断点(嵌入侧栏时比视口媒体查询可靠) */
  985. container-name: control-panel;
  986. container-type: inline-size;
  987. display: flex;
  988. flex-wrap: wrap;
  989. justify-content: space-between;
  990. gap: 6px;
  991. padding: 8px 12px;
  992. background: #f5f7fa;
  993. border: 1px solid #ddd;
  994. border-radius: 6px;
  995. margin-bottom: 10px;
  996. }
  997. /* 🌟 独占一行;容器宽度 ≤600 时频率块全宽,>600 时约半宽 */
  998. .full-row {
  999. width: 100%;
  1000. display: flex;
  1001. justify-content: space-between;
  1002. .panel-block {
  1003. width: 92%;
  1004. }
  1005. }
  1006. @container control-panel (min-width: 701px) {
  1007. .full-row .panel-block {
  1008. width: 45%;
  1009. }
  1010. }
  1011. .panel-block {
  1012. display: flex;
  1013. align-items: center;
  1014. gap: 6px;
  1015. }
  1016. .label {
  1017. font-size: 12px;
  1018. color: #666;
  1019. }
  1020. .label1 {
  1021. font-size: 12px;
  1022. color: #666;
  1023. display: inline-block;
  1024. width: 75px;
  1025. }
  1026. .btn-group {
  1027. display: flex;
  1028. gap: 6px;
  1029. }
  1030. .btn {
  1031. padding: 2px 8px;
  1032. font-size: 12px;
  1033. border: 1px solid #ccc;
  1034. border-radius: 3px;
  1035. cursor: pointer;
  1036. }
  1037. .btn.active {
  1038. background: #409eff;
  1039. color: #fff;
  1040. }
  1041. .full-width {
  1042. width: 100%;
  1043. }
  1044. .el-cascader {
  1045. font-size: 12px;
  1046. }
  1047. .el-cascader__tags {
  1048. max-width: 240px;
  1049. overflow: hidden;
  1050. }
  1051. .feature-grid {
  1052. display: grid;
  1053. grid-template-columns: repeat(4, 1fr); // 两列布局
  1054. gap: 10px;
  1055. width: 100%;
  1056. }
  1057. .feature-item {
  1058. display: flex;
  1059. align-items: center;
  1060. gap: 6px;
  1061. }
  1062. .el-select {
  1063. flex: 1;
  1064. }
  1065. .pannel {
  1066. margin: 5px;
  1067. padding: 8px 10px;
  1068. font-size: 12px;
  1069. line-height: 1.5;
  1070. // color: #303133;
  1071. background: #f5f7fa;
  1072. border: 1px solid #ddd;
  1073. border-radius: 4px;
  1074. display: flex;
  1075. }
  1076. .pannel-row {
  1077. display: flex;
  1078. align-items: flex-start;
  1079. margin: 0px 10px;
  1080. // margin-right: 5px;
  1081. // gap: 5px;
  1082. // margin-bottom: 4px;
  1083. &:last-child {
  1084. margin-bottom: 0;
  1085. }
  1086. }
  1087. .pannel-key {
  1088. // flex: 0 0 37px;
  1089. // flex: 0 0 45px;
  1090. color: #666;
  1091. }
  1092. .pannel-val {
  1093. flex: 1;
  1094. word-break: break-all;
  1095. }
  1096. </style>