work-order-completion.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. <script lang="ts" setup>
  2. import { ref, onMounted } from 'vue'
  3. import dayjs from 'dayjs'
  4. import { useDebounceFn } from '@vueuse/core'
  5. import MiniBarChart from '@/components/WorkOrderCompletionBar/index.vue'
  6. import CountTo from '@/components/count-to1.vue'
  7. import { IotReportApi } from '@/api/pms/report'
  8. import { rangeShortcuts } from '@/utils/formatTime'
  9. import download from '@/utils/download'
  10. // 定义时间类型
  11. type TimeType = 'year' | 'month' | 'day'
  12. interface Query {
  13. deptId?: number
  14. createTime?: [string, string]
  15. type?: string
  16. pageNo: number
  17. pageSize: number
  18. }
  19. interface ChartDataItem {
  20. label: string
  21. num: number
  22. }
  23. interface StatItem {
  24. key: string
  25. title: string
  26. type: string
  27. icon: string
  28. total: number
  29. charts: ChartDataItem[]
  30. tb: number
  31. hb: number
  32. }
  33. // 选项配置数组
  34. const timeOptions: { label: string; value: TimeType }[] = [
  35. { label: '年', value: 'year' },
  36. { label: '月', value: 'month' },
  37. { label: '日', value: 'day' }
  38. ]
  39. const activeTimeType = ref<TimeType | undefined>('year')
  40. const query = ref<Query>({
  41. pageNo: 1,
  42. pageSize: 10
  43. })
  44. const defaultStats: StatItem[] = [
  45. {
  46. key: 'yx',
  47. title: '运行记录',
  48. type: '运行记录',
  49. icon: 'i-material-symbols:list-alt-outline',
  50. total: 0,
  51. charts: [],
  52. tb: 0,
  53. hb: 0
  54. },
  55. {
  56. key: 'rb',
  57. title: '生产日报',
  58. type: '日报',
  59. icon: 'i-material-symbols:calendar-today-outline',
  60. total: 0,
  61. charts: [],
  62. tb: 0,
  63. hb: 0
  64. },
  65. {
  66. key: 'wx',
  67. title: '维修工单',
  68. type: '维修工单',
  69. icon: 'i-material-symbols:home-repair-service-outline',
  70. total: 0,
  71. charts: [],
  72. tb: 0,
  73. hb: 0
  74. },
  75. {
  76. key: 'by',
  77. title: '保养工单',
  78. type: '保养工单',
  79. icon: 'i-material-symbols:construction-rounded',
  80. total: 0,
  81. charts: [],
  82. tb: 0,
  83. hb: 0
  84. },
  85. {
  86. key: 'xj',
  87. title: '巡检工单',
  88. type: '巡检工单',
  89. icon: 'i-material-symbols:warning-outline',
  90. total: 0,
  91. charts: [],
  92. tb: 0,
  93. hb: 0
  94. }
  95. ]
  96. const statList = ref<StatItem[]>(JSON.parse(JSON.stringify(defaultStats)))
  97. const dataLoading = ref(false)
  98. const list = ref<any[]>([])
  99. const loading = ref(false)
  100. const total = ref(0)
  101. const handleTimeChange = (type: TimeType, init = false) => {
  102. activeTimeType.value = type
  103. const formatStr = 'YYYY-MM-DD HH:mm:ss'
  104. const endTime = dayjs().endOf('day').format(formatStr)
  105. let startTime = ''
  106. switch (type) {
  107. case 'year':
  108. startTime = dayjs().startOf('year').format(formatStr)
  109. break
  110. case 'month':
  111. startTime = dayjs().startOf('month').format(formatStr)
  112. break
  113. case 'day':
  114. startTime = dayjs().startOf('day').format(formatStr)
  115. break
  116. }
  117. query.value.createTime = [startTime, endTime]
  118. console.log(`切换为[${type}]:`, query.value.createTime)
  119. if (!init) loadData()
  120. }
  121. const labelMap = {
  122. wxoareject: '审批不通过',
  123. wxoa: '审批中',
  124. wxclose: '关闭',
  125. wxfinished: '完成',
  126. wxtx: '待填写',
  127. xjtodo: '待执行',
  128. xjignore: '忽略',
  129. xjfinished: '已执行',
  130. yx0: '待填写',
  131. yx1: '已完成',
  132. yx2: '填写中',
  133. yx3: '忽略',
  134. by1: '未保养',
  135. by2: '已保养',
  136. rb0: '未完成',
  137. rb1: '已完成'
  138. }
  139. // 模拟数据加载
  140. const loadData = useDebounceFn(async function () {
  141. dataLoading.value = true
  142. const { pageNo, pageSize, type, ...other } = query.value
  143. const res = await IotReportApi.getOrderNumber({ ...other, timeType: activeTimeType.value })
  144. statList.value.forEach((item) => {
  145. const data = res[item.key] ?? {}
  146. item.total = data.total?.total ?? 0
  147. item.tb = data.total?.tb ?? 0
  148. item.hb = data.total?.hb ?? 0
  149. item.charts = data.status
  150. .filter((d) => d.num !== 0)
  151. .map((d) => ({
  152. label: labelMap[item.key + d.status],
  153. num: d.num
  154. }))
  155. })
  156. dataLoading.value = false
  157. }, 500)
  158. const loadList = useDebounceFn(async function () {
  159. loading.value = true
  160. const res = await IotReportApi.getOrderPage(query.value)
  161. // console.log('res :>> ', res)
  162. // const mockTableData = Array.from({ length: query.value.pageSize }).map((_, index) => {
  163. // const types = ['维修工单', '保养工单', '巡检工单', '运行记录', '生产日报']
  164. // const companies = ['第一工程公司', '第二工程公司', '总包单位']
  165. // const statuses = ['已完成', '未完成', '处理中']
  166. // return {
  167. // id: index + 1,
  168. // orderType: types[Math.floor(Math.random() * types.length)],
  169. // createTime: dayjs()
  170. // .subtract(Math.floor(Math.random() * 10), 'day')
  171. // .format('YYYY-MM-DD HH:mm:ss'),
  172. // companyName: companies[Math.floor(Math.random() * companies.length)],
  173. // projectDept: `项目部-${Math.floor(Math.random() * 10) + 1}`,
  174. // teamName: `作业队-${String.fromCharCode(65 + Math.floor(Math.random() * 5))}`,
  175. // status: statuses[Math.floor(Math.random() * statuses.length)],
  176. // deviceName: `设备-${Math.floor(Math.random() * 1000)}`
  177. // }
  178. // })
  179. list.value = res.list
  180. total.value = res.total
  181. loading.value = false
  182. }, 500)
  183. function handleSizeChange(val: number) {
  184. query.value.pageSize = val
  185. query.value.pageNo = 1
  186. loadList()
  187. }
  188. function handleCurrentChange(val: number) {
  189. query.value.pageNo = val
  190. loadList()
  191. }
  192. function handleQuery(setPage = true) {
  193. if (setPage) {
  194. query.value.pageNo = 1
  195. }
  196. loadList()
  197. loadData()
  198. }
  199. onMounted(() => {
  200. handleTimeChange('year', true)
  201. })
  202. watch(
  203. [() => query.value.createTime, () => query.value.deptId],
  204. () => {
  205. handleQuery()
  206. },
  207. { immediate: true }
  208. )
  209. function selectType(type: string | undefined) {
  210. query.value.type = type
  211. query.value.pageNo = 1
  212. loadList()
  213. }
  214. function handleReset() {
  215. handleTimeChange('year')
  216. selectType(undefined)
  217. }
  218. const handleClear = () => {
  219. handleTimeChange('year')
  220. }
  221. const handleChange = () => {
  222. activeTimeType.value = undefined
  223. }
  224. const message = useMessage()
  225. const exportLoading = ref(false)
  226. async function handleExport() {
  227. try {
  228. await message.exportConfirm()
  229. exportLoading.value = true
  230. const res = await IotReportApi.exportOrderPage(query.value)
  231. download.excel(res, '工单完成情况.xlsx')
  232. } finally {
  233. exportLoading.value = false
  234. }
  235. }
  236. </script>
  237. <template>
  238. <div
  239. class="grid grid-cols-[auto_1fr] grid-rows-[208px_1fr] gap-4 h-[calc(100vh-20px-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
  240. >
  241. <DeptTreeSelect
  242. :top-id="156"
  243. :deptId="156"
  244. v-model="query.deptId"
  245. :init-select="false"
  246. :show-title="false"
  247. />
  248. <!-- <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2"> </div> -->
  249. <div class="flex flex-col gap-4 h-full">
  250. <div
  251. class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 gap-4 flex-1"
  252. v-loading="dataLoading"
  253. >
  254. <section
  255. v-for="item in statList"
  256. :key="item.key"
  257. class="bg-white dark:bg-[#1d1e1f] rounded-xl shadow flex justify-between items-stretch min-h-[140px] border border-gray-200 dark:border-gray-700/50 relative overflow-hidden group transition-colors duration-300 transition-transform hover:scale-105 duration-500"
  258. :class="{ 'scale-105': item.type === query.type }"
  259. @click="selectType(item.type)"
  260. >
  261. <div class="flex flex-col justify-between z-10 p-4 pb-2 pr-0 gap-1">
  262. <div class="text-gray-500 dark:text-gray-400 text-sm font-medium">
  263. {{ item.title }}
  264. </div>
  265. <div class="text-3xl font-bold tracking-tight text-gray-900! dark:text-white! mt-2">
  266. <count-to :start-val="0" :end-val="item.total" :duration="100">
  267. <span class="text-xs leading-9 text-[var(--el-text-color-regular)]">
  268. 暂无数据
  269. </span>
  270. </count-to>
  271. </div>
  272. <div class="flex flex-col gap-2 w-24">
  273. <div v-show="item.tb !== 0" class="flex items-center gap-1">
  274. <div
  275. class="px-2 py-0.5 rounded text-xs font-bold flex items-center space-x-1"
  276. :class="
  277. item.tb >= 0
  278. ? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400'
  279. : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
  280. "
  281. >
  282. <div
  283. :class="
  284. item.tb >= 0
  285. ? 'i-material-symbols:trending-up'
  286. : 'i-material-symbols:trending-down'
  287. "
  288. class="text-sm"
  289. ></div>
  290. <span>{{ Math.abs(item.hb) }}%</span>
  291. </div>
  292. <span class="w-6 text-xs text-gray-500 dark:text-gray-400">同比</span>
  293. </div>
  294. <div v-show="item.hb !== 0" class="flex items-center gap-1">
  295. <div
  296. class="px-2 py-0.5 rounded text-xs font-bold flex items-center space-x-1"
  297. :class="
  298. item.hb >= 0
  299. ? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400'
  300. : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
  301. "
  302. >
  303. <div
  304. :class="
  305. item.hb >= 0
  306. ? 'i-material-symbols:trending-up'
  307. : 'i-material-symbols:trending-down'
  308. "
  309. class="text-sm"
  310. ></div>
  311. <span>{{ Math.abs(item.hb) }}%</span>
  312. </div>
  313. <span class="min-w-8 text-xs text-gray-500 dark:text-gray-400">环比</span>
  314. </div>
  315. </div>
  316. </div>
  317. <!-- 右侧:ECharts图表 -->
  318. <div class="flex-1 h-full z-10 relative">
  319. <MiniBarChart :items="item.charts" :max="item.total" />
  320. </div>
  321. <!-- 背景装饰:颜色自适应 -->
  322. <div
  323. class="absolute -right-6 -bottom-6 opacity-[0.05] pointer-events-none transition-transform group-hover:scale-150 duration-500 text-black dark:text-white"
  324. >
  325. <div :class="item.icon" class="text-9xl"></div>
  326. </div>
  327. </section>
  328. </div>
  329. <div class="flex justify-between">
  330. <div class="flex gap-4">
  331. <el-date-picker
  332. size="default"
  333. v-model="query.createTime"
  334. value-format="YYYY-MM-DD HH:mm:ss"
  335. type="daterange"
  336. start-placeholder="开始日期"
  337. end-placeholder="结束日期"
  338. :shortcuts="rangeShortcuts"
  339. class="!w-220px"
  340. @clear="handleClear"
  341. @change="handleChange"
  342. :clearable="false"
  343. :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
  344. />
  345. <el-button-group size="default">
  346. <el-button
  347. v-for="item in timeOptions"
  348. :key="item.value"
  349. :type="activeTimeType === item.value ? 'primary' : ''"
  350. @click="handleTimeChange(item.value)"
  351. >
  352. {{ item.label }}
  353. </el-button>
  354. </el-button-group>
  355. </div>
  356. <div class="flex gap-2">
  357. <el-button size="default" @click="handleReset">重置</el-button>
  358. <!-- @click="handleExport"
  359. :loading="exportLoading" -->
  360. <el-button
  361. @click="handleExport"
  362. :loading="exportLoading"
  363. size="default"
  364. plain
  365. type="success"
  366. >
  367. <Icon icon="ep:download" class="mr-5px" /> 导出
  368. </el-button>
  369. </div>
  370. </div>
  371. </div>
  372. <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
  373. <div class="flex-1 relative">
  374. <el-auto-resizer class="absolute">
  375. <template #default="{ width, height }">
  376. <el-table
  377. :data="list"
  378. v-loading="loading"
  379. stripe
  380. class="absolute"
  381. :max-height="height"
  382. :height="height"
  383. show-overflow-tooltip
  384. :width="width"
  385. scrollbar-always-on
  386. >
  387. <el-table-column label="序号" type="index" width="60" align="center" />
  388. <el-table-column label="工单类别" prop="type" align="center" width="80" />
  389. <el-table-column
  390. label="生成日期"
  391. prop="createTime"
  392. align="center"
  393. width="160"
  394. :formatter="(row) => row.createTime.split(' ')[0]"
  395. />
  396. <el-table-column label="公司" prop="company" align="center" width="100" />
  397. <el-table-column label="项目部" prop="project" align="center" />
  398. <el-table-column label="队伍" prop="deptName" align="center" />
  399. <el-table-column label="状态" prop="status" align="center" width="80">
  400. <!-- <template #default="{ row }">
  401. <el-tag v-if="row.status === '已完成'" type="success" effect="dark" size="small"
  402. >已完成</el-tag
  403. >
  404. <el-tag
  405. v-else-if="row.status === '未完成'"
  406. type="danger"
  407. effect="dark"
  408. size="small"
  409. >未完成</el-tag
  410. >
  411. <el-tag v-else type="warning" effect="plain" size="small">{{
  412. row.status
  413. }}</el-tag>
  414. </template> -->
  415. </el-table-column>
  416. <el-table-column label="设备" prop="device" align="center" />
  417. </el-table>
  418. </template>
  419. </el-auto-resizer>
  420. </div>
  421. <div class="h-10 mt-4 flex items-center justify-end">
  422. <el-pagination
  423. size="default"
  424. v-show="total > 0"
  425. v-model:current-page="query.pageNo"
  426. v-model:page-size="query.pageSize"
  427. :background="true"
  428. :page-sizes="[10, 20, 30, 50, 100]"
  429. :total="total"
  430. layout="total, sizes, prev, pager, next, jumper"
  431. @size-change="handleSizeChange"
  432. @current-change="handleCurrentChange"
  433. />
  434. </div>
  435. </div>
  436. </div>
  437. </template>
  438. <style scoped>
  439. :deep(.el-table) {
  440. border-top-right-radius: 8px;
  441. border-top-left-radius: 8px;
  442. .el-table__cell {
  443. height: 52px;
  444. }
  445. .el-table__header-wrapper {
  446. .el-table__cell {
  447. background: var(--el-fill-color-light);
  448. }
  449. }
  450. }
  451. </style>