dashboardAdmin.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780
  1. <template>
  2. <div class="dashbord">
  3. <div class="chart-group">
  4. <el-card>
  5. <SubTitle title="设备运行" />
  6. <div class="chart-content">
  7. <img class="positionImg" src="@/assets/images/position.png" alt="" />
  8. </div>
  9. </el-card>
  10. </div>
  11. <div class="chart-group">
  12. <el-card>
  13. <SubTitle title="项目情况" />
  14. <div class="chart-content">
  15. <BaseChart width="100%" height="100%" :option="projectOption" />
  16. </div>
  17. </el-card>
  18. <el-card>
  19. <SubTitle title="设备总览" />
  20. <div class="chart-content">
  21. <BaseChart width="100%" height="100%" :option="equipOption" />
  22. </div>
  23. </el-card>
  24. <el-card>
  25. <SubTitle title="巡检人员" />
  26. <div class="chart-content">
  27. <div class="check-summary">
  28. <div>
  29. <img src="@/assets/images/home/banzu.svg" alt="" />
  30. <div class="check-name">
  31. <div>8</div>
  32. <div>班组</div>
  33. </div>
  34. </div>
  35. <div>
  36. <img src="@/assets/images/home/person.svg" alt="" />
  37. <div class="check-name">
  38. <div>40</div>
  39. <div>巡检人员</div>
  40. </div>
  41. </div>
  42. </div>
  43. <div class="check-rank">
  44. <div>当月巡检排名</div>
  45. <CustomTabs v-model:active="checkActive" :tabs="checkTabs" />
  46. </div>
  47. <el-table :data="tableData" style="width: 100%; margin-top: 10px" max-height="180">
  48. <el-table-column prop="name" label="巡检员">
  49. <template #default="scope">
  50. {{ `${scope.$index + 1}、${scope.row.name}` }}
  51. </template>
  52. </el-table-column>
  53. <el-table-column prop="group" label="所在班组" />
  54. <el-table-column prop="num" label="数量" />
  55. </el-table>
  56. </div>
  57. </el-card>
  58. <el-card>
  59. <SubTitle title="设备故障" />
  60. <div class="chart-content">
  61. <div style="display: flex; justify-content: flex-end; margin-top: 10px">
  62. <CustomTabs v-model:active="faultActive" :tabs="faultTabs" />
  63. </div>
  64. <BaseChart width="100%" height="100%" :option="rankOption" />
  65. </div>
  66. </el-card>
  67. </div>
  68. </div>
  69. </template>
  70. <script setup lang="ts">
  71. import CustomTabs from './components/CustomTabs.vue';
  72. const checkTabs = [
  73. {
  74. name: '按巡检设备数',
  75. value: '1'
  76. },
  77. {
  78. name: '按发现故障数',
  79. value: '2'
  80. }
  81. ];
  82. const faultTabs = [
  83. {
  84. name: '按设备类型',
  85. value: '1'
  86. },
  87. {
  88. name: '按故障类型',
  89. value: '2'
  90. }
  91. ];
  92. const checkActive = ref('1');
  93. const faultActive = ref('1');
  94. const tableData = [
  95. {
  96. name: '王刚',
  97. group: '班组4',
  98. num: '123'
  99. },
  100. {
  101. name: '李思',
  102. group: '班组2',
  103. num: '88'
  104. },
  105. {
  106. name: '张伟',
  107. group: '班组3',
  108. num: '65'
  109. },
  110. {
  111. name: '张强',
  112. group: '班组1',
  113. num: '54'
  114. }
  115. ];
  116. function getParametricEquation(startRatio, endRatio, isSelected, isHovered, k, height) {
  117. // 计算
  118. let midRatio = (startRatio + endRatio) / 2;
  119. let startRadian = startRatio * Math.PI * 2;
  120. let endRadian = endRatio * Math.PI * 2;
  121. let midRadian = midRatio * Math.PI * 2;
  122. // 如果只有一个扇形,则不实现选中效果。
  123. if (startRatio === 0 && endRatio === 1) {
  124. isSelected = false;
  125. }
  126. // 通过扇形内径/外径的值,换算出辅助参数 k(默认值 1/3)
  127. k = typeof k !== 'undefined' ? k : 1 / 3;
  128. // 计算选中效果分别在 x 轴、y 轴方向上的位移(未选中,则位移均为 0)
  129. let offsetX = isSelected ? Math.cos(midRadian) * 0.1 : 0;
  130. let offsetY = isSelected ? Math.sin(midRadian) * 0.1 : 0;
  131. // 计算高亮效果的放大比例(未高亮,则比例为 1)
  132. let hoverRate = isHovered ? 1.05 : 1;
  133. // 返回曲面参数方程
  134. return {
  135. u: {
  136. min: -Math.PI,
  137. max: Math.PI * 3,
  138. step: Math.PI / 32
  139. },
  140. v: {
  141. min: 0,
  142. max: Math.PI * 2,
  143. step: Math.PI / 20
  144. },
  145. x: function (u, v) {
  146. if (u < startRadian) {
  147. return offsetX + Math.cos(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
  148. }
  149. if (u > endRadian) {
  150. return offsetX + Math.cos(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
  151. }
  152. return offsetX + Math.cos(u) * (1 + Math.cos(v) * k) * hoverRate;
  153. },
  154. y: function (u, v) {
  155. if (u < startRadian) {
  156. return offsetY + Math.sin(startRadian) * (1 + Math.cos(v) * k) * hoverRate;
  157. }
  158. if (u > endRadian) {
  159. return offsetY + Math.sin(endRadian) * (1 + Math.cos(v) * k) * hoverRate;
  160. }
  161. return offsetY + Math.sin(u) * (1 + Math.cos(v) * k) * hoverRate;
  162. },
  163. z: function (u, v) {
  164. if (u < -Math.PI * 0.5) {
  165. return Math.sin(u);
  166. }
  167. if (u > Math.PI * 2.5) {
  168. return Math.sin(u);
  169. }
  170. return Math.sin(v) > 0 ? 0.2 * height : -1;
  171. }
  172. };
  173. }
  174. // 生成模拟 3D 饼图的配置项
  175. function getPie3D(pieData, internalDiameterRatio) {
  176. let series = [];
  177. let sumValue = 0;
  178. let startValue = 0;
  179. let endValue = 0;
  180. let legendData = [];
  181. let k = typeof internalDiameterRatio !== 'undefined' ? (1 - internalDiameterRatio) / (1 + internalDiameterRatio) : 1 / 3;
  182. // 为每一个饼图数据,生成一个 series-surface 配置
  183. for (let i = 0; i < pieData.length; i++) {
  184. sumValue += pieData[i].value;
  185. let seriesItem = {
  186. name: typeof pieData[i].name === 'undefined' ? `series${i}` : pieData[i].name,
  187. type: 'surface',
  188. parametric: true,
  189. wireframe: {
  190. show: false
  191. },
  192. pieData: pieData[i],
  193. pieStatus: {
  194. selected: false,
  195. hovered: false,
  196. k: k
  197. }
  198. };
  199. if (typeof pieData[i].itemStyle != 'undefined') {
  200. let itemStyle = {};
  201. typeof pieData[i].itemStyle.color != 'undefined' ? (itemStyle.color = pieData[i].itemStyle.color) : null;
  202. typeof pieData[i].itemStyle.opacity != 'undefined' ? (itemStyle.opacity = pieData[i].itemStyle.opacity) : null;
  203. seriesItem.itemStyle = itemStyle;
  204. }
  205. series.push(seriesItem);
  206. }
  207. // 使用上一次遍历时,计算出的数据和 sumValue,调用 getParametricEquation 函数,
  208. // 向每个 series-surface 传入不同的参数方程 series-surface.parametricEquation,也就是实现每一个扇形。
  209. for (let i = 0; i < series.length; i++) {
  210. endValue = startValue + series[i].pieData.value;
  211. console.log(series[i]);
  212. series[i].pieData.startRatio = startValue / sumValue;
  213. series[i].pieData.endRatio = endValue / sumValue;
  214. series[i].parametricEquation = getParametricEquation(
  215. series[i].pieData.startRatio,
  216. series[i].pieData.endRatio,
  217. false,
  218. false,
  219. k,
  220. series[i].pieData.value
  221. );
  222. startValue = endValue;
  223. legendData.push(series[i].name);
  224. }
  225. // // 补充一个透明的圆环,用于支撑高亮功能的近似实现。
  226. series.push({
  227. name: 'mouseoutSeries',
  228. type: 'surface',
  229. parametric: true,
  230. wireframe: {
  231. show: false
  232. },
  233. itemStyle: {
  234. opacity: 0.1,
  235. color: '#8997DE'
  236. },
  237. parametricEquation: {
  238. u: {
  239. min: 0,
  240. max: Math.PI * 2,
  241. step: Math.PI / 20
  242. },
  243. v: {
  244. min: 0,
  245. max: Math.PI,
  246. step: Math.PI / 20
  247. },
  248. x: function (u, v) {
  249. return ((Math.sin(v) * Math.sin(u) + Math.sin(u)) / Math.PI) * 2;
  250. },
  251. y: function (u, v) {
  252. return ((Math.sin(v) * Math.cos(u) + Math.cos(u)) / Math.PI) * 2;
  253. },
  254. z: function (u, v) {
  255. return Math.cos(v) > 0 ? -0.5 : -5;
  256. }
  257. }
  258. });
  259. // // 补充一个透明的圆环,用于支撑高亮功能的近似实现。
  260. series.push({
  261. name: 'mouseoutSeries',
  262. type: 'surface',
  263. parametric: true,
  264. wireframe: {
  265. show: false
  266. },
  267. itemStyle: {
  268. opacity: 0.1,
  269. color: '#8997DE'
  270. },
  271. parametricEquation: {
  272. u: {
  273. min: 0,
  274. max: Math.PI * 2,
  275. step: Math.PI / 20
  276. },
  277. v: {
  278. min: 0,
  279. max: Math.PI,
  280. step: Math.PI / 20
  281. },
  282. x: function (u, v) {
  283. return ((Math.sin(v) * Math.sin(u) + Math.sin(u)) / Math.PI) * 2;
  284. },
  285. y: function (u, v) {
  286. return ((Math.sin(v) * Math.cos(u) + Math.cos(u)) / Math.PI) * 2;
  287. },
  288. z: function (u, v) {
  289. return Math.cos(v) > 0 ? -5 : -7;
  290. }
  291. }
  292. });
  293. series.push({
  294. name: 'mouseoutSeries',
  295. type: 'surface',
  296. parametric: true,
  297. wireframe: {
  298. show: false
  299. },
  300. itemStyle: {
  301. opacity: 0.1,
  302. color: '#8997DE'
  303. },
  304. parametricEquation: {
  305. u: {
  306. min: 0,
  307. max: Math.PI * 2,
  308. step: Math.PI / 20
  309. },
  310. v: {
  311. min: 0,
  312. max: Math.PI,
  313. step: Math.PI / 20
  314. },
  315. x: function (u, v) {
  316. return ((Math.sin(v) * Math.sin(u) + Math.sin(u)) / Math.PI) * 2.2;
  317. },
  318. y: function (u, v) {
  319. return ((Math.sin(v) * Math.cos(u) + Math.cos(u)) / Math.PI) * 2.2;
  320. },
  321. z: function (u, v) {
  322. return Math.cos(v) > 0 ? -7 : -7;
  323. }
  324. }
  325. });
  326. return series;
  327. }
  328. let colors = ['#085AC7', '#24525E', '#C3972E'];
  329. let xData = ['运营', '施工', '结束'];
  330. let yData = [568, 175, 396];
  331. // 传入数据生成 option
  332. let optionsData = [];
  333. let total = 0;
  334. yData.forEach((v) => {
  335. total += v;
  336. });
  337. for (let i = 0; i < xData.length; i++) {
  338. optionsData.push({
  339. name: xData[i],
  340. value: yData[i],
  341. itemStyle: {
  342. color: colors[i],
  343. opacity: 0.7
  344. }
  345. });
  346. }
  347. const series = getPie3D(optionsData, 0.8);
  348. const projectOption = computed(() => {
  349. return {
  350. legend: {
  351. tooltip: {
  352. show: true
  353. },
  354. data: xData,
  355. orient: 'vertial',
  356. bottom: '5%',
  357. left: 'center',
  358. itemGap: 14,
  359. itemHeight: 10,
  360. itemWidth: 15,
  361. formatter: (name) => {
  362. const res = optionsData.filter((n) => {
  363. return n.name === name;
  364. });
  365. if (!res.length) return;
  366. return `${name} ${res[0].value}个 ${res[0].value ? ((res[0].value / total) * 100).toFixed(2) : 0}%`;
  367. }
  368. },
  369. animation: true,
  370. tooltip: {
  371. formatter: (params) => {
  372. if (params.seriesName !== 'mouseoutSeries' && params.seriesName !== 'pie2d') {
  373. return `${params.seriesName}<br/><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${params.color};"></span>${projectOption.value.series[params.seriesIndex].pieData.value}个`;
  374. }
  375. },
  376. textStyle: {
  377. fontSize: 12
  378. }
  379. },
  380. xAxis3D: {
  381. min: -1,
  382. max: 1
  383. },
  384. yAxis3D: {
  385. min: -1,
  386. max: 1
  387. },
  388. zAxis3D: {
  389. min: -1,
  390. max: 1
  391. },
  392. grid3D: {
  393. show: false,
  394. boxHeight: 0.5,
  395. top: '-10%',
  396. viewControl: {
  397. distance: 240,
  398. alpha: 30,
  399. beta: 10,
  400. autoRotate: true // 自动旋转
  401. }
  402. },
  403. series: series
  404. };
  405. });
  406. const equipData = ref<any>([
  407. {
  408. name: '报废',
  409. value: 1546
  410. },
  411. {
  412. name: '维修中',
  413. value: 189
  414. },
  415. {
  416. name: '故障',
  417. value: 1452
  418. },
  419. {
  420. name: '其他',
  421. value: 189
  422. },
  423. {
  424. name: '正在运行',
  425. value: 600
  426. },
  427. {
  428. name: '停役',
  429. value: 200
  430. }
  431. ]);
  432. const color = ['#1990FF', '#8543E0', '#30C25B', '#16C2C2', '#FACC14', '#F04864'];
  433. equipData.value.forEach((item, index) => {
  434. const tag = index % 6;
  435. item.itemStyle = {
  436. color: color[tag] || ''
  437. };
  438. });
  439. let equipTotal = 0;
  440. equipData.value.forEach((v) => {
  441. equipTotal += v.value;
  442. });
  443. const equipOption = computed(() => {
  444. return {
  445. legend: {
  446. show: true,
  447. left: '0%',
  448. bottom: '5%',
  449. itemGap: 10,
  450. borderRadius: 5,
  451. itemWidth: 10,
  452. icon: 'circle',
  453. itemHeight: 10,
  454. data: equipData.value,
  455. formatter: function (name) {
  456. const res = equipData.value.filter((n) => {
  457. return n.name === name;
  458. });
  459. if (!res.length) return;
  460. return `${name} ${res[0].value ? ((res[0].value / equipTotal) * 100).toFixed(2) : 0}% ${res[0].value}`;
  461. }
  462. },
  463. tooltip: {
  464. trigger: 'item',
  465. backgroundColor: 'rgba(13,5,30,.6)',
  466. borderWidth: 1,
  467. borderColor: '#32A1FF',
  468. padding: 5,
  469. textStyle: {
  470. color: '#fff'
  471. },
  472. formatter: (params) => {
  473. return `${params.name}<br/>${params.marker}${params.value}`;
  474. }
  475. },
  476. title: {
  477. show: true,
  478. text: '设备总数',
  479. itemGap: 5, //主副标题之间的距离
  480. left: 'center',
  481. top: '28%',
  482. textStyle: {
  483. fontSize: 13,
  484. color: '#9E9E9E',
  485. fontWeight: 'normal'
  486. },
  487. subtext: '4226',
  488. subtextStyle: {
  489. fontSize: 18,
  490. fontWeight: 500,
  491. color: '#333'
  492. }
  493. },
  494. series: [
  495. {
  496. name: '',
  497. type: 'pie',
  498. radius: ['40%', '60%'],
  499. center: ['50%', '35%'],
  500. itemStyle: {
  501. borderWidth: 2, //描边线宽
  502. borderColor: '#fff'
  503. },
  504. label: {
  505. show: false
  506. },
  507. labelLine: {},
  508. data: equipData.value
  509. }
  510. ]
  511. };
  512. });
  513. const rankData = [
  514. { name: '设备类型一', value: 323 },
  515. { name: '设备类型二', value: 108 },
  516. { name: '设备类型三', value: 95 },
  517. { name: '设备类型四', value: 43 },
  518. { name: '设备类型五', value: 10 }
  519. ]; // 类别
  520. let rankTotal = 0; // 数据总数
  521. rankData.forEach((v) => {
  522. rankTotal += v.value;
  523. });
  524. const rankOption = computed(() => {
  525. return {
  526. grid: {
  527. left: '5%',
  528. top: '3%', // 设置条形图的边距
  529. right: '12%',
  530. bottom: '15%'
  531. },
  532. xAxis: {
  533. splitLine: {
  534. show: false,
  535. lineStyle: {
  536. color: 'rgba(255,255,255,0.2)',
  537. type: 'dashed'
  538. }
  539. },
  540. axisLine: {
  541. show: false
  542. },
  543. axisLabel: {
  544. show: false,
  545. color: '#ABBFE3'
  546. },
  547. axisTick: {
  548. show: false
  549. }
  550. },
  551. yAxis: [
  552. {
  553. type: 'category',
  554. inverse: true,
  555. data: rankData.map((item) => item.name),
  556. axisLine: {
  557. show: false
  558. },
  559. axisTick: {
  560. show: false
  561. },
  562. axisLabel: {
  563. show: true,
  564. textStyle: {
  565. verticalAlign: 'bottom',
  566. color: '#000',
  567. fontSize: 12,
  568. fontFamily: 'Microsoft YaHei',
  569. align: 'left',
  570. padding: [0, 0, 9, 5]
  571. },
  572. formatter: (name, index) => {
  573. const _index = index + 1;
  574. return `NO${_index}. ${name}`;
  575. }
  576. },
  577. offset: 0
  578. }
  579. ],
  580. series: [
  581. {
  582. // 内
  583. type: 'bar',
  584. barWidth: 10,
  585. barCateGoryGap: 20,
  586. legendHoverLink: false,
  587. silent: true,
  588. itemStyle: {
  589. normal: {
  590. barBorderRadius: 10,
  591. color: {
  592. type: 'linear',
  593. x: 0,
  594. y: 0,
  595. x2: 1,
  596. y2: 0,
  597. colorStops: [
  598. {
  599. offset: 0,
  600. color: '#FFFFFF' // 0% 处的颜色
  601. },
  602. {
  603. offset: 1,
  604. color: '#0768FF' // 100% 处的颜色
  605. }
  606. ]
  607. }
  608. }
  609. },
  610. label: {
  611. normal: {
  612. show: false,
  613. position: '[0, 0, 15, 10]',
  614. formatter: '{b}',
  615. textStyle: {
  616. color: '#fff',
  617. fontSize: 14
  618. }
  619. }
  620. },
  621. data: rankData,
  622. z: 2,
  623. animationEasing: 'elasticOut'
  624. },
  625. {
  626. // 外边框
  627. type: 'pictorialBar',
  628. symbol: 'rect',
  629. symbolBoundingData: rankTotal,
  630. itemStyle: {
  631. barBorderRadius: 10,
  632. normal: {
  633. color: 'none'
  634. }
  635. },
  636. label: {
  637. normal: {
  638. padding: [0, 10, 0, 14],
  639. formatter: (params) => {
  640. return params.data;
  641. },
  642. color: '#03FF00',
  643. fontWeight: 'bold',
  644. position: 'right',
  645. distance: 1, // 向右偏移位置
  646. show: true
  647. }
  648. },
  649. data: rankData.map((item) => item.value),
  650. z: 0,
  651. animationEasing: 'elasticOut'
  652. },
  653. {
  654. name: '外框',
  655. type: 'bar',
  656. barCateGoryGap: 20,
  657. barGap: '-100%', // 设置外框粗细
  658. data: new Array(rankData.length).fill(rankTotal),
  659. barWidth: 10,
  660. itemStyle: {
  661. normal: {
  662. barBorderRadius: [0, 6, 6, 0],
  663. color: '#F2F2F2'
  664. },
  665. emphasis: {
  666. barBorderRadius: [0, 6, 6, 0],
  667. color: '#F2F2F2'
  668. }
  669. },
  670. z: 0
  671. }
  672. ]
  673. };
  674. });
  675. </script>
  676. <style lang="scss" scoped>
  677. .positionImg {
  678. width: 100%;
  679. height: 100%;
  680. padding: 10px 0;
  681. }
  682. .chart-group {
  683. display: flex;
  684. margin-top: 5px;
  685. :deep(.el-card__body) {
  686. height: 100%;
  687. padding: 15px 10px !important;
  688. }
  689. .el-card {
  690. flex: 1;
  691. height: 350px;
  692. background: #fff;
  693. &:not(:first-child) {
  694. margin-left: 5px;
  695. }
  696. }
  697. .chart-content {
  698. height: calc(100% - 10px);
  699. margin-top: 5px;
  700. border-top: 1px solid #eaebee;
  701. }
  702. }
  703. .check-summary {
  704. display: flex;
  705. margin-top: 10px;
  706. >div {
  707. flex: 1;
  708. padding: 5px 10px;
  709. display: flex;
  710. background: #fafbfc;
  711. border: 1px solid #e4e5e9;
  712. border-radius: 4px;
  713. &:not(:first-child) {
  714. margin-left: 10px;
  715. }
  716. img {
  717. height: 40px;
  718. }
  719. .check-name {
  720. margin-left: 10px;
  721. div:first-child {
  722. font-size: 16px;
  723. font-weight: 500;
  724. }
  725. div:last-child {
  726. font-size: 14px;
  727. }
  728. }
  729. }
  730. }
  731. .check-rank {
  732. margin-top: 10px;
  733. display: flex;
  734. justify-content: space-between;
  735. >div:first-child {
  736. font-size: 14px;
  737. }
  738. }
  739. </style>