|
|
@@ -0,0 +1,365 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import { ref, onMounted } from 'vue'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import { useDebounceFn } from '@vueuse/core'
|
|
|
+import MiniBarChart from '@/components/WorkOrderCompletionBar/index.vue'
|
|
|
+import CountTo from '@/components/count-to1.vue'
|
|
|
+
|
|
|
+// 定义时间类型
|
|
|
+type TimeType = 'year' | 'month' | 'day'
|
|
|
+
|
|
|
+interface Query {
|
|
|
+ deptId?: number
|
|
|
+ createTime?: [string, string]
|
|
|
+ pageNo: number
|
|
|
+ pageSize: number
|
|
|
+}
|
|
|
+
|
|
|
+interface StatItem {
|
|
|
+ key: string
|
|
|
+ title: string
|
|
|
+ icon: string
|
|
|
+ total: number
|
|
|
+ completed: number
|
|
|
+ incomplete: number
|
|
|
+ trend: number
|
|
|
+}
|
|
|
+
|
|
|
+// 选项配置数组
|
|
|
+const timeOptions: { label: string; value: TimeType }[] = [
|
|
|
+ { label: '年', value: 'year' },
|
|
|
+ { label: '月', value: 'month' },
|
|
|
+ { label: '日', value: 'day' }
|
|
|
+]
|
|
|
+
|
|
|
+const activeTimeType = ref<TimeType>('year')
|
|
|
+const query = ref<Query>({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10
|
|
|
+})
|
|
|
+
|
|
|
+const defaultStats: StatItem[] = [
|
|
|
+ {
|
|
|
+ key: 'run',
|
|
|
+ title: '运行记录',
|
|
|
+ icon: 'i-material-symbols:list-alt-outline',
|
|
|
+ total: 0,
|
|
|
+ completed: 0,
|
|
|
+ incomplete: 0,
|
|
|
+ trend: 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'prod',
|
|
|
+ title: '生产日报',
|
|
|
+ icon: 'i-material-symbols:calendar-today-outline',
|
|
|
+ total: 0,
|
|
|
+ completed: 0,
|
|
|
+ incomplete: 0,
|
|
|
+ trend: 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'repair',
|
|
|
+ title: '维修工单',
|
|
|
+ icon: 'i-material-symbols:home-repair-service-outline',
|
|
|
+ total: 0,
|
|
|
+ completed: 0,
|
|
|
+ incomplete: 0,
|
|
|
+ trend: 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'maintain',
|
|
|
+ title: '保养工单',
|
|
|
+ icon: 'i-material-symbols:construction-rounded',
|
|
|
+ total: 0,
|
|
|
+ completed: 0,
|
|
|
+ incomplete: 0,
|
|
|
+ trend: 0
|
|
|
+ },
|
|
|
+ {
|
|
|
+ key: 'inspect',
|
|
|
+ title: '巡检工单',
|
|
|
+ icon: 'i-material-symbols:warning-outline',
|
|
|
+ total: 0,
|
|
|
+ completed: 0,
|
|
|
+ incomplete: 0,
|
|
|
+ trend: 0
|
|
|
+ }
|
|
|
+]
|
|
|
+
|
|
|
+const statList = ref<StatItem[]>(JSON.parse(JSON.stringify(defaultStats)))
|
|
|
+const dataLoading = ref(false)
|
|
|
+const list = ref<any[]>([])
|
|
|
+const loading = ref(false)
|
|
|
+const total = ref(0)
|
|
|
+
|
|
|
+const handleTimeChange = (type: TimeType, init = false) => {
|
|
|
+ activeTimeType.value = type
|
|
|
+
|
|
|
+ const formatStr = 'YYYY-MM-DD HH:mm:ss'
|
|
|
+ const endTime = dayjs().endOf('day').format(formatStr)
|
|
|
+ let startTime = ''
|
|
|
+
|
|
|
+ switch (type) {
|
|
|
+ case 'year':
|
|
|
+ startTime = dayjs().startOf('year').format(formatStr)
|
|
|
+ break
|
|
|
+ case 'month':
|
|
|
+ startTime = dayjs().startOf('month').format(formatStr)
|
|
|
+ break
|
|
|
+ case 'day':
|
|
|
+ startTime = dayjs().startOf('day').format(formatStr)
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ query.value.createTime = [startTime, endTime]
|
|
|
+
|
|
|
+ console.log(`切换为[${type}]:`, query.value.createTime)
|
|
|
+
|
|
|
+ if (!init) loadData()
|
|
|
+}
|
|
|
+
|
|
|
+// 模拟数据加载
|
|
|
+const loadData = useDebounceFn(async function () {
|
|
|
+ dataLoading.value = true
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 600)) // 模拟延迟
|
|
|
+
|
|
|
+ statList.value.forEach((item) => {
|
|
|
+ const randomTotal = Math.floor(Math.random() * 500) + 50
|
|
|
+ const randomCompleted = Math.floor(randomTotal * (0.6 + Math.random() * 0.3))
|
|
|
+ const randomIncomplete = randomTotal - randomCompleted
|
|
|
+ const randomTrend = Number((Math.random() * 20 - 10).toFixed(1))
|
|
|
+
|
|
|
+ item.total = randomTotal
|
|
|
+ item.completed = randomCompleted
|
|
|
+ item.incomplete = randomIncomplete
|
|
|
+ item.trend = randomTrend
|
|
|
+ })
|
|
|
+ dataLoading.value = false
|
|
|
+}, 500)
|
|
|
+
|
|
|
+const loadList = useDebounceFn(async function () {
|
|
|
+ loading.value = true
|
|
|
+ await new Promise((resolve) => setTimeout(resolve, 500))
|
|
|
+
|
|
|
+ const mockTableData = Array.from({ length: query.value.pageSize }).map((_, index) => {
|
|
|
+ const types = ['维修工单', '保养工单', '巡检工单', '运行记录', '生产日报']
|
|
|
+ const companies = ['第一工程公司', '第二工程公司', '总包单位']
|
|
|
+ const statuses = ['已完成', '未完成', '处理中']
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: index + 1,
|
|
|
+ orderType: types[Math.floor(Math.random() * types.length)],
|
|
|
+ createTime: dayjs()
|
|
|
+ .subtract(Math.floor(Math.random() * 10), 'day')
|
|
|
+ .format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ companyName: companies[Math.floor(Math.random() * companies.length)],
|
|
|
+ projectDept: `项目部-${Math.floor(Math.random() * 10) + 1}`,
|
|
|
+ teamName: `作业队-${String.fromCharCode(65 + Math.floor(Math.random() * 5))}`,
|
|
|
+ status: statuses[Math.floor(Math.random() * statuses.length)],
|
|
|
+ deviceName: `设备-${Math.floor(Math.random() * 1000)}`
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ list.value = mockTableData
|
|
|
+ total.value = 85
|
|
|
+ loading.value = false
|
|
|
+}, 500)
|
|
|
+
|
|
|
+function handleSizeChange(val: number) {
|
|
|
+ query.value.pageSize = val
|
|
|
+ query.value.pageNo = 1
|
|
|
+ loadList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleCurrentChange(val: number) {
|
|
|
+ query.value.pageNo = val
|
|
|
+ loadList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleQuery(setPage = true) {
|
|
|
+ if (setPage) {
|
|
|
+ query.value.pageNo = 1
|
|
|
+ }
|
|
|
+ loadList()
|
|
|
+ loadData()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ handleTimeChange('year', true)
|
|
|
+})
|
|
|
+
|
|
|
+watch(
|
|
|
+ [() => query.value.createTime, () => query.value.deptId],
|
|
|
+ () => {
|
|
|
+ handleQuery()
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div
|
|
|
+ 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))]"
|
|
|
+ >
|
|
|
+ <div class="p-4 bg-white dark:bg-[#1d1e1f] shadow rounded-lg row-span-2">
|
|
|
+ <DeptTreeSelect
|
|
|
+ :top-id="156"
|
|
|
+ :deptId="156"
|
|
|
+ v-model="query.deptId"
|
|
|
+ :init-select="false"
|
|
|
+ :show-title="false"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="flex flex-col gap-4 h-full overflow-hidden">
|
|
|
+ <div class="grid grid-cols-1 md:grid-cols-3 xl:grid-cols-5 gap-4" v-loading="dataLoading">
|
|
|
+ <section
|
|
|
+ v-for="item in statList"
|
|
|
+ :key="item.key"
|
|
|
+ class="bg-white dark:bg-[#1d1e1f] rounded-xl shadow p-4 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"
|
|
|
+ >
|
|
|
+ <!-- 左侧:文字信息 -->
|
|
|
+ <div class="flex flex-col justify-between z-10 w-[55%]">
|
|
|
+ <div>
|
|
|
+ <!-- 标题:白底深灰,黑底浅灰 -->
|
|
|
+ <div class="text-gray-500 dark:text-gray-400 text-sm font-medium mb-1">
|
|
|
+ {{ item.title }}
|
|
|
+ </div>
|
|
|
+ <!-- 数值:白底黑色,黑底白色 -->
|
|
|
+ <div class="text-3xl font-bold tracking-tight text-gray-900! dark:text-white! mt-1">
|
|
|
+ <count-to :start-val="0" :end-val="item.total" :duration="2000" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 环比 -->
|
|
|
+ <div class="flex items-center gap-2 mt-2">
|
|
|
+ <!-- 标签:针对亮色/暗色分别设置背景和文字颜色 -->
|
|
|
+ <div
|
|
|
+ class="px-2 py-0.5 rounded text-xs font-bold flex items-center space-x-1"
|
|
|
+ :class="
|
|
|
+ item.trend >= 0
|
|
|
+ ? 'bg-emerald-100 text-emerald-600 dark:bg-emerald-500/20 dark:text-emerald-400'
|
|
|
+ : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
|
|
|
+ "
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ :class="
|
|
|
+ item.trend >= 0
|
|
|
+ ? 'i-material-symbols:trending-up'
|
|
|
+ : 'i-material-symbols:trending-down'
|
|
|
+ "
|
|
|
+ class="text-sm"
|
|
|
+ ></div>
|
|
|
+ <span>{{ Math.abs(item.trend) }}%</span>
|
|
|
+ </div>
|
|
|
+ <span class="text-xs text-gray-500 dark:text-gray-400">环比</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧:ECharts图表 -->
|
|
|
+ <div class="w-[45%] h-full z-10 relative">
|
|
|
+ <MiniBarChart
|
|
|
+ :completed="item.completed"
|
|
|
+ :incomplete="item.incomplete"
|
|
|
+ :max="item.total"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 背景装饰:颜色自适应 -->
|
|
|
+ <div
|
|
|
+ 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"
|
|
|
+ >
|
|
|
+ <div :class="item.icon" class="text-9xl"></div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between">
|
|
|
+ <el-button-group size="default">
|
|
|
+ <el-button
|
|
|
+ v-for="item in timeOptions"
|
|
|
+ :key="item.value"
|
|
|
+ :type="activeTimeType === item.value ? 'primary' : ''"
|
|
|
+ @click="handleTimeChange(item.value)"
|
|
|
+ >
|
|
|
+ {{ item.label }}
|
|
|
+ </el-button>
|
|
|
+ </el-button-group>
|
|
|
+ <el-button size="default" type="primary">导出</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg p-4 flex flex-col">
|
|
|
+ <div class="flex-1 relative">
|
|
|
+ <el-auto-resizer class="absolute">
|
|
|
+ <template #default="{ width, height }">
|
|
|
+ <el-table
|
|
|
+ :data="list"
|
|
|
+ v-loading="loading"
|
|
|
+ stripe
|
|
|
+ class="absolute"
|
|
|
+ :max-height="height"
|
|
|
+ :height="height"
|
|
|
+ show-overflow-tooltip
|
|
|
+ :width="width"
|
|
|
+ scrollbar-always-on
|
|
|
+ >
|
|
|
+ <el-table-column label="序号" type="index" width="60" align="center" />
|
|
|
+ <el-table-column label="工单类别" prop="orderType" align="center" min-width="110" />
|
|
|
+ <el-table-column label="生成日期" prop="createTime" align="center" min-width="160" />
|
|
|
+ <el-table-column label="公司" prop="companyName" align="center" min-width="140" />
|
|
|
+ <el-table-column label="项目部" prop="projectDept" align="center" min-width="120" />
|
|
|
+ <el-table-column label="队伍" prop="teamName" align="center" min-width="100" />
|
|
|
+ <el-table-column label="状态" prop="status" align="center" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag v-if="row.status === '已完成'" type="success" effect="dark" size="small"
|
|
|
+ >已完成</el-tag
|
|
|
+ >
|
|
|
+ <el-tag
|
|
|
+ v-else-if="row.status === '未完成'"
|
|
|
+ type="danger"
|
|
|
+ effect="dark"
|
|
|
+ size="small"
|
|
|
+ >未完成</el-tag
|
|
|
+ >
|
|
|
+ <el-tag v-else type="warning" effect="plain" size="small">{{
|
|
|
+ row.status
|
|
|
+ }}</el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="设备" prop="deviceName" align="center" min-width="120" />
|
|
|
+ </el-table>
|
|
|
+ </template>
|
|
|
+ </el-auto-resizer>
|
|
|
+ </div>
|
|
|
+ <div class="h-10 mt-4 flex items-center justify-end">
|
|
|
+ <el-pagination
|
|
|
+ size="default"
|
|
|
+ v-show="total > 0"
|
|
|
+ v-model:current-page="query.pageNo"
|
|
|
+ v-model:page-size="query.pageSize"
|
|
|
+ :background="true"
|
|
|
+ :page-sizes="[10, 20, 30, 50, 100]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+:deep(.el-table) {
|
|
|
+ border-top-right-radius: 8px;
|
|
|
+ border-top-left-radius: 8px;
|
|
|
+
|
|
|
+ .el-table__cell {
|
|
|
+ height: 52px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-table__header-wrapper {
|
|
|
+ .el-table__cell {
|
|
|
+ background: var(--el-fill-color-light);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|