RepairWorkHour.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411
  1. <template>
  2. <div class="page-header-index-wide">
  3. <a-card :bordered="false">
  4. <a-tabs v-model="activeTab" @change="handleTabChange">
  5. <a-tab-pane tab="按维修人统计" key="user"></a-tab-pane>
  6. <a-tab-pane tab="按设备统计" key="sb"></a-tab-pane>
  7. </a-tabs>
  8. <div class="table-page-search-wrapper" @keyup.enter="handleSearch">
  9. <a-form layout="inline">
  10. <a-row :gutter="48">
  11. <a-col :md="8" :sm="24" v-if="activeTab === 'user'">
  12. <a-form-item label="维修人">
  13. <a-input v-model="queryParam.repairUserName" placeholder="请输入维修人姓名" allow-clear/>
  14. </a-form-item>
  15. </a-col>
  16. <a-col :md="8" :sm="24" v-else>
  17. <a-form-item label="设备">
  18. <a-input v-model="queryParam.keyword" placeholder="请输入设备名称/编号" allow-clear/>
  19. </a-form-item>
  20. </a-col>
  21. <a-col :md="10" :sm="24">
  22. <a-form-item label="维修开始时间">
  23. <a-range-picker
  24. v-model="dateRange"
  25. format="YYYY-MM-DD"
  26. style="width: 100%"
  27. :placeholder="['开始日期', '结束日期']"/>
  28. </a-form-item>
  29. </a-col>
  30. <a-col :md="6" :sm="24">
  31. <span class="table-page-search-submitButtons">
  32. <a-button type="primary" @click="handleSearch">查询</a-button>
  33. <a-button style="margin-left: 8px" @click="resetSearchForm">重置</a-button>
  34. </span>
  35. </a-col>
  36. </a-row>
  37. </a-form>
  38. </div>
  39. <div class="table-operator">
  40. <a-button type="primary" icon="download" @click="doExport">导出</a-button>
  41. </div>
  42. <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px">
  43. <span style="font-weight: 600">{{ chartTitle }}(按总维修工时降序)</span>
  44. <span>
  45. <a-radio-group v-if="activeTab === 'sb'" v-model="chartDim" size="small" style="margin-right: 8px" @change="doRenderChart">
  46. <a-radio-button value="sb">按设备</a-radio-button>
  47. <a-radio-button value="type">按设备类型</a-radio-button>
  48. </a-radio-group>
  49. <a-radio-group v-model="chartTop" size="small" @change="doRenderChart">
  50. <a-radio-button value="top20">前20名</a-radio-button>
  51. <a-radio-button value="all">全部</a-radio-button>
  52. </a-radio-group>
  53. </span>
  54. </div>
  55. <a-spin :spinning="chartLoading">
  56. <div :id="chartContainerId" style="min-height: 400px"></div>
  57. </a-spin>
  58. <s-table
  59. ref="table"
  60. :key="activeTab"
  61. size="default"
  62. :rowKey="rowKey"
  63. :columns="columns"
  64. :data="loadData"
  65. showPagination="auto"
  66. >
  67. <span slot="action" slot-scope="record">
  68. <template>
  69. <a @click="handleDetail(record)">查看明细</a>
  70. <a-divider type="vertical"/>
  71. <a @click="doExportDetail(record)">导出明细</a>
  72. </template>
  73. </span>
  74. </s-table>
  75. </a-card>
  76. <detail-modal ref="detailModal"/>
  77. </div>
  78. </template>
  79. <script>
  80. import { STable } from '@/components'
  81. import { Chart } from '@antv/g2'
  82. import moment from 'moment'
  83. import DetailModal from './modules/DetailModal'
  84. import {
  85. getWorkHourByUser,
  86. getWorkHourBySb,
  87. exportWorkHourUser,
  88. exportWorkHourSb,
  89. exportWorkHourDetail
  90. } from '@/api/report/application-form'
  91. export default {
  92. name: 'RepairWorkHourReport',
  93. components: {
  94. STable,
  95. DetailModal
  96. },
  97. data () {
  98. return {
  99. // 当前统计维度:user-按维修人,sb-按设备
  100. activeTab: 'user',
  101. // 查询参数
  102. queryParam: {
  103. repairUserName: null,
  104. keyword: null
  105. },
  106. // 维修开始时间范围 默认本月,避免一次性拉取全部历史数据
  107. dateRange: [moment().startOf('month'), moment().endOf('month')],
  108. // 图表
  109. chart: null,
  110. chartLoading: false,
  111. chartContainerId: 'workHourChartContainer',
  112. // 图表展示范围:top20-前20名,all-全部(数据量大时默认前20,防止柱子过细/标签重叠)
  113. chartTop: 'top20',
  114. // 图表聚合维度(仅按设备统计Tab):sb-按设备,type-按设备类型(前端对全量数据分组)
  115. chartDim: 'sb',
  116. // 图表全量数据缓存,切换前20/全部时不重复请求
  117. chartRows: [],
  118. // 加载数据方法 必须为 Promise 对象
  119. loadData: parameter => {
  120. const query = this.buildQuery()
  121. parameter = {
  122. ...parameter,
  123. ...query
  124. }
  125. const fetchFn = this.activeTab === 'user' ? getWorkHourByUser : getWorkHourBySb
  126. return fetchFn(parameter)
  127. .then(res => {
  128. return res.data
  129. })
  130. }
  131. }
  132. },
  133. computed: {
  134. chartTitle () {
  135. if (this.activeTab === 'user') {
  136. return '维修人工时统计图'
  137. }
  138. return this.chartDim === 'type' ? '设备类型工时统计图' : '设备工时统计图'
  139. },
  140. rowKey () {
  141. return this.activeTab === 'user' ? 'repairUserId' : 'sbId'
  142. },
  143. columns () {
  144. const indexColumn = {
  145. title: '序号',
  146. checked: true,
  147. dataIndex: 'index',
  148. width: '70px',
  149. customRender: (text, record, index) => {
  150. const pagination = this.$refs.table && this.$refs.table.localPagination
  151. if (pagination) {
  152. return `${(pagination.current - 1) * pagination.pageSize + index + 1}`
  153. }
  154. return `${index + 1}`
  155. }
  156. }
  157. const hourRender = (text) => {
  158. return this.BaseTool.Object.isBlank(text) ? '0.00' : Number(text).toFixed(2)
  159. }
  160. const commonColumns = [
  161. {
  162. title: '维修次数',
  163. checked: true,
  164. dataIndex: 'num'
  165. },
  166. {
  167. title: '总维修工时(小时)',
  168. checked: true,
  169. dataIndex: 'totalRepairHours',
  170. customRender: hourRender
  171. },
  172. {
  173. title: '平均工时(小时)',
  174. checked: true,
  175. dataIndex: 'avgRepairHours',
  176. customRender: hourRender
  177. },
  178. {
  179. title: '总停机修复时长(小时)',
  180. checked: true,
  181. dataIndex: 'totalDealHours',
  182. customRender: hourRender
  183. },
  184. {
  185. title: '操作',
  186. checked: true,
  187. key: 'action',
  188. width: '180px',
  189. align: 'center',
  190. scopedSlots: { customRender: 'action' }
  191. }
  192. ]
  193. if (this.activeTab === 'user') {
  194. return [
  195. indexColumn,
  196. {
  197. title: '维修人',
  198. checked: true,
  199. dataIndex: 'repairUserName'
  200. },
  201. ...commonColumns
  202. ]
  203. }
  204. return [
  205. indexColumn,
  206. {
  207. title: '设备编号',
  208. checked: true,
  209. dataIndex: 'sbNo'
  210. },
  211. {
  212. title: '设备名称',
  213. checked: true,
  214. dataIndex: 'sbName'
  215. },
  216. {
  217. title: '设备类型',
  218. checked: true,
  219. dataIndex: 'sbTypeName'
  220. },
  221. ...commonColumns
  222. ]
  223. }
  224. },
  225. mounted () {
  226. this.$nextTick(() => {
  227. this.loadChartData()
  228. })
  229. },
  230. beforeDestroy () {
  231. this.chart && this.chart.destroy()
  232. this.chart = null
  233. },
  234. methods: {
  235. loadChartData () {
  236. this.chartLoading = true
  237. const fetchFn = this.activeTab === 'user' ? getWorkHourByUser : getWorkHourBySb
  238. const parameter = {
  239. pageNum: 1,
  240. pageSize: 500,
  241. ...this.buildQuery()
  242. }
  243. fetchFn(parameter)
  244. .then(res => {
  245. const rows = (res.data && res.data.rows) || []
  246. // 保存原始行,按图表维度分组/截取统一在 doRenderChart 处理
  247. this.chartRows = rows
  248. this.doRenderChart()
  249. })
  250. .finally(() => {
  251. this.chartLoading = false
  252. })
  253. },
  254. doRenderChart () {
  255. let data = []
  256. if (this.activeTab === 'user') {
  257. data = this.chartRows.map(r => ({
  258. name: r.repairUserName || '-',
  259. totalRepairHours: Number(r.totalRepairHours || 0),
  260. num: Number(r.num || 0)
  261. }))
  262. } else if (this.chartDim === 'type') {
  263. // 按设备类型:前端对按设备全量数据分组汇总,次数/总工时求和,不重复请求后端
  264. const groupMap = {}
  265. this.chartRows.forEach(r => {
  266. const key = r.sbTypeName || '未分类'
  267. if (!groupMap[key]) {
  268. groupMap[key] = { name: key, totalRepairHours: 0, num: 0 }
  269. }
  270. groupMap[key].totalRepairHours += Number(r.totalRepairHours || 0)
  271. groupMap[key].num += Number(r.num || 0)
  272. })
  273. data = Object.keys(groupMap).map(key => {
  274. const item = groupMap[key]
  275. item.totalRepairHours = Math.round(item.totalRepairHours * 100) / 100
  276. return item
  277. })
  278. data.sort((a, b) => b.totalRepairHours - a.totalRepairHours)
  279. } else {
  280. // 设备维度用名称(编号)区分同名设备
  281. data = this.chartRows.map(r => ({
  282. name: (r.sbName || '-') + '(' + (r.sbNo || '-') + ')',
  283. totalRepairHours: Number(r.totalRepairHours || 0),
  284. num: Number(r.num || 0)
  285. }))
  286. }
  287. if (this.chartTop === 'top20' && data.length > 20) {
  288. data = data.slice(0, 20)
  289. }
  290. this.chart && this.chart.destroy()// 防止重复渲染
  291. this.chart = null
  292. if (!data.length) {
  293. return
  294. }
  295. this.$nextTick(() => {
  296. this.chart = new Chart({
  297. container: this.chartContainerId,
  298. autoFit: true,
  299. height: 400
  300. })
  301. this.chart.data(data)
  302. this.chart.scale({
  303. totalRepairHours: { alias: '总维修工时(小时)', nice: true },
  304. num: { alias: '维修次数', nice: true }
  305. })
  306. this.chart.axis('name', { label: { autoRotate: true, autoHide: false } })
  307. this.chart.axis('totalRepairHours', { title: { offset: 40 } })
  308. this.chart.axis('num', { title: { offset: 40 }, grid: null })
  309. this.chart.tooltip({ shared: true, showCrosshairs: false })
  310. this.chart.legend({ position: 'top-right' })
  311. this.chart.interval().position('name*totalRepairHours')
  312. this.chart.line().position('name*num').color('#faad14').size(2)
  313. this.chart.point().position('name*num').color('#faad14').size(3).shape('circle')
  314. this.chart.interaction('active-region')
  315. // 全部模式下分类过多时出现横向滚动条,避免柱子过细、标签重叠
  316. this.chart.option('scrollbar', {
  317. type: 'horizontal'
  318. })
  319. this.chart.render()
  320. })
  321. },
  322. buildQuery () {
  323. const query = {}
  324. if (this.activeTab === 'user') {
  325. if (this.BaseTool.String.isNotBlank(this.queryParam.repairUserName)) {
  326. query.repairUserName = this.queryParam.repairUserName
  327. }
  328. } else {
  329. if (this.BaseTool.String.isNotBlank(this.queryParam.keyword)) {
  330. query.keyword = this.queryParam.keyword
  331. }
  332. }
  333. if (this.dateRange && this.dateRange.length === 2) {
  334. query.searchRepairStartTime = this.dateRange[0].format('YYYY-MM-DD') + ' 00:00:00'
  335. query.searchRepairEndTime = this.dateRange[1].format('YYYY-MM-DD') + ' 23:59:59'
  336. }
  337. return query
  338. },
  339. handleTabChange () {
  340. // 切换维度后图表聚合维度恢复按设备
  341. this.chartDim = 'sb'
  342. this.$nextTick(() => {
  343. this.$refs.table.refresh(true)
  344. this.loadChartData()
  345. })
  346. },
  347. handleSearch () {
  348. this.$refs.table.refresh(true)
  349. this.loadChartData()
  350. },
  351. resetSearchForm () {
  352. this.queryParam = {
  353. repairUserName: null,
  354. keyword: null
  355. }
  356. // 重置后仍默认统计本月
  357. this.dateRange = [moment().startOf('month'), moment().endOf('month')]
  358. this.$refs.table.refresh(true)
  359. this.loadChartData()
  360. },
  361. doExport () {
  362. const parameter = this.buildQuery()
  363. const exportFn = this.activeTab === 'user' ? exportWorkHourUser : exportWorkHourSb
  364. exportFn(parameter).then(file => {
  365. this.BaseTool.UPLOAD.downLoadExportExcel(file)
  366. })
  367. },
  368. handleDetail (record) {
  369. const modal = this.$refs.detailModal
  370. modal.base({
  371. type: this.activeTab,
  372. record: record,
  373. searchRepairStartTime: this.buildQuery().searchRepairStartTime,
  374. searchRepairEndTime: this.buildQuery().searchRepairEndTime
  375. })
  376. },
  377. doExportDetail (record) {
  378. const parameter = this.buildDetailQuery(record)
  379. exportWorkHourDetail(parameter).then(file => {
  380. this.BaseTool.UPLOAD.downLoadExportExcel(file)
  381. })
  382. },
  383. buildDetailQuery (record) {
  384. const parameter = {}
  385. if (this.activeTab === 'user') {
  386. parameter.repairUserId = record.repairUserId
  387. } else {
  388. parameter.sbId = record.sbId
  389. }
  390. const timeQuery = this.buildQuery()
  391. if (timeQuery.searchRepairStartTime) {
  392. parameter.searchRepairStartTime = timeQuery.searchRepairStartTime
  393. }
  394. if (timeQuery.searchRepairEndTime) {
  395. parameter.searchRepairEndTime = timeQuery.searchRepairEndTime
  396. }
  397. return parameter
  398. }
  399. }
  400. }
  401. </script>
  402. <style lang="less" scoped>
  403. </style>