costs.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418
  1. <script lang="ts" setup>
  2. import { ref, onMounted } from 'vue'
  3. import dayjs from 'dayjs'
  4. import CountTo from '@/components/count-to1.vue'
  5. import { IotReportApi } from '@/api/pms/report'
  6. import { useDebounceFn } from '@vueuse/core'
  7. import download from '@/utils/download'
  8. import { rangeShortcuts } from '@/utils/formatTime'
  9. // 定义时间类型
  10. type TimeType = 'year' | 'month' | 'day'
  11. interface Query {
  12. deptId?: number
  13. createTime?: [string, string]
  14. pageNo: number
  15. pageSize: number
  16. type?: string
  17. }
  18. // 选项配置数组
  19. const timeOptions: { label: string; value: TimeType }[] = [
  20. { label: '年', value: 'year' },
  21. { label: '月', value: 'month' },
  22. { label: '日', value: 'day' }
  23. ]
  24. const activeTimeType = ref<TimeType | undefined>('year')
  25. const query = ref<Query>({
  26. pageNo: 1,
  27. pageSize: 10
  28. })
  29. const handleTimeChange = (type: TimeType, init = false) => {
  30. activeTimeType.value = type
  31. const formatStr = 'YYYY-MM-DD HH:mm:ss'
  32. const endTime = dayjs().endOf('day').format(formatStr)
  33. let startTime = ''
  34. switch (type) {
  35. case 'year':
  36. startTime = dayjs().startOf('year').format(formatStr)
  37. break
  38. case 'month':
  39. startTime = dayjs().startOf('month').format(formatStr)
  40. break
  41. case 'day':
  42. startTime = dayjs().startOf('day').format(formatStr)
  43. break
  44. }
  45. query.value.createTime = [startTime, endTime]
  46. console.log(`切换为[${type}]:`, query.value.createTime)
  47. if (!init) loadData()
  48. }
  49. interface StatItem {
  50. key: string
  51. title: string
  52. icon: string
  53. type: string
  54. class: { bg1: string; bg2: string; text: string }
  55. value: number
  56. tb: number
  57. hb: number
  58. }
  59. const defaultStats: StatItem[] = [
  60. {
  61. key: 'repair',
  62. title: '内部维修成本',
  63. type: '内部维修',
  64. icon: 'i-material-symbols:home-repair-service-outline',
  65. class: {
  66. bg1: 'bg-[var(--el-color-success-light-7)]',
  67. bg2: 'bg-[var(--el-color-success-light-5)]',
  68. text: 'text-[var(--el-color-success)]'
  69. },
  70. value: 0,
  71. tb: 0,
  72. hb: 0
  73. },
  74. {
  75. key: 'byFee',
  76. title: '保养成本',
  77. type: '保养',
  78. icon: 'i-material-symbols:construction-rounded',
  79. class: {
  80. bg1: 'bg-[var(--el-color-primary-light-7)]',
  81. bg2: 'bg-[var(--el-color-primary-light-5)]',
  82. text: 'text-[var(--el-color-primary)]'
  83. },
  84. value: 0,
  85. tb: 0,
  86. hb: 0
  87. },
  88. {
  89. key: 'out',
  90. title: '委外维修',
  91. type: '委外维修',
  92. icon: 'i-material-symbols:work-outline',
  93. class: {
  94. bg1: 'bg-[var(--el-color-warning-light-7)]',
  95. bg2: 'bg-[var(--el-color-warning-light-5)]',
  96. text: 'text-[var(--el-color-warning)]'
  97. },
  98. value: 0,
  99. tb: 0,
  100. hb: 0
  101. }
  102. ]
  103. const statList = ref<StatItem[]>(defaultStats)
  104. const dataLoading = ref(false)
  105. const loadData = useDebounceFn(async function () {
  106. dataLoading.value = true
  107. try {
  108. const { pageNo, pageSize, type, ...other } = query.value
  109. const res = await IotReportApi.getCostsFee({ ...other, timeType: activeTimeType.value })
  110. const repair = statList.value[0]!
  111. const out = statList.value[2]!
  112. const byFee = statList.value[1]!
  113. out.value = res[out.key].total || 0
  114. out.tb = Number(((res[out.key].tb || 0) * 100).toFixed(0))
  115. out.hb = Number(((res[out.key].hb || 0) * 100).toFixed(0))
  116. repair.value = res[repair.key].total || 0
  117. repair.tb = Number(((res[repair.key].tb || 0) * 100).toFixed(0))
  118. repair.hb = Number(((res[repair.key].hb || 0) * 100).toFixed(0))
  119. byFee.value = res[byFee.key].total || 0
  120. byFee.tb = Number(((res[byFee.key].tb || 0) * 100).toFixed(0))
  121. byFee.hb = Number(((res[byFee.key].hb || 0) * 100).toFixed(0))
  122. } catch (error) {
  123. console.log('error :>> ', error)
  124. } finally {
  125. dataLoading.value = false
  126. }
  127. }, 500)
  128. const list = ref<any[]>([])
  129. const loading = ref(false)
  130. const total = ref(0)
  131. function handleSizeChange(val: number) {
  132. query.value.pageSize = val
  133. query.value.pageNo = 1
  134. loadList()
  135. }
  136. function handleCurrentChange(val: number) {
  137. query.value.pageNo = val
  138. loadList()
  139. }
  140. function handleQuery(setPage = true) {
  141. if (setPage) {
  142. query.value.pageNo = 1
  143. }
  144. loadList()
  145. loadData()
  146. }
  147. const loadList = useDebounceFn(async function () {
  148. loading.value = true
  149. try {
  150. const data = await IotReportApi.getCostsPage(query.value)
  151. list.value = data.list
  152. total.value = data.total
  153. // nextTick(() => {
  154. // calculateColumnWidths(columns.value)
  155. // })
  156. } finally {
  157. loading.value = false
  158. }
  159. }, 500)
  160. onMounted(() => {
  161. handleTimeChange('year', true)
  162. })
  163. watch(
  164. [() => query.value.createTime, () => query.value.deptId],
  165. () => {
  166. handleQuery()
  167. },
  168. { immediate: true }
  169. )
  170. function selectType(type: string | undefined) {
  171. query.value.type = type
  172. query.value.pageNo = 1
  173. loadList()
  174. }
  175. function handleReset() {
  176. handleTimeChange('year')
  177. selectType(undefined)
  178. }
  179. const exportLoading = ref(false)
  180. const handleExport = async () => {
  181. exportLoading.value = true
  182. try {
  183. const data = await IotReportApi.exportCosts(query.value)
  184. download.excel(data, '运维成本.xls')
  185. } finally {
  186. exportLoading.value = false
  187. }
  188. }
  189. const handleClear = () => {
  190. handleTimeChange('year')
  191. }
  192. const handleChange = () => {
  193. activeTimeType.value = undefined
  194. }
  195. </script>
  196. <template>
  197. <div
  198. class="grid grid-cols-[auto_1fr] grid-rows-[196px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
  199. >
  200. <DeptTreeSelect
  201. :top-id="156"
  202. :deptId="156"
  203. v-model="query.deptId"
  204. :init-select="false"
  205. :show-title="false"
  206. class="row-span-2"
  207. />
  208. <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
  209. <div class="grid grid-rows-[1fr_32px] gap-4">
  210. <div class="grid grid-cols-3 gap-4" v-loading="dataLoading">
  211. <!-- 使用 v-for 循环渲染 -->
  212. <section
  213. v-for="item in statList"
  214. :key="item.key"
  215. class="flex flex-col items-center gap-y-4 rounded-lg shadow p-4 transition-transform hover:scale-105 duration-500"
  216. :class="{ [item.class.bg1]: true, 'scale-105': item.type === query.type }"
  217. @click="selectType(item.type)"
  218. >
  219. <!-- 头部:图标 + 标题 -->
  220. <div class="flex items-center gap-x-3">
  221. <div class="rounded-2 p-2" :class="[item.class.text, item.class.bg2]">
  222. <div :class="item.icon"></div>
  223. </div>
  224. <div class="text-[var(--el-text-color-primary)] text-sm font-medium">
  225. {{ item.title }}
  226. </div>
  227. </div>
  228. <!-- 数值区域:CountTo -->
  229. <count-to
  230. class="text-3xl font-semibold"
  231. :start-val="0"
  232. :end-val="item.value"
  233. :decimals="2"
  234. suffix="元"
  235. :duration="1000"
  236. >
  237. <!-- 插槽内容:当数据为空或0时的显示 (根据 count-to 组件的具体实现决定是否显示) -->
  238. <span class="text-xs leading-8 text-[var(--el-text-color-regular)]">暂无数据</span>
  239. </count-to>
  240. <!-- 底部:环比数据 -->
  241. <!-- 根据 trend 正负动态改变图标和颜色 -->
  242. <div class="mt-2 flex items-center gap-x-4">
  243. <div
  244. v-show="item.hb"
  245. class="flex items-center gap-x-1 text-xs font-medium"
  246. :class="item.class.text"
  247. >
  248. <!-- 动态图标:大于等于0向上,小于0向下 -->
  249. <div
  250. :class="
  251. item.hb >= 0
  252. ? 'i-material-symbols:arrow-warm-up-rounded'
  253. : 'i-material-symbols:arrow-cool-down-rounded'
  254. "
  255. ></div>
  256. <span class="vertical-middle"> {{ item.hb > 0 ? '+' + item.hb : item.hb }}% </span>
  257. <span>环比</span>
  258. </div>
  259. <div
  260. v-show="item.tb"
  261. class="flex items-center gap-x-1 text-xs font-medium"
  262. :class="item.class.text"
  263. >
  264. <!-- 动态图标:大于等于0向上,小于0向下 -->
  265. <div
  266. :class="
  267. item.tb >= 0
  268. ? 'i-material-symbols:arrow-warm-up-rounded'
  269. : 'i-material-symbols:arrow-cool-down-rounded'
  270. "
  271. ></div>
  272. <span class="vertical-middle"> {{ item.tb > 0 ? '+' + item.tb : item.tb }}% </span>
  273. <span>同比</span>
  274. </div>
  275. </div>
  276. </section>
  277. </div>
  278. <div class="flex justify-between gap-4">
  279. <div class="flex items-center gap-4">
  280. <el-date-picker
  281. size="default"
  282. v-model="query.createTime"
  283. value-format="YYYY-MM-DD HH:mm:ss"
  284. type="daterange"
  285. start-placeholder="开始日期"
  286. end-placeholder="结束日期"
  287. :shortcuts="rangeShortcuts"
  288. class="!w-220px"
  289. @clear="handleClear"
  290. @change="handleChange"
  291. :clearable="false"
  292. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  293. />
  294. <el-button-group size="default">
  295. <el-button
  296. v-for="item in timeOptions"
  297. :key="item.value"
  298. :type="activeTimeType === item.value ? 'primary' : ''"
  299. @click="handleTimeChange(item.value)"
  300. >
  301. {{ item.label }}
  302. </el-button>
  303. </el-button-group>
  304. </div>
  305. <div class="flex items-center gap-2">
  306. <el-button size="default" @click="handleReset">重置</el-button>
  307. <el-button
  308. size="default"
  309. plain
  310. type="success"
  311. @click="handleExport"
  312. :loading="exportLoading"
  313. >
  314. <Icon icon="ep:download" class="mr-5px" /> 导出
  315. </el-button>
  316. </div>
  317. </div>
  318. </div>
  319. <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col mt-4">
  320. <div class="flex-1 relative">
  321. <el-auto-resizer class="absolute">
  322. <template #default="{ width, height }">
  323. <el-table
  324. :data="list"
  325. v-loading="loading"
  326. stripe
  327. class="absolute"
  328. :max-height="height"
  329. :height="height"
  330. show-overflow-tooltip
  331. :width="width"
  332. scrollbar-always-on
  333. >
  334. <el-table-column label="序号" type="index" width="50" align="center" />
  335. <el-table-column label="日期" prop="date" align="center" />
  336. <el-table-column label="类别" prop="type" align="center" />
  337. <el-table-column label="设备编号" prop="deviceCode" align="center" />
  338. <el-table-column label="设备名称" prop="deviceName" align="center" />
  339. <el-table-column
  340. label="成本"
  341. prop="cost"
  342. align="center"
  343. :formatter="(row) => (row.cost ?? 0) + '元'"
  344. />
  345. </el-table>
  346. </template>
  347. </el-auto-resizer>
  348. </div>
  349. <div class="h-10 mt-4 flex items-center justify-end">
  350. <el-pagination
  351. size="default"
  352. v-show="total > 0"
  353. v-model:current-page="query.pageNo"
  354. v-model:page-size="query.pageSize"
  355. :background="true"
  356. :page-sizes="[10, 20, 30, 50, 100]"
  357. :total="total"
  358. layout="total, sizes, prev, pager, next, jumper"
  359. @size-change="handleSizeChange"
  360. @current-change="handleCurrentChange"
  361. />
  362. </div>
  363. </div>
  364. </div>
  365. </template>
  366. <style scoped>
  367. :deep(.el-table) {
  368. border-top-right-radius: 8px;
  369. border-top-left-radius: 8px;
  370. .el-table__cell {
  371. height: 52px;
  372. }
  373. .el-table__header-wrapper {
  374. .el-table__cell {
  375. background: var(--el-fill-color-light);
  376. }
  377. }
  378. }
  379. </style>