RepairWorkHour.vue 15 KB

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