work-order-completion.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  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. // 定义时间类型
  9. type TimeType = 'year' | 'month' | 'day'
  10. interface Query {
  11. deptId?: number
  12. createTime?: [string, string]
  13. pageNo: number
  14. pageSize: number
  15. }
  16. interface ChartDataItem {
  17. label: string
  18. num: number
  19. }
  20. interface StatItem {
  21. key: string
  22. title: string
  23. icon: string
  24. total: number
  25. charts: ChartDataItem[]
  26. trend: number
  27. }
  28. // 选项配置数组
  29. const timeOptions: { label: string; value: TimeType }[] = [
  30. { label: '年', value: 'year' },
  31. { label: '月', value: 'month' },
  32. { label: '日', value: 'day' }
  33. ]
  34. const activeTimeType = ref<TimeType>('year')
  35. const query = ref<Query>({
  36. pageNo: 1,
  37. pageSize: 10
  38. })
  39. const defaultStats: StatItem[] = [
  40. {
  41. key: 'yx',
  42. title: '运行记录',
  43. icon: 'i-material-symbols:list-alt-outline',
  44. total: 0,
  45. charts: [],
  46. trend: 0
  47. },
  48. {
  49. key: 'rb',
  50. title: '生产日报',
  51. icon: 'i-material-symbols:calendar-today-outline',
  52. total: 0,
  53. charts: [],
  54. trend: 0
  55. },
  56. {
  57. key: 'wx',
  58. title: '维修工单',
  59. icon: 'i-material-symbols:home-repair-service-outline',
  60. total: 0,
  61. charts: [],
  62. trend: 0
  63. },
  64. {
  65. key: 'by',
  66. title: '保养工单',
  67. icon: 'i-material-symbols:construction-rounded',
  68. total: 0,
  69. charts: [],
  70. trend: 0
  71. },
  72. {
  73. key: 'xj',
  74. title: '巡检工单',
  75. icon: 'i-material-symbols:warning-outline',
  76. total: 0,
  77. charts: [],
  78. trend: 0
  79. }
  80. ]
  81. const statList = ref<StatItem[]>(JSON.parse(JSON.stringify(defaultStats)))
  82. const dataLoading = ref(false)
  83. const list = ref<any[]>([])
  84. const loading = ref(false)
  85. const total = ref(0)
  86. const handleTimeChange = (type: TimeType, init = false) => {
  87. activeTimeType.value = type
  88. const formatStr = 'YYYY-MM-DD HH:mm:ss'
  89. const endTime = dayjs().endOf('day').format(formatStr)
  90. let startTime = ''
  91. switch (type) {
  92. case 'year':
  93. startTime = dayjs().startOf('year').format(formatStr)
  94. break
  95. case 'month':
  96. startTime = dayjs().startOf('month').format(formatStr)
  97. break
  98. case 'day':
  99. startTime = dayjs().startOf('day').format(formatStr)
  100. break
  101. }
  102. query.value.createTime = [startTime, endTime]
  103. console.log(`切换为[${type}]:`, query.value.createTime)
  104. if (!init) loadData()
  105. }
  106. const labelMap = {
  107. wxoareject: '审批不通过',
  108. wxoa: '审批中',
  109. wxclose: '关闭',
  110. wxfinished: '完成',
  111. wxtx: '待填写',
  112. xjtodo: '待执行',
  113. xjignore: '忽略',
  114. xjfinished: '已执行',
  115. yx0: '待执行',
  116. yx1: '已执行',
  117. yx2: '执行中',
  118. yx3: '填写中',
  119. by1: '未保养',
  120. by2: '已保养',
  121. rb0: '未完成',
  122. rb1: '已完成'
  123. }
  124. // 模拟数据加载
  125. const loadData = useDebounceFn(async function () {
  126. dataLoading.value = true
  127. const { pageNo, pageSize, ...other } = query.value
  128. const res = await IotReportApi.getOrderNumber(other)
  129. statList.value.forEach((item) => {
  130. const data = res[item.key] || []
  131. item.total = data.reduce((acc, cur) => acc + cur.num, 0)
  132. item.charts = data.map((d) => ({
  133. label: labelMap[item.key + d.status],
  134. num: d.num
  135. }))
  136. })
  137. dataLoading.value = false
  138. }, 500)
  139. const loadList = useDebounceFn(async function () {
  140. loading.value = true
  141. const res = await IotReportApi.getOrderPage(query.value)
  142. // console.log('res :>> ', res)
  143. // const mockTableData = Array.from({ length: query.value.pageSize }).map((_, index) => {
  144. // const types = ['维修工单', '保养工单', '巡检工单', '运行记录', '生产日报']
  145. // const companies = ['第一工程公司', '第二工程公司', '总包单位']
  146. // const statuses = ['已完成', '未完成', '处理中']
  147. // return {
  148. // id: index + 1,
  149. // orderType: types[Math.floor(Math.random() * types.length)],
  150. // createTime: dayjs()
  151. // .subtract(Math.floor(Math.random() * 10), 'day')
  152. // .format('YYYY-MM-DD HH:mm:ss'),
  153. // companyName: companies[Math.floor(Math.random() * companies.length)],
  154. // projectDept: `项目部-${Math.floor(Math.random() * 10) + 1}`,
  155. // teamName: `作业队-${String.fromCharCode(65 + Math.floor(Math.random() * 5))}`,
  156. // status: statuses[Math.floor(Math.random() * statuses.length)],
  157. // deviceName: `设备-${Math.floor(Math.random() * 1000)}`
  158. // }
  159. // })
  160. list.value = res.list
  161. total.value = res.total
  162. loading.value = false
  163. }, 500)
  164. function handleSizeChange(val: number) {
  165. query.value.pageSize = val
  166. query.value.pageNo = 1
  167. loadList()
  168. }
  169. function handleCurrentChange(val: number) {
  170. query.value.pageNo = val
  171. loadList()
  172. }
  173. function handleQuery(setPage = true) {
  174. if (setPage) {
  175. query.value.pageNo = 1
  176. }
  177. loadList()
  178. loadData()
  179. }
  180. onMounted(() => {
  181. handleTimeChange('year', true)
  182. })
  183. watch(
  184. [() => query.value.createTime, () => query.value.deptId],
  185. () => {
  186. handleQuery()
  187. },
  188. { immediate: true }
  189. )
  190. </script>
  191. <template>
  192. <div
  193. class="grid grid-cols-[15%_1fr] grid-rows-[196px_1fr] gap-4 h-[calc(100vh-var(--top-tool-height)-var(--tags-view-height)-var(--app-footer-height))]"
  194. >
  195. <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
  196. <DeptTreeSelect
  197. :top-id="156"
  198. :deptId="156"
  199. v-model="query.deptId"
  200. :init-select="false"
  201. :show-title="false"
  202. />
  203. </div>
  204. <div class="flex flex-col gap-4 h-full overflow-hidden">
  205. <div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 gap-4" v-loading="dataLoading">
  206. <section
  207. v-for="item in statList"
  208. :key="item.key"
  209. 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"
  210. >
  211. <!-- 左侧:文字信息 -->
  212. <div class="flex flex-col justify-between z-10 p-4 pr-0">
  213. <div>
  214. <!-- 标题:白底深灰,黑底浅灰 -->
  215. <div class="text-gray-500 dark:text-gray-400 text-sm font-medium mb-1">
  216. {{ item.title }}
  217. </div>
  218. <!-- 数值:白底黑色,黑底白色 -->
  219. <div class="text-3xl font-bold tracking-tight text-gray-900! dark:text-white! mt-1">
  220. <count-to :start-val="0" :end-val="item.total" :duration="100" />
  221. </div>
  222. </div>
  223. <!-- 环比 -->
  224. <div class="flex items-center gap-2 mt-2">
  225. <!-- 标签:针对亮色/暗色分别设置背景和文字颜色 -->
  226. <div
  227. class="px-2 py-0.5 rounded text-xs font-bold flex items-center space-x-1"
  228. :class="
  229. item.trend >= 0
  230. ? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400'
  231. : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
  232. "
  233. >
  234. <div
  235. :class="
  236. item.trend >= 0
  237. ? 'i-material-symbols:trending-up'
  238. : 'i-material-symbols:trending-down'
  239. "
  240. class="text-sm"
  241. ></div>
  242. <span>{{ Math.abs(item.trend) }}%</span>
  243. </div>
  244. <span class="text-xs text-gray-500 dark:text-gray-400">环比</span>
  245. </div>
  246. </div>
  247. <!-- 右侧:ECharts图表 -->
  248. <div class="flex-1 h-full z-10 relative">
  249. <MiniBarChart :items="item.charts" :max="item.total" />
  250. </div>
  251. <!-- 背景装饰:颜色自适应 -->
  252. <div
  253. 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"
  254. >
  255. <div :class="item.icon" class="text-9xl"></div>
  256. </div>
  257. </section>
  258. </div>
  259. <div class="flex justify-between">
  260. <el-button-group size="default">
  261. <el-button
  262. v-for="item in timeOptions"
  263. :key="item.value"
  264. :type="activeTimeType === item.value ? 'primary' : ''"
  265. @click="handleTimeChange(item.value)"
  266. >
  267. {{ item.label }}
  268. </el-button>
  269. </el-button-group>
  270. <el-button size="default" type="primary">导出</el-button>
  271. </div>
  272. </div>
  273. <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
  274. <div class="flex-1 relative">
  275. <el-auto-resizer class="absolute">
  276. <template #default="{ width, height }">
  277. <el-table
  278. :data="list"
  279. v-loading="loading"
  280. stripe
  281. class="absolute"
  282. :max-height="height"
  283. :height="height"
  284. show-overflow-tooltip
  285. :width="width"
  286. scrollbar-always-on
  287. >
  288. <el-table-column label="序号" type="index" width="60" align="center" />
  289. <el-table-column label="工单类别" prop="type" align="center" width="80" />
  290. <el-table-column
  291. label="生成日期"
  292. prop="createTime"
  293. align="center"
  294. width="160"
  295. :formatter="(row) => row.createTime.split(' ')[0]"
  296. />
  297. <el-table-column label="公司" prop="company" align="center" width="100" />
  298. <el-table-column label="项目部" prop="project" align="center" />
  299. <el-table-column label="队伍" prop="deptName" align="center" />
  300. <el-table-column label="状态" prop="status" align="center" width="80">
  301. <!-- <template #default="{ row }">
  302. <el-tag v-if="row.status === '已完成'" type="success" effect="dark" size="small"
  303. >已完成</el-tag
  304. >
  305. <el-tag
  306. v-else-if="row.status === '未完成'"
  307. type="danger"
  308. effect="dark"
  309. size="small"
  310. >未完成</el-tag
  311. >
  312. <el-tag v-else type="warning" effect="plain" size="small">{{
  313. row.status
  314. }}</el-tag>
  315. </template> -->
  316. </el-table-column>
  317. <el-table-column label="设备" prop="device" align="center" />
  318. </el-table>
  319. </template>
  320. </el-auto-resizer>
  321. </div>
  322. <div class="h-10 mt-4 flex items-center justify-end">
  323. <el-pagination
  324. size="default"
  325. v-show="total > 0"
  326. v-model:current-page="query.pageNo"
  327. v-model:page-size="query.pageSize"
  328. :background="true"
  329. :page-sizes="[10, 20, 30, 50, 100]"
  330. :total="total"
  331. layout="total, sizes, prev, pager, next, jumper"
  332. @size-change="handleSizeChange"
  333. @current-change="handleCurrentChange"
  334. />
  335. </div>
  336. </div>
  337. </div>
  338. </template>
  339. <style scoped>
  340. :deep(.el-table) {
  341. border-top-right-radius: 8px;
  342. border-top-left-radius: 8px;
  343. .el-table__cell {
  344. height: 52px;
  345. }
  346. .el-table__header-wrapper {
  347. .el-table__cell {
  348. background: var(--el-fill-color-light);
  349. }
  350. }
  351. }
  352. </style>