charging.vue 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. <template>
  2. <div class="app-container">
  3. <el-row :gutter="20">
  4. <el-col :span="4" :xs="24">
  5. <div class="head-container">
  6. <el-input v-model="areaName" placeholder="请输入服务区名称" clearable size="small" prefix-icon="el-icon-search"
  7. style="margin-bottom: 20px" />
  8. </div>
  9. <div class="head-container">
  10. <el-tree ref="tree" :data="areaOptions" :props="defaultProps" :expand-on-click-node="false" :filter-node-method="filterNode"
  11. node-key="id" :default-expanded-keys="this.defalutArr" highlight-current @node-click="handleNodeClick" />
  12. </div>
  13. </el-col>
  14. <el-col :span="20" :xs="24">
  15. <el-tabs v-model="activeTab" @tab-click="handleTabChange">
  16. <el-tab-pane label="总览" name="first">
  17. <div class="custom-form">
  18. <el-select v-model="queryParams.meterCls" size="mini" @change="meterClsChange">
  19. <el-option v-for="item in emsClsOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
  20. </el-select>
  21. </div>
  22. <div class="panel">
  23. <div class="panel-title">当月数据</div>
  24. <div class="panel-body">
  25. <BaseChart width="100%" height="300px" :option="pieOptions" />
  26. </div>
  27. </div>
  28. <div class="panel">
  29. <div class="panel-title">历史数据</div>
  30. <div class="panel-body">
  31. <div class="custom-form">
  32. <el-date-picker v-model="queryParams.historyRange" size="mini" type="monthrange" align="right" :clearable="false"
  33. range-separator="至" start-placeholder="开始月份" end-placeholder="结束月份" value-format="yyyyMM"
  34. :picker-options="pickerOptions" @change="getList">
  35. </el-date-picker>
  36. </div>
  37. <el-table :data="historyData" style="width: 100%;margin-bottom:20px" max-height="300px">
  38. <el-table-column prop="meteredTime" align="center" label="账单月份(年-月)">
  39. </el-table-column>
  40. <el-table-column
  41. prop="meteredValue"
  42. align="center"
  43. :label="`商户用量(${queryParams.meterCls === 45 ? 'kW·h' : '吨'})`">
  44. <template slot-scope="scope">
  45. {{ scope.row.meteredValue }}
  46. </template>
  47. </el-table-column>
  48. <el-table-column prop="meteredPrice" align="center" label="商户金额(¥)">
  49. </el-table-column>
  50. <el-table-column
  51. prop="sharedValue"
  52. align="center"
  53. :label="`公摊用量(${queryParams.meterCls === 45 ? 'kW·h' : '吨'})`">
  54. <template slot-scope="scope">
  55. {{ scope.row.meteredValue }}
  56. </template>
  57. </el-table-column>
  58. <el-table-column prop="sharedPrice" align="center" label="公摊费用(¥)">
  59. </el-table-column>
  60. <el-table-column
  61. prop="totalValue"
  62. align="center"
  63. :label="`总用量(${queryParams.meterCls === 45 ? 'kW·h' : '吨'})`">
  64. <template slot-scope="scope">
  65. {{ scope.row.meteredValue }}
  66. </template>
  67. </el-table-column>
  68. <el-table-column prop="totalPrice" align="center" label="总金额(¥)">
  69. </el-table-column>
  70. </el-table>
  71. <BaseChart width="100%" height="300px" :option="overviewOptions" />
  72. </div>
  73. </div>
  74. </el-tab-pane>
  75. <el-tab-pane label="个户" name="second">
  76. <div class="custom-form">
  77. <el-select v-model="queryParams.meterCls" size="mini" @change="meterClsChange">
  78. <el-option v-for="item in emsClsOptions" :key="item.code" :label="item.name" :value="item.code"></el-option>
  79. </el-select>
  80. </div>
  81. <div class="panel">
  82. <div class="panel-title">当月数据</div>
  83. <div class="panel-body">
  84. <el-table :data="currentData" style="width: 100%">
  85. <el-table-column prop="meteredTime" align="center" label="账单月份(年-月)">
  86. </el-table-column>
  87. <el-table-column
  88. prop="meteredValue"
  89. align="center"
  90. :label="`用量(${queryParams.meterCls === 45 ? 'kW·h' : '吨'})`">
  91. <template slot-scope="scope">
  92. {{ scope.row.meteredValue }}
  93. </template>
  94. </el-table-column>
  95. <el-table-column prop="meteredPrice" align="center" label="金额(¥)">
  96. </el-table-column>
  97. <el-table-column prop="sharedPrice" align="center" label="公摊费用(¥)">
  98. </el-table-column>
  99. <el-table-column prop="sharedComputeType" align="center" label="公摊类型">
  100. <template slot-scope="scope">
  101. {{formatDict(scope.row.sharedComputeType,'computeTypeOptions')}}
  102. </template>
  103. </el-table-column>
  104. <el-table-column prop="totalPrice" align="center" label="总金额(¥)">
  105. </el-table-column>
  106. </el-table>
  107. </div>
  108. </div>
  109. <div class="panel">
  110. <div class="panel-title">历史数据</div>
  111. <div class="panel-body">
  112. <div class="custom-form">
  113. <el-date-picker v-model="queryParams.historyRange" size="mini" type="monthrange" align="right" :clearable="false"
  114. range-separator="至" start-placeholder="开始月份" end-placeholder="结束月份" value-format="yyyyMM"
  115. :picker-options="pickerOptions" @change="getList">
  116. </el-date-picker>
  117. </div>
  118. <el-table :data="historyData" style="width: 100%;margin-bottom:20px" max-height="300px">
  119. <el-table-column prop="meteredTime" align="center" label="账单月份(年-月)">
  120. </el-table-column>
  121. <el-table-column
  122. prop="meteredValue"
  123. align="center"
  124. :label="`用量(${queryParams.meterCls === 45 ? 'kW·h' : '吨'})`">
  125. <template slot-scope="scope">
  126. {{ scope.row.meteredValue }}
  127. </template>
  128. </el-table-column>
  129. <el-table-column prop="meteredPrice" align="center" label="金额(¥)">
  130. </el-table-column>
  131. <!-- <el-table-column prop="sharedPrice" align="center" label="公摊价格">
  132. </el-table-column>
  133. <el-table-column prop="sharedComputeType" align="center" label="公摊类型">
  134. <template slot-scope="scope">
  135. {{formatDict(scope.row.sharedComputeType,'computeTypeOptions')}}
  136. </template>
  137. </el-table-column> -->
  138. <el-table-column prop="totalPrice" align="center" label="总价(¥)">
  139. </el-table-column>
  140. </el-table>
  141. <BaseChart width="100%" height="300px" :option="elecOptions" />
  142. </div>
  143. </div>
  144. </el-tab-pane>
  145. </el-tabs>
  146. </el-col>
  147. </el-row>
  148. </div>
  149. </template>
  150. <script>
  151. import { areaTreeSelectByTag } from '@/api/basecfg/area'
  152. import Treeselect from '@riophae/vue-treeselect'
  153. import '@riophae/vue-treeselect/dist/vue-treeselect.css'
  154. import { chargingList, overviewInfo } from '@/api/mgr/charging.js'
  155. import { dateFormat } from '@/utils/index.js'
  156. import BaseChart from '@/components/BaseChart'
  157. export default {
  158. name: 'Device',
  159. components: { Treeselect, BaseChart },
  160. data() {
  161. const nowDay = new Date()
  162. const lastMonth = new Date(nowDay.getFullYear(), nowDay.getMonth() - 1)
  163. return {
  164. activeTab: 'first',
  165. // 总条数
  166. areaName: undefined,
  167. areaOptions: [],
  168. defaultProps: {
  169. children: 'children',
  170. label: 'label'
  171. },
  172. emsClsOptions: [
  173. { code: 45, name: '电表' },
  174. { code: 70, name: '水表' }
  175. ],
  176. // 查询参数
  177. queryParams: {
  178. pageNum: 1,
  179. pageSize: 10,
  180. areaCode: null,
  181. meterCls: 45,
  182. historyRange: []
  183. },
  184. lastMonth: dateFormat(lastMonth, 'yyyyMM'),
  185. currentData: [],
  186. historyData: [],
  187. defalutArr: [],
  188. computeTypeOptions: [
  189. { name: '不计入', code: 0 },
  190. { name: '个户平摊', code: 1 },
  191. { name: '面积公摊)', code: 2 }
  192. ],
  193. pickerOptions: {
  194. disabledDate(time) {
  195. const t = new Date().getDate()
  196. return time.getTime() > Date.now() - 8.64e7 * t
  197. }
  198. }
  199. }
  200. },
  201. created() {
  202. this.setDefaultMonthRange()
  203. this.handleTabChange()
  204. },
  205. computed: {
  206. pieOptions() {
  207. let options = {}
  208. if (this.activeTab == 'first') {
  209. const { meterCls } = this.queryParams
  210. let pieData = []
  211. let total = ''
  212. if (this.currentData.length) {
  213. const { meteredValue, sharedValue, totalValue, meteredPrice, sharedPrice, totalPrice } = this.currentData[0]
  214. total = totalValue
  215. pieData = [
  216. {
  217. value: meteredValue,
  218. price: meteredPrice,
  219. name: '商户用量',
  220. totalValue,
  221. totalPrice,
  222. itemStyle: {
  223. color: '#8d7fec'
  224. }
  225. },
  226. {
  227. value: sharedValue,
  228. price: sharedPrice,
  229. name: '公摊用量',
  230. totalValue,
  231. totalPrice,
  232. itemStyle: {
  233. color: '#6BD9BC'
  234. }
  235. }
  236. ]
  237. }
  238. options = {
  239. title: [
  240. {
  241. text: '总用量',
  242. subtext: total + `${meterCls === 45 ? '度' : '吨'}`,
  243. textStyle: {
  244. fontSize: 15,
  245. color: 'black'
  246. },
  247. subtextStyle: {
  248. fontSize: 20,
  249. color: 'black'
  250. },
  251. textAlign: 'center',
  252. x: '34.5%',
  253. y: '40%'
  254. }
  255. ],
  256. tooltip: {
  257. trigger: 'item',
  258. formatter: function(parms) {
  259. var str =
  260. parms.data.name +
  261. '</br>' +
  262. '用量:' +
  263. parms.data.value +
  264. `${meterCls === 45 ? '度' : '吨'}` +
  265. '</br>' +
  266. '金额:' +
  267. parms.data.price +
  268. '元'
  269. return str
  270. }
  271. },
  272. legend: {
  273. type: 'scroll',
  274. orient: 'vertical',
  275. left: '65%',
  276. align: 'left',
  277. top: 'middle',
  278. textStyle: {
  279. color: '#8C8C8C'
  280. }
  281. },
  282. series: [
  283. {
  284. type: 'pie',
  285. center: ['35%', '50%'],
  286. radius: ['40%', '65%'],
  287. clockwise: false, // 饼图的扇区是否是顺时针排布
  288. avoidLabelOverlap: false,
  289. itemStyle: {
  290. // 图形样式
  291. normal: {
  292. borderColor: '#ffffff',
  293. borderWidth: 1
  294. }
  295. },
  296. label: {
  297. formatter: function(val) {
  298. const unit = meterCls === 45 ? '度' : '吨'
  299. return '{a|' + val.name + '}{b|\n' + val.value + unit + '}{b|\n' + val.data.price + '元}'
  300. },
  301. textStyle: {
  302. rich: {
  303. a: {
  304. color: '#333333',
  305. fontSize: '12'
  306. },
  307. b: {
  308. color: '#0086FF',
  309. fontSize: '12',
  310. padding: [4, 0, 0, 0]
  311. }
  312. }
  313. }
  314. },
  315. labelLine: {
  316. length: 10,
  317. length2: 50
  318. },
  319. labelLayout: {
  320. verticalAlign: 'bottom',
  321. },
  322. data: pieData
  323. }
  324. ]
  325. }
  326. }
  327. return options
  328. },
  329. overviewOptions() {
  330. let option = {}
  331. if (this.activeTab === 'first') {
  332. const xData = this.historyData.map(item => item.meteredTime)
  333. const quantity = this.historyData.map(item => item.meteredValue)
  334. const shareQuantity = this.historyData.map(item => item.sharedValue)
  335. const cost = this.historyData.map(item => item.meteredPrice)
  336. const shareCost = this.historyData.map(item => item.sharedPrice)
  337. const { meterCls } = this.queryParams
  338. option = {
  339. tooltip: {
  340. trigger: 'axis',
  341. axisPointer: {
  342. type: 'cross',
  343. crossStyle: {
  344. color: '#999'
  345. }
  346. }
  347. },
  348. grid: {
  349. left: '5%'
  350. },
  351. legend: {
  352. data:['商户用量', '商户金额', '公摊用量', '公摊金额']
  353. },
  354. xAxis: {
  355. type: 'category',
  356. data: xData,
  357. axisPointer: {
  358. type: 'shadow'
  359. }
  360. },
  361. yAxis: [
  362. {
  363. name: meterCls === 45 ? 'kW·h(千瓦时)' : '吨',
  364. type: 'value'
  365. },
  366. {
  367. name: '¥(元)',
  368. type: 'value'
  369. }
  370. ],
  371. series: [
  372. {
  373. name: '商户用量',
  374. type: 'bar',
  375. data: quantity,
  376. barWidth: 30,
  377. itemStyle: {
  378. normal: {
  379. color: '#6395FA'
  380. }
  381. }
  382. },
  383. {
  384. name: '公摊用量',
  385. type: 'bar',
  386. data: shareQuantity,
  387. barWidth: 30,
  388. itemStyle: {
  389. normal: {
  390. color: '#8CDF6C'
  391. }
  392. }
  393. },
  394. {
  395. name: '商户金额',
  396. type: 'line',
  397. yAxisIndex: 1,
  398. data: cost,
  399. showSymbol: true,
  400. itemStyle: {
  401. normal: {
  402. color: '#5BD9A5'
  403. }
  404. }
  405. },
  406. {
  407. name: '公摊金额',
  408. type: 'line',
  409. yAxisIndex: 1,
  410. data: shareCost,
  411. showSymbol: true,
  412. }
  413. ]
  414. }
  415. }
  416. return option
  417. },
  418. elecOptions() {
  419. const xData = this.historyData.map(item => item.meteredTime)
  420. const quantity = this.historyData.map(item => item.meteredValue)
  421. const cost = this.historyData.map(item => item.meteredPrice)
  422. const { meterCls } = this.queryParams
  423. const option = {
  424. tooltip: {
  425. trigger: 'axis',
  426. axisPointer: {
  427. type: 'cross',
  428. crossStyle: {
  429. color: '#999'
  430. }
  431. }
  432. },
  433. grid: {
  434. left: '5%'
  435. },
  436. legend: {
  437. data: ['用量', '金额']
  438. },
  439. xAxis: {
  440. type: 'category',
  441. data: xData,
  442. axisPointer: {
  443. type: 'shadow'
  444. }
  445. },
  446. yAxis: [
  447. {
  448. name: meterCls === 45 ? 'kW·h(千瓦时)' : '吨',
  449. type: 'value'
  450. },
  451. {
  452. name: '¥(元)',
  453. type: 'value'
  454. }
  455. ],
  456. series: [
  457. {
  458. name: '用量',
  459. type: 'bar',
  460. data: quantity,
  461. barWidth: 30,
  462. itemStyle: {
  463. normal: {
  464. color: '#6395FA'
  465. }
  466. }
  467. },
  468. {
  469. name: '金额',
  470. type: 'line',
  471. yAxisIndex: 1,
  472. data: cost,
  473. showSymbol: true,
  474. itemStyle: {
  475. normal: {
  476. color: '#5BD9A5'
  477. }
  478. }
  479. }
  480. ]
  481. }
  482. return option
  483. }
  484. },
  485. methods: {
  486. meterClsChange(value) {
  487. this.queryParams.meterCls = value;
  488. this.getList('current')
  489. this.getList()
  490. },
  491. setDefaultMonthRange() {
  492. const nowDay = new Date()
  493. const lastMonth = new Date(nowDay.getFullYear(), nowDay.getMonth() - 1)
  494. const months = this.getPreviousFiveMonthsYearMonth(lastMonth)
  495. const {year, month} = months[months.length - 1]
  496. const lastFiveMonth = month < 10 ? `0${month}` : month
  497. this.queryParams.historyRange = [`${year}${lastFiveMonth}`, this.lastMonth]
  498. },
  499. getPreviousFiveMonthsYearMonth(date) {
  500. const months = [...Array(5).keys()].map(i => {
  501. const year = date.getFullYear()
  502. const month = date.getMonth() - i
  503. if (month < 0) {
  504. return {
  505. year: year - 1,
  506. month: 12 + month
  507. }
  508. }
  509. return {year, month: month + 1}
  510. })
  511. return months
  512. },
  513. formatDict(val, options, key = 'code', text = 'name') {
  514. let name = null
  515. this[options].forEach(item => {
  516. if (item[key] === val) {
  517. name = item[text]
  518. }
  519. })
  520. return name
  521. },
  522. async getList(type) {
  523. let startTime = null;
  524. let endTime = null;
  525. const { areaCode, meterCls, historyRange } = this.queryParams;
  526. if (type === 'current') {
  527. startTime = this.lastMonth;
  528. endTime = this.lastMonth;
  529. } else {
  530. startTime = historyRange[0];
  531. endTime = historyRange[1];
  532. }
  533. try {
  534. // 个户
  535. if (this.activeTab === 'second') {
  536. const response = await chargingList({
  537. areaPath: areaCode,
  538. tier: 'Zoning',
  539. meterCls,
  540. startTime,
  541. endTime
  542. });
  543. if (response && response.data) {
  544. if (type === 'current') {
  545. this.currentData = response.data;
  546. console.log("现在数据", this.currentData);
  547. } else {
  548. this.historyData = response.data;
  549. }
  550. } else {
  551. console.error('请求数据失败,无数据返回');
  552. }
  553. } else {
  554. const response = await overviewInfo({
  555. areaPath: areaCode,
  556. tier: 'Area',
  557. meterCls,
  558. startTime,
  559. endTime
  560. });
  561. if (response && response.data) {
  562. if (type === 'current') {
  563. this.currentData = response.data;
  564. } else {
  565. this.historyData = response.data;
  566. }
  567. } else {
  568. console.error('请求数据失败,无数据返回');
  569. }
  570. }
  571. } catch (error) {
  572. console.error('请求数据失败123:', error);
  573. }
  574. },
  575. async handleTabChange() {
  576. if (this.activeTab === 'first') {
  577. await this.getAreaTreeByTag('Area', 'Area_01');
  578. if (this.areaOptions.length > 0) {
  579. // 设置默认展开的节点
  580. this.defalutArr = [];
  581. this.recursion(this.areaOptions[0], this.defalutArr);
  582. this.queryParams.areaCode = this.defalutArr.join('/');
  583. this.$refs['tree'].setCurrentKey(this.defalutArr[this.defalutArr.length - 1]);
  584. this.$refs['tree'].setCheckedKeys([this.queryParams.areaCode]);
  585. }
  586. } else if (this.activeTab === 'second') {
  587. await this.getAreaTreeByTag('Zoning', 'Area_01');
  588. this.defalutArr = [];
  589. this.recursion(this.areaOptions[0], this.defalutArr);
  590. this.queryParams.areaCode = this.defalutArr.join('/');
  591. this.$refs['tree'].setCurrentKey(this.defalutArr[this.defalutArr.length - 1]);
  592. this.$refs['tree'].setCheckedKeys([this.queryParams.areaCode]);
  593. }
  594. this.meterClsChange(45);
  595. },
  596. recursion(data, defalutArr) {
  597. defalutArr.push(data.id)
  598. if (data.children && data.children.length) {
  599. this.recursion(data.children[0], defalutArr)
  600. }
  601. },
  602. /** 查询区域树结构 */
  603. async getAreaTreeByTag(tier, tagCode) {
  604. await areaTreeSelectByTag(tier, tagCode).then(response => {
  605. this.areaOptions = response.data
  606. })
  607. },
  608. // 筛选节点
  609. filterNode(value, data) {
  610. if (!value) return true
  611. return data.label.indexOf(value) !== -1
  612. },
  613. handleNodeClick(data, node) {
  614. // 商户树形结构只能点击最后一级
  615. if (this.activeTab === 'second' && node.level < 3) {
  616. return this.$refs['tree'].setCurrentKey(null);
  617. }
  618. const nodeArr = [];
  619. const selectNode = this.$refs.tree.getNode(data);
  620. this.platform(selectNode, nodeArr);
  621. this.queryParams.areaCode = nodeArr.map(item => item.id).join('/');
  622. this.$refs['tree'].setCurrentKey(this.queryParams.areaCode);
  623. this.getList('current'); // 获取当月数据
  624. this.getList('history'); // 获取历史数据
  625. },
  626. // 递归函数
  627. platform(node, array) {
  628. if (!node.parent) {
  629. return;
  630. }
  631. array.unshift(node.data);
  632. this.platform(node.parent, array);
  633. },
  634. }
  635. }
  636. </script>
  637. <style lang="scss" scoped>
  638. .app-container {
  639. ::v-deep .el-tabs__content {
  640. overflow: initial;
  641. }
  642. }
  643. .custom-form {
  644. padding-bottom: 10px;
  645. }
  646. .panel {
  647. display: flex;
  648. flex-direction: column;
  649. .panel-title {
  650. display: flex;
  651. align-items: center;
  652. font-size: 14px;
  653. font-weight: 500;
  654. &::before {
  655. content: '';
  656. display: inline-block;
  657. height: 14px;
  658. width: 3px;
  659. border-radius: 6px;
  660. background: #409eff;
  661. margin-right: 5px;
  662. }
  663. }
  664. .panel-body {
  665. flex: 1;
  666. background: #fff;
  667. padding: 10px 0;
  668. }
  669. }
  670. </style>