|
@@ -1,163 +1,164 @@
|
|
|
<template>
|
|
<template>
|
|
|
- <div class="statistics-page">
|
|
|
|
|
- <!-- 第一行:统计卡片行 -->
|
|
|
|
|
- <el-row :gutter="16" class="section-row">
|
|
|
|
|
- <el-col :span="24">
|
|
|
|
|
- <el-card class="chart-card filter-card" shadow="never">
|
|
|
|
|
- <el-form
|
|
|
|
|
- class="filter-form"
|
|
|
|
|
- :model="queryParams"
|
|
|
|
|
- ref="queryFormRef"
|
|
|
|
|
- :inline="true"
|
|
|
|
|
- label-width="68px"
|
|
|
|
|
- >
|
|
|
|
|
- <el-form-item label="所属部门" prop="project_name">
|
|
|
|
|
- <el-tree-select
|
|
|
|
|
- v-model="queryParams.deptId"
|
|
|
|
|
- :data="deptList"
|
|
|
|
|
- :props="defaultProps"
|
|
|
|
|
- check-strictly
|
|
|
|
|
- node-key="id"
|
|
|
|
|
- filterable
|
|
|
|
|
- placeholder="请选择所在部门"
|
|
|
|
|
- clearable
|
|
|
|
|
- style="width: 180px"
|
|
|
|
|
- />
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- <el-form-item label="创建时间" prop="createTime">
|
|
|
|
|
- <el-date-picker
|
|
|
|
|
- v-model="queryParams.createTime"
|
|
|
|
|
- value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
- type="daterange"
|
|
|
|
|
- start-placeholder="开始日期"
|
|
|
|
|
- end-placeholder="结束日期"
|
|
|
|
|
- :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
|
|
|
|
- class="!w-220px"
|
|
|
|
|
- />
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
-<!-- <el-form-item label="填写状态" prop="project_name">
|
|
|
|
|
- <el-select
|
|
|
|
|
- v-model="queryParams.status"
|
|
|
|
|
- placeholder="填写状态"
|
|
|
|
|
- clearable
|
|
|
|
|
- class="!w-240px"
|
|
|
|
|
- >
|
|
|
|
|
- <el-option
|
|
|
|
|
- v-for="dict in getIntDictOptions(DICT_TYPE.OPERATION_FILL_ORDER_STATUS)"
|
|
|
|
|
- :key="dict.value"
|
|
|
|
|
- :label="dict.label"
|
|
|
|
|
- :value="dict.value"
|
|
|
|
|
- />
|
|
|
|
|
- </el-select>
|
|
|
|
|
- </el-form-item>-->
|
|
|
|
|
- <el-form-item class="filter-actions">
|
|
|
|
|
- <el-button class="action-btn" @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
|
|
|
|
- <el-button class="action-btn" @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
|
|
|
|
- </el-form-item>
|
|
|
|
|
- </el-form>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
- <!-- 第二行:图表行 -->
|
|
|
|
|
- <el-row :gutter="16" class="section-row">
|
|
|
|
|
- <el-col :span="12">
|
|
|
|
|
- <el-card class="chart-card" shadow="never">
|
|
|
|
|
- <template #header>
|
|
|
|
|
- <div class="card-header">
|
|
|
|
|
- <span class="card-title">运行记录工单统计</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <el-row class="gauge-row">
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,4,queryParams.createTime)">
|
|
|
|
|
- <div ref="reportingChartRef" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>总数</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,1,queryParams.createTime)">
|
|
|
|
|
- <div ref="dealFinishedChartRef" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>已填写</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,0,queryParams.createTime)">
|
|
|
|
|
- <div ref="transOrderChartRef" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>未填写</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,2,queryParams.createTime)">
|
|
|
|
|
- <div ref="writeChartRef" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>填写中</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openFill(queryParams.deptId,3,queryParams.createTime)">
|
|
|
|
|
- <div ref="ignoreChartRef" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>忽略</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="12">
|
|
|
|
|
- <el-card class="chart-card" shadow="never">
|
|
|
|
|
- <template #header>
|
|
|
|
|
- <div class="card-header">
|
|
|
|
|
- <span class="card-title">运行记录设备统计</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <el-row class="gauge-row">
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openForm(queryParams.deptId,2,queryParams.createTime)">
|
|
|
|
|
- <div ref="reportingChartRef1" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>总数</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openForm(queryParams.deptId,1,queryParams.createTime)">
|
|
|
|
|
- <div ref="dealFinishedChartRef1" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>已填写</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openForm(queryParams.deptId,0,queryParams.createTime)">
|
|
|
|
|
- <div ref="transOrderChartRef1" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>未填写</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- <el-col :span="4" class="gauge-item" @click="openForm(queryParams.deptId,3,queryParams.createTime)">
|
|
|
|
|
- <div ref="ignoreChartRef1" class="h-[160px] w-full"></div>
|
|
|
|
|
- <div class="gauge-label">
|
|
|
|
|
- <span>忽略</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
- <!-- 第三行:消息统计行 -->
|
|
|
|
|
- <el-row class="section-row">
|
|
|
|
|
- <el-col :span="24">
|
|
|
|
|
- <el-card class="chart-card" shadow="never">
|
|
|
|
|
- <template #header>
|
|
|
|
|
- <div class="card-header">
|
|
|
|
|
- <span class="card-title">运行记录工单统计</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- </template>
|
|
|
|
|
- <div ref="chartContainer" class="trend-chart"></div>
|
|
|
|
|
- </el-card>
|
|
|
|
|
- </el-col>
|
|
|
|
|
- </el-row>
|
|
|
|
|
|
|
+ <div ref="wrapperRef" class="statistics-scale-wrapper bg absolute top-0 left-0 size-full z-10">
|
|
|
|
|
+ <div class="mx-a overflow-hidden" :style="targetWrapperStyle">
|
|
|
|
|
+ <div class="statistics-page bg" :style="targetAreaStyle">
|
|
|
|
|
+ <header class="header">运行记录填报统计</header>
|
|
|
|
|
+
|
|
|
|
|
+ <main class="statistics-content">
|
|
|
|
|
+ <el-row :gutter="16" class="section-row">
|
|
|
|
|
+ <el-col :span="24">
|
|
|
|
|
+ <el-card class="chart-card filter-card panel" shadow="never">
|
|
|
|
|
+ <div class="panel-title panel-title--md">
|
|
|
|
|
+ <div class="icon-decorator">
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 筛选条件
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-form
|
|
|
|
|
+ class="filter-form"
|
|
|
|
|
+ :model="queryParams"
|
|
|
|
|
+ ref="queryFormRef"
|
|
|
|
|
+ :inline="true"
|
|
|
|
|
+ label-width="86px">
|
|
|
|
|
+ <el-form-item label="所属部门" prop="project_name">
|
|
|
|
|
+ <el-tree-select
|
|
|
|
|
+ v-model="queryParams.deptId"
|
|
|
|
|
+ :data="deptList"
|
|
|
|
|
+ :props="defaultProps"
|
|
|
|
|
+ check-strictly
|
|
|
|
|
+ node-key="id"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ placeholder="请选择所在部门"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ class="filter-control" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="创建时间" prop="createTime">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="queryParams.createTime"
|
|
|
|
|
+ value-format="YYYY-MM-DD HH:mm:ss"
|
|
|
|
|
+ type="daterange"
|
|
|
|
|
+ :shortcuts="rangeShortcuts"
|
|
|
|
|
+ start-placeholder="开始日期"
|
|
|
|
|
+ end-placeholder="结束日期"
|
|
|
|
|
+ :default-time="datePickerDefaultTime"
|
|
|
|
|
+ class="filter-control filter-control--date" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item class="filter-actions">
|
|
|
|
|
+ <el-button class="action-btn" size="default" @click="handleQuery">
|
|
|
|
|
+ <Icon icon="ep:search" class="mr-5px" /> 搜索
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button class="action-btn" size="default" @click="resetQuery">
|
|
|
|
|
+ <Icon icon="ep:refresh" class="mr-5px" /> 重置
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="16" class="section-row">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-card class="chart-card panel" shadow="never">
|
|
|
|
|
+ <div class="panel-title">
|
|
|
|
|
+ <div class="icon-decorator">
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 运行记录工单统计
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="metric-grid metric-grid--order">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="item in orderMetricCards"
|
|
|
|
|
+ :key="item.key"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="metric-card"
|
|
|
|
|
+ :style="{
|
|
|
|
|
+ '--metric-accent': item.accent,
|
|
|
|
|
+ '--metric-glow': item.glow,
|
|
|
|
|
+ '--metric-progress': `${item.percent}%`
|
|
|
|
|
+ }"
|
|
|
|
|
+ @click="openFill(queryParams.deptId, item.status, queryParams.createTime)">
|
|
|
|
|
+ <span class="metric-card__label">{{ item.label }}</span>
|
|
|
|
|
+ <strong class="metric-card__value">
|
|
|
|
|
+ <count-to
|
|
|
|
|
+ class="metric-card__count"
|
|
|
|
|
+ :start-val="0"
|
|
|
|
|
+ :end-val="item.value"
|
|
|
|
|
+ :duration="800"
|
|
|
|
|
+ separator="" />
|
|
|
|
|
+ </strong>
|
|
|
|
|
+ <span class="metric-card__hint">{{ item.hint }}</span>
|
|
|
|
|
+ <span class="metric-card__progress">
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-card class="chart-card panel" shadow="never">
|
|
|
|
|
+ <div class="panel-title">
|
|
|
|
|
+ <div class="icon-decorator">
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 运行记录设备统计
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div class="metric-grid metric-grid--device">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="item in deviceMetricCards"
|
|
|
|
|
+ :key="item.key"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="metric-card"
|
|
|
|
|
+ :style="{
|
|
|
|
|
+ '--metric-accent': item.accent,
|
|
|
|
|
+ '--metric-glow': item.glow,
|
|
|
|
|
+ '--metric-progress': `${item.percent}%`
|
|
|
|
|
+ }"
|
|
|
|
|
+ @click="openForm(queryParams.deptId, item.status, queryParams.createTime)">
|
|
|
|
|
+ <span class="metric-card__label">{{ item.label }}</span>
|
|
|
|
|
+ <strong class="metric-card__value">
|
|
|
|
|
+ <count-to
|
|
|
|
|
+ class="metric-card__count"
|
|
|
|
|
+ :start-val="0"
|
|
|
|
|
+ :end-val="item.value"
|
|
|
|
|
+ :duration="800"
|
|
|
|
|
+ separator="" />
|
|
|
|
|
+ </strong>
|
|
|
|
|
+ <span class="metric-card__hint">{{ item.hint }}</span>
|
|
|
|
|
+ <span class="metric-card__progress">
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row class="section-row trend-row">
|
|
|
|
|
+ <el-col :span="24">
|
|
|
|
|
+ <el-card class="chart-card trend-card panel" shadow="never">
|
|
|
|
|
+ <div class="panel-title">
|
|
|
|
|
+ <div class="icon-decorator">
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ <span></span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ 部门运行记录工单统计
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div ref="chartContainer" class="trend-chart"></div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ </main>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts" name="Index">
|
|
<script setup lang="ts" name="Index">
|
|
|
import * as echarts from 'echarts/core'
|
|
import * as echarts from 'echarts/core'
|
|
|
-import { BarChart } from 'echarts/charts'; // 显式导入柱状图模块
|
|
|
|
|
|
|
+import { BarChart } from 'echarts/charts' // 显式导入柱状图模块
|
|
|
|
|
|
|
|
import {
|
|
import {
|
|
|
GridComponent,
|
|
GridComponent,
|
|
@@ -166,24 +167,79 @@ import {
|
|
|
ToolboxComponent,
|
|
ToolboxComponent,
|
|
|
TooltipComponent
|
|
TooltipComponent
|
|
|
} from 'echarts/components'
|
|
} from 'echarts/components'
|
|
|
-import { GaugeChart, LineChart, PieChart } from 'echarts/charts'
|
|
|
|
|
|
|
+import { LineChart, PieChart } from 'echarts/charts'
|
|
|
import { LabelLayout, UniversalTransition } from 'echarts/features'
|
|
import { LabelLayout, UniversalTransition } from 'echarts/features'
|
|
|
import { CanvasRenderer } from 'echarts/renderers'
|
|
import { CanvasRenderer } from 'echarts/renderers'
|
|
|
-import {
|
|
|
|
|
- IotStatisticsDeviceMessageSummaryRespVO,
|
|
|
|
|
- IotStatisticsSummaryRespVO
|
|
|
|
|
-} from '@/api/iot/statistics'
|
|
|
|
|
-import { formatDate } from '@/utils/formatTime'
|
|
|
|
|
import { IotStatApi } from '@/api/pms/stat'
|
|
import { IotStatApi } from '@/api/pms/stat'
|
|
|
-import { ref, reactive, onMounted, onUnmounted } from "vue";
|
|
|
|
|
-import { defaultProps, handleTree } from "@/utils/tree";
|
|
|
|
|
-import * as DeptApi from "@/api/system/dept";
|
|
|
|
|
-import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
|
|
|
|
-import {useUserStore} from "@/store/modules/user";
|
|
|
|
|
-// 导入部门数据类型
|
|
|
|
|
-import { DeptTreeItem } from '@/api/system/dept'
|
|
|
|
|
-import StatisticsForm from './StatisticsForm.vue'
|
|
|
|
|
|
|
+import CountTo from '@/components/count-to1.vue'
|
|
|
|
|
+import { computed, ref, reactive, onMounted, onUnmounted } from 'vue'
|
|
|
|
|
+import { DESIGN_HEIGHT, DESIGN_WIDTH } from '@/utils/kb'
|
|
|
|
|
+import { defaultProps, handleTree } from '@/utils/tree'
|
|
|
|
|
+import * as DeptApi from '@/api/system/dept'
|
|
|
|
|
+import type { DeptVO } from '@/api/system/dept'
|
|
|
|
|
+import { useUserStore } from '@/store/modules/user'
|
|
|
|
|
+import { rangeShortcuts } from '@/utils/formatTime'
|
|
|
|
|
+import dayjs from 'dayjs'
|
|
|
const { push } = useRouter()
|
|
const { push } = useRouter()
|
|
|
|
|
+
|
|
|
|
|
+const CHART_COLORS = {
|
|
|
|
|
+ blue: '#1d5fbf',
|
|
|
|
|
+ blueLight: '#8ec5ff',
|
|
|
|
|
+ orange: '#d96b1f',
|
|
|
|
|
+ green: '#13936f',
|
|
|
|
|
+ red: '#d94b4b',
|
|
|
|
|
+ purple: '#5f56d8',
|
|
|
|
|
+ split: 'rgba(31, 91, 184, 0.1)',
|
|
|
|
|
+ axis: '#24364f'
|
|
|
|
|
+}
|
|
|
|
|
+const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
|
|
|
|
|
+const datePickerDefaultTime: [Date, Date] = [
|
|
|
|
|
+ dayjs().startOf('day').toDate(),
|
|
|
|
|
+ dayjs().endOf('day').toDate()
|
|
|
|
|
+]
|
|
|
|
|
+const wrapperRef = ref<HTMLDivElement>()
|
|
|
|
|
+const scale = ref(1)
|
|
|
|
|
+
|
|
|
|
|
+let resizeObserver: ResizeObserver | null = null
|
|
|
|
|
+let resizeRaf = 0
|
|
|
|
|
+
|
|
|
|
|
+const targetWrapperStyle = computed(() => ({
|
|
|
|
|
+ width: `${DESIGN_WIDTH * scale.value}px`,
|
|
|
|
|
+ height: `${DESIGN_HEIGHT * scale.value}px`
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+const targetAreaStyle = computed(() => ({
|
|
|
|
|
+ '--kb-scale': scale.value,
|
|
|
|
|
+ width: `${DESIGN_WIDTH * scale.value}px`,
|
|
|
|
|
+ height: `${DESIGN_HEIGHT * scale.value}px`
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
|
|
+function updateScale() {
|
|
|
|
|
+ cancelAnimationFrame(resizeRaf)
|
|
|
|
|
+
|
|
|
|
|
+ resizeRaf = requestAnimationFrame(() => {
|
|
|
|
|
+ const wrapper = wrapperRef.value
|
|
|
|
|
+ if (!wrapper) return
|
|
|
|
|
+
|
|
|
|
|
+ const { clientWidth, clientHeight } = wrapper
|
|
|
|
|
+ if (!clientWidth || !clientHeight) return
|
|
|
|
|
+
|
|
|
|
|
+ scale.value = Math.min(clientWidth / DESIGN_WIDTH, clientHeight / DESIGN_HEIGHT)
|
|
|
|
|
+ nextTick(handleResize)
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getDefaultTimeRange = () => {
|
|
|
|
|
+ const end = dayjs()
|
|
|
|
|
+ const start = end.subtract(7, 'day')
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ startTime: start.valueOf(),
|
|
|
|
|
+ endTime: end.valueOf(),
|
|
|
|
|
+ createTime: [start.format(DATE_TIME_FORMAT), end.format(DATE_TIME_FORMAT)]
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 初始化echarts
|
|
// 初始化echarts
|
|
|
echarts.use([
|
|
echarts.use([
|
|
|
TooltipComponent,
|
|
TooltipComponent,
|
|
@@ -196,33 +252,39 @@ echarts.use([
|
|
|
GridComponent,
|
|
GridComponent,
|
|
|
LineChart,
|
|
LineChart,
|
|
|
UniversalTransition,
|
|
UniversalTransition,
|
|
|
- GaugeChart,
|
|
|
|
|
BarChart
|
|
BarChart
|
|
|
])
|
|
])
|
|
|
|
|
|
|
|
const deptList = ref<DeptTreeItem[]>([]) // 树形结构部门列表
|
|
const deptList = ref<DeptTreeItem[]>([]) // 树形结构部门列表
|
|
|
const deptDataList = ref<DeptDataItem[]>([]) // 部门数据列表
|
|
const deptDataList = ref<DeptDataItem[]>([]) // 部门数据列表
|
|
|
|
|
|
|
|
|
|
+type QueryParams = {
|
|
|
|
|
+ startTime: number
|
|
|
|
|
+ endTime: number
|
|
|
|
|
+ createTime: string[] | null
|
|
|
|
|
+ deptId: number | null
|
|
|
|
|
+ status: number | null
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type DeptTreeItem = DeptVO & {
|
|
|
|
|
+ children?: DeptTreeItem[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 定义部门数据类型
|
|
// 定义部门数据类型
|
|
|
interface DeptDataItem {
|
|
interface DeptDataItem {
|
|
|
- deptId: number;
|
|
|
|
|
- name: string;
|
|
|
|
|
- totalCount: number;
|
|
|
|
|
- filledCount: number;
|
|
|
|
|
- unfilledCount: number;
|
|
|
|
|
- fillingCount: number;
|
|
|
|
|
|
|
+ deptId: number
|
|
|
|
|
+ name: string
|
|
|
|
|
+ totalCount: number
|
|
|
|
|
+ filledCount: number
|
|
|
|
|
+ unfilledCount: number
|
|
|
|
|
+ fillingCount: number
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** IoT 首页 */
|
|
/** IoT 首页 */
|
|
|
-defineOptions({ name: 'iotOpeationSta' })
|
|
|
|
|
-
|
|
|
|
|
-const timeRange = ref('7d') // 修改默认选择为近一周
|
|
|
|
|
-const dateRange = ref<[Date, Date] | null>(null)
|
|
|
|
|
|
|
+defineOptions({ name: 'IotOpeationSta' })
|
|
|
|
|
|
|
|
-const queryParams = reactive({
|
|
|
|
|
- startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
|
|
|
|
|
- endTime: Date.now(), // 设置默认结束时间为当前时间
|
|
|
|
|
- createTime: [],
|
|
|
|
|
|
|
+const queryParams = reactive<QueryParams>({
|
|
|
|
|
+ ...getDefaultTimeRange(),
|
|
|
deptId: null, // 选中的部门ID
|
|
deptId: null, // 选中的部门ID
|
|
|
status: null // 填写状态
|
|
status: null // 填写状态
|
|
|
})
|
|
})
|
|
@@ -234,16 +296,23 @@ const handleQuery = () => {
|
|
|
initChart()
|
|
initChart()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const formRef = ref()
|
|
|
|
|
-const openForm = (deptId?:string,isFill?:string,createTime?:[]) => {
|
|
|
|
|
- push({ name: 'FillOrderInfo2',params:{deptId,isFill,createTime}})
|
|
|
|
|
|
|
+const initialQueryParams: QueryParams = { ...queryParams }
|
|
|
|
|
+
|
|
|
|
|
+const openForm = (deptId?: number | null, isFill?: number, createTime?: string[] | null) => {
|
|
|
|
|
+ const params: Record<string, string | string[]> = {}
|
|
|
|
|
+ if (deptId != null) params.deptId = String(deptId)
|
|
|
|
|
+ if (isFill != null) params.isFill = String(isFill)
|
|
|
|
|
+ if (createTime?.length) params.createTime = createTime
|
|
|
|
|
+
|
|
|
|
|
+ push({ name: 'FillOrderInfo2', params })
|
|
|
}
|
|
}
|
|
|
-const openFill = (deptId?:string,orderStatus?:string,createTime?:[]) => {
|
|
|
|
|
|
|
+const openFill = (deptId?: number | null, orderStatus?: number, createTime?: string[] | null) => {
|
|
|
push({
|
|
push({
|
|
|
name: 'IotOpeationFill1',
|
|
name: 'IotOpeationFill1',
|
|
|
- query: { // 使用query传递参数,而非params
|
|
|
|
|
- deptId,
|
|
|
|
|
- orderStatus,
|
|
|
|
|
|
|
+ query: {
|
|
|
|
|
+ // 使用query传递参数,而非params
|
|
|
|
|
+ deptId: deptId == null ? undefined : String(deptId),
|
|
|
|
|
+ orderStatus: orderStatus == null ? undefined : String(orderStatus),
|
|
|
// 数组可以直接传递,会自动序列化
|
|
// 数组可以直接传递,会自动序列化
|
|
|
createTime: createTime ? createTime.join(',') : undefined
|
|
createTime: createTime ? createTime.join(',') : undefined
|
|
|
// 建议将数组转为字符串,避免路由参数格式问题
|
|
// 建议将数组转为字符串,避免路由参数格式问题
|
|
@@ -251,65 +320,21 @@ const openFill = (deptId?:string,orderStatus?:string,createTime?:[]) => {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-
|
|
|
|
|
-
|
|
|
|
|
const resetQuery = () => {
|
|
const resetQuery = () => {
|
|
|
|
|
+ const defaultTimeRange = getDefaultTimeRange()
|
|
|
|
|
+
|
|
|
// 重置查询参数
|
|
// 重置查询参数
|
|
|
- queryParams.startTime = Date.now() - 7 * 24 * 60 * 60 * 1000
|
|
|
|
|
- queryParams.endTime = Date.now()
|
|
|
|
|
- queryParams.deptId = useUserStore().getUser.deptId;
|
|
|
|
|
- queryParams.createTime = null
|
|
|
|
|
|
|
+ queryParams.startTime = defaultTimeRange.startTime
|
|
|
|
|
+ queryParams.endTime = defaultTimeRange.endTime
|
|
|
|
|
+ queryParams.deptId = useUserStore().getUser.deptId ?? null
|
|
|
|
|
+ queryParams.createTime = defaultTimeRange.createTime
|
|
|
|
|
|
|
|
// 重新获取数据
|
|
// 重新获取数据
|
|
|
getStats()
|
|
getStats()
|
|
|
|
|
+ getDevStats()
|
|
|
initChart()
|
|
initChart()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-//运行记录工单统计
|
|
|
|
|
-const reportingChartRef = ref() // 在线设备统计的图表
|
|
|
|
|
-const dealFinishedChartRef = ref() // 离线设备统计的图表
|
|
|
|
|
-const transOrderChartRef = ref() // 待激活设备统计的图表
|
|
|
|
|
-const writeChartRef = ref() // 待填写图表
|
|
|
|
|
-const ignoreChartRef = ref() // 上下行消息量统计的图表
|
|
|
|
|
-//运行记录设备统计
|
|
|
|
|
-const reportingChartRef1 = ref() // 在线设备统计的图表
|
|
|
|
|
-const dealFinishedChartRef1 = ref() // 离线设备统计的图表
|
|
|
|
|
-const transOrderChartRef1 = ref() // 待激活设备统计的图表
|
|
|
|
|
-const ignoreChartRef1 = ref() // 上下行消息量统计的图表
|
|
|
|
|
-
|
|
|
|
|
-// 基础统计数据
|
|
|
|
|
-const statsData = ref<IotStatisticsSummaryRespVO>({
|
|
|
|
|
- productCategoryCount: 0,
|
|
|
|
|
- productCount: 0,
|
|
|
|
|
- deviceCount: 0,
|
|
|
|
|
- deviceMessageCount: 0,
|
|
|
|
|
- productCategoryTodayCount: 0,
|
|
|
|
|
- productTodayCount: 0,
|
|
|
|
|
- deviceTodayCount: 0,
|
|
|
|
|
- deviceMessageTodayCount: 0,
|
|
|
|
|
- deviceOnlineCount: 0,
|
|
|
|
|
- deviceOfflineCount: 0,
|
|
|
|
|
- deviceInactiveCount: 0,
|
|
|
|
|
- productCategoryDeviceCounts: {}
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-const day = ref({
|
|
|
|
|
- failureDay: undefined,
|
|
|
|
|
- maintainDay: undefined
|
|
|
|
|
-})
|
|
|
|
|
-const week = ref({
|
|
|
|
|
- failureWeek: undefined,
|
|
|
|
|
- maintainWeek: undefined
|
|
|
|
|
-})
|
|
|
|
|
-const month = ref({
|
|
|
|
|
- failureMonth: undefined,
|
|
|
|
|
- maintainMonth: undefined
|
|
|
|
|
-})
|
|
|
|
|
-const total = ref({
|
|
|
|
|
- failureTotal: undefined,
|
|
|
|
|
- maintainTotal: undefined
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
const status = ref<IotStatusItem[]>([])
|
|
const status = ref<IotStatusItem[]>([])
|
|
|
const status1 = ref<IotStatusItem[]>([])
|
|
const status1 = ref<IotStatusItem[]>([])
|
|
|
// 定义状态项接口
|
|
// 定义状态项接口
|
|
@@ -318,200 +343,177 @@ interface IotStatusItem {
|
|
|
filledCount: number
|
|
filledCount: number
|
|
|
unfilledCount: number
|
|
unfilledCount: number
|
|
|
fillingCount: number
|
|
fillingCount: number
|
|
|
|
|
+ ignoreCount?: number
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// 消息统计数据
|
|
|
|
|
-const messageStats = ref<IotStatisticsDeviceMessageSummaryRespVO>({
|
|
|
|
|
- upstreamCounts: {},
|
|
|
|
|
- downstreamCounts: {}
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-/** 处理快捷时间范围选择 */
|
|
|
|
|
-const handleTimeRangeChange = (timeRange: string) => {
|
|
|
|
|
- const now = Date.now()
|
|
|
|
|
- let startTime: number
|
|
|
|
|
-
|
|
|
|
|
- switch (timeRange) {
|
|
|
|
|
- case '1h':
|
|
|
|
|
- startTime = now - 60 * 60 * 1000
|
|
|
|
|
- break
|
|
|
|
|
- case '24h':
|
|
|
|
|
- startTime = now - 24 * 60 * 60 * 1000
|
|
|
|
|
- break
|
|
|
|
|
- case '7d':
|
|
|
|
|
- startTime = now - 7 * 24 * 60 * 60 * 1000
|
|
|
|
|
- break
|
|
|
|
|
- default:
|
|
|
|
|
- return
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // 清空日期选择器
|
|
|
|
|
- dateRange.value = null
|
|
|
|
|
-
|
|
|
|
|
- // 更新查询参数
|
|
|
|
|
- queryParams.startTime = startTime
|
|
|
|
|
- queryParams.endTime = now
|
|
|
|
|
|
|
+const toFiniteNumber = (value: unknown) => {
|
|
|
|
|
+ const numberValue = Number(value)
|
|
|
|
|
+ return Number.isFinite(numberValue) ? numberValue : 0
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- // 重新获取数据
|
|
|
|
|
- getStats()
|
|
|
|
|
- initChart()
|
|
|
|
|
|
|
+const getMetricPercent = (value: number, total: number) => {
|
|
|
|
|
+ if (!total) return value > 0 ? 100 : 0
|
|
|
|
|
+ return Math.min(100, Math.round((value / total) * 100))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 处理自定义日期范围选择 */
|
|
|
|
|
-const handleDateRangeChange = (value: [Date, Date] | null) => {
|
|
|
|
|
- if (value) {
|
|
|
|
|
- // 清空快捷选项
|
|
|
|
|
- timeRange.value = ''
|
|
|
|
|
|
|
+const createMetricCard = (
|
|
|
|
|
+ key: string,
|
|
|
|
|
+ label: string,
|
|
|
|
|
+ value: number,
|
|
|
|
|
+ total: number,
|
|
|
|
|
+ status: number,
|
|
|
|
|
+ accent: string,
|
|
|
|
|
+ glow: string
|
|
|
|
|
+) => ({
|
|
|
|
|
+ key,
|
|
|
|
|
+ label,
|
|
|
|
|
+ value,
|
|
|
|
|
+ status,
|
|
|
|
|
+ accent,
|
|
|
|
|
+ glow,
|
|
|
|
|
+ percent: key === 'total' ? 100 : getMetricPercent(value, total),
|
|
|
|
|
+ hint: key === 'total' ? '全部记录' : `占比 ${getMetricPercent(value, total)}%`
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
- // 更新查询参数
|
|
|
|
|
- queryParams.startTime = value[0].getTime()
|
|
|
|
|
- queryParams.endTime = value[1].getTime()
|
|
|
|
|
|
|
+const orderMetricCards = computed(() => {
|
|
|
|
|
+ const firstStatus = status.value[0] || {}
|
|
|
|
|
+ const totalCount = toFiniteNumber(firstStatus.totalCount)
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'total',
|
|
|
|
|
+ '总数',
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 4,
|
|
|
|
|
+ CHART_COLORS.blue,
|
|
|
|
|
+ 'rgba(29, 95, 191, 0.16)'
|
|
|
|
|
+ ),
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'filled',
|
|
|
|
|
+ '已填写',
|
|
|
|
|
+ toFiniteNumber(firstStatus.filledCount),
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 1,
|
|
|
|
|
+ CHART_COLORS.green,
|
|
|
|
|
+ 'rgba(19, 147, 111, 0.16)'
|
|
|
|
|
+ ),
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'unfilled',
|
|
|
|
|
+ '未填写',
|
|
|
|
|
+ toFiniteNumber(firstStatus.unfilledCount),
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 0,
|
|
|
|
|
+ CHART_COLORS.orange,
|
|
|
|
|
+ 'rgba(217, 107, 31, 0.16)'
|
|
|
|
|
+ ),
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'filling',
|
|
|
|
|
+ '填写中',
|
|
|
|
|
+ toFiniteNumber(firstStatus.fillingCount),
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 2,
|
|
|
|
|
+ CHART_COLORS.red,
|
|
|
|
|
+ 'rgba(217, 75, 75, 0.15)'
|
|
|
|
|
+ ),
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'ignore',
|
|
|
|
|
+ '忽略',
|
|
|
|
|
+ toFiniteNumber(firstStatus.ignoreCount),
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 3,
|
|
|
|
|
+ CHART_COLORS.purple,
|
|
|
|
|
+ 'rgba(95, 86, 216, 0.16)'
|
|
|
|
|
+ )
|
|
|
|
|
+ ]
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
- // 重新获取数据
|
|
|
|
|
- getStats()
|
|
|
|
|
- initChart()
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const deviceMetricCards = computed(() => {
|
|
|
|
|
+ const firstStatus = status1.value[0] || {}
|
|
|
|
|
+ const totalCount = toFiniteNumber(firstStatus.totalCount)
|
|
|
|
|
+
|
|
|
|
|
+ return [
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'total',
|
|
|
|
|
+ '总数',
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 2,
|
|
|
|
|
+ CHART_COLORS.blue,
|
|
|
|
|
+ 'rgba(29, 95, 191, 0.16)'
|
|
|
|
|
+ ),
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'filled',
|
|
|
|
|
+ '已填写',
|
|
|
|
|
+ toFiniteNumber(firstStatus.filledCount),
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 1,
|
|
|
|
|
+ CHART_COLORS.green,
|
|
|
|
|
+ 'rgba(19, 147, 111, 0.16)'
|
|
|
|
|
+ ),
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'unfilled',
|
|
|
|
|
+ '未填写',
|
|
|
|
|
+ toFiniteNumber(firstStatus.unfilledCount),
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 0,
|
|
|
|
|
+ CHART_COLORS.orange,
|
|
|
|
|
+ 'rgba(217, 107, 31, 0.16)'
|
|
|
|
|
+ ),
|
|
|
|
|
+ createMetricCard(
|
|
|
|
|
+ 'ignore',
|
|
|
|
|
+ '忽略',
|
|
|
|
|
+ toFiniteNumber(firstStatus.ignoreCount),
|
|
|
|
|
+ totalCount,
|
|
|
|
|
+ 3,
|
|
|
|
|
+ CHART_COLORS.purple,
|
|
|
|
|
+ 'rgba(95, 86, 216, 0.16)'
|
|
|
|
|
+ )
|
|
|
|
|
+ ]
|
|
|
|
|
+})
|
|
|
|
|
|
|
|
/** 获取统计数据 */
|
|
/** 获取统计数据 */
|
|
|
const getStats = async () => {
|
|
const getStats = async () => {
|
|
|
// 获取基础统计数据
|
|
// 获取基础统计数据
|
|
|
// 获取部门统计数据
|
|
// 获取部门统计数据
|
|
|
IotStatApi.getDeptStatistics(queryParams).then((res) => {
|
|
IotStatApi.getDeptStatistics(queryParams).then((res) => {
|
|
|
- deptDataList.value = res.deptCountList || [];
|
|
|
|
|
- status.value = res.totalList || [];
|
|
|
|
|
|
|
+ deptDataList.value = res.deptCountList || []
|
|
|
|
|
+ status.value = res.totalList || []
|
|
|
initChart()
|
|
initChart()
|
|
|
- initCharts()
|
|
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
const getDevStats = async () => {
|
|
const getDevStats = async () => {
|
|
|
// 获取基础统计数据
|
|
// 获取基础统计数据
|
|
|
// 获取部门统计数据
|
|
// 获取部门统计数据
|
|
|
IotStatApi.getDevSta(queryParams).then((res) => {
|
|
IotStatApi.getDevSta(queryParams).then((res) => {
|
|
|
- status1.value = res.totalList || [];
|
|
|
|
|
|
|
+ status1.value = res.totalList || []
|
|
|
initChart()
|
|
initChart()
|
|
|
- initCharts()
|
|
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-/** 初始化图表 */
|
|
|
|
|
-const initCharts = () => {
|
|
|
|
|
- // 使用数组的第一个元素(如果存在)
|
|
|
|
|
- const firstStatus = status.value[0] || {}
|
|
|
|
|
- const firstStatus1 = status1.value[0] || {}
|
|
|
|
|
- // 上报中
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- reportingChartRef.value,
|
|
|
|
|
- firstStatus.totalCount === undefined ? 0 : firstStatus.totalCount,
|
|
|
|
|
- '#0d9'
|
|
|
|
|
- )
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- reportingChartRef1.value,
|
|
|
|
|
- firstStatus1.totalCount === undefined ? 0 : firstStatus1.totalCount,
|
|
|
|
|
- '#0d9'
|
|
|
|
|
- )
|
|
|
|
|
- // 处理完成
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- dealFinishedChartRef.value,
|
|
|
|
|
- firstStatus.filledCount === undefined ? 0 : firstStatus.filledCount,
|
|
|
|
|
- '#05b'
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- ignoreChartRef.value,
|
|
|
|
|
- firstStatus.ignoreCount === undefined ? 0 : firstStatus.ignoreCount,
|
|
|
|
|
- 'purple'
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- ignoreChartRef1.value,
|
|
|
|
|
- firstStatus1.ignoreCount === undefined ? 0 : firstStatus1.ignoreCount,
|
|
|
|
|
- 'purple'
|
|
|
|
|
- )
|
|
|
|
|
-
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- dealFinishedChartRef1.value,
|
|
|
|
|
- firstStatus1.filledCount === undefined ? 0 : firstStatus1.filledCount,
|
|
|
|
|
- '#05b'
|
|
|
|
|
- )
|
|
|
|
|
- // 转工单
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- transOrderChartRef.value,
|
|
|
|
|
- firstStatus.unfilledCount === undefined ? 0 : firstStatus.unfilledCount,
|
|
|
|
|
- '#f50'
|
|
|
|
|
- )
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- transOrderChartRef1.value,
|
|
|
|
|
- firstStatus1.unfilledCount === undefined ? 0 : firstStatus1.unfilledCount,
|
|
|
|
|
- '#f50'
|
|
|
|
|
- )
|
|
|
|
|
- // 待填写
|
|
|
|
|
- initGaugeChart(
|
|
|
|
|
- writeChartRef.value,
|
|
|
|
|
- firstStatus.fillingCount === undefined ? 0 : firstStatus.fillingCount,
|
|
|
|
|
- '#05b'
|
|
|
|
|
- )
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-/** 初始化仪表盘图表 */
|
|
|
|
|
-const initGaugeChart = (el: any, value: number, color: string) => {
|
|
|
|
|
- echarts.init(el).setOption({
|
|
|
|
|
- series: [
|
|
|
|
|
- {
|
|
|
|
|
- type: 'gauge',
|
|
|
|
|
- startAngle: 360,
|
|
|
|
|
- endAngle: 0,
|
|
|
|
|
- min: 0,
|
|
|
|
|
- max: statsData.value.deviceCount || 100, // 使用设备总数作为最大值
|
|
|
|
|
- progress: {
|
|
|
|
|
- show: true,
|
|
|
|
|
- width: 12,
|
|
|
|
|
- itemStyle: {
|
|
|
|
|
- color: color
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- axisLine: {
|
|
|
|
|
- lineStyle: {
|
|
|
|
|
- width: 12,
|
|
|
|
|
- color: [[1, '#E5E7EB']]
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- axisTick: { show: false },
|
|
|
|
|
- splitLine: { show: false },
|
|
|
|
|
- axisLabel: { show: false },
|
|
|
|
|
- pointer: { show: false },
|
|
|
|
|
- anchor: { show: false },
|
|
|
|
|
- title: { show: false },
|
|
|
|
|
- detail: {
|
|
|
|
|
- valueAnimation: true,
|
|
|
|
|
- fontSize: 24,
|
|
|
|
|
- fontWeight: 'bold',
|
|
|
|
|
- fontFamily: 'Inter, sans-serif',
|
|
|
|
|
- color: color,
|
|
|
|
|
- offsetCenter: [0, '0'],
|
|
|
|
|
- formatter: (value: number) => {
|
|
|
|
|
- return `${value} `
|
|
|
|
|
- }
|
|
|
|
|
- },
|
|
|
|
|
- data: [{ value: value }]
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
- })
|
|
|
|
|
|
|
+const chartContainer = ref<HTMLDivElement | null>(null)
|
|
|
|
|
+let chartInstance: ReturnType<typeof echarts.init> | null = null
|
|
|
|
|
+
|
|
|
|
|
+const toVisibleBarData = (data: number[]) => data.map((value) => (value > 0 ? value : null))
|
|
|
|
|
+const barValueLabel = {
|
|
|
|
|
+ show: true,
|
|
|
|
|
+ position: 'top',
|
|
|
|
|
+ color: CHART_COLORS.axis,
|
|
|
|
|
+ fontSize: 11,
|
|
|
|
|
+ fontWeight: 700,
|
|
|
|
|
+ formatter: ({ value }) => (Number(value) > 0 ? `${value}` : '')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const chartContainer = ref(null)
|
|
|
|
|
-let chartInstance = null
|
|
|
|
|
-
|
|
|
|
|
// 初始化部门统计图表
|
|
// 初始化部门统计图表
|
|
|
const initChart = () => {
|
|
const initChart = () => {
|
|
|
if (!chartContainer.value) return
|
|
if (!chartContainer.value) return
|
|
|
|
|
|
|
|
// 准备数据
|
|
// 准备数据
|
|
|
- const deptNames = deptDataList.value.map(item => item.name)
|
|
|
|
|
- const totalCounts = deptDataList.value.map(item => item.totalCount)
|
|
|
|
|
- const filledCounts = deptDataList.value.map(item => item.filledCount)
|
|
|
|
|
- const unfilledCounts = deptDataList.value.map(item => item.unfilledCount)
|
|
|
|
|
- const fillingCounts = deptDataList.value.map(item => item.fillingCount)
|
|
|
|
|
|
|
+ const deptNames = deptDataList.value.map((item) => item.name)
|
|
|
|
|
+ const totalCounts = deptDataList.value.map((item) => item.totalCount)
|
|
|
|
|
+ const filledCounts = deptDataList.value.map((item) => item.filledCount)
|
|
|
|
|
+ const unfilledCounts = deptDataList.value.map((item) => item.unfilledCount)
|
|
|
|
|
+ const fillingCounts = deptDataList.value.map((item) => item.fillingCount)
|
|
|
|
|
|
|
|
// ECharts配置
|
|
// ECharts配置
|
|
|
const option = {
|
|
const option = {
|
|
@@ -520,9 +522,20 @@ const initChart = () => {
|
|
|
axisPointer: {
|
|
axisPointer: {
|
|
|
type: 'shadow'
|
|
type: 'shadow'
|
|
|
},
|
|
},
|
|
|
|
|
+ backgroundColor: 'rgba(255,255,255,0.96)',
|
|
|
|
|
+ borderColor: 'rgba(255,255,255,0.95)',
|
|
|
|
|
+ borderWidth: 1,
|
|
|
|
|
+ textStyle: {
|
|
|
|
|
+ color: '#3c3c3c'
|
|
|
|
|
+ },
|
|
|
|
|
+ extraCssText:
|
|
|
|
|
+ 'border-radius: 8px; box-shadow: 0 8px 20px rgba(31, 91, 184, 0.16); backdrop-filter: blur(12px);',
|
|
|
formatter: (params) => {
|
|
formatter: (params) => {
|
|
|
- let result = `<div class="font-bold">${params[0].axisValue}</div>`
|
|
|
|
|
- params.forEach(param => {
|
|
|
|
|
|
|
+ const visibleParams = params.filter((param) => Number(param.value) > 0)
|
|
|
|
|
+ if (!visibleParams.length) return ''
|
|
|
|
|
+
|
|
|
|
|
+ let result = `<div class="font-bold">${visibleParams[0].axisValue}</div>`
|
|
|
|
|
+ visibleParams.forEach((param) => {
|
|
|
result += `<div>${param.marker} ${param.seriesName}: ${param.value}</div>`
|
|
result += `<div>${param.marker} ${param.seriesName}: ${param.value}</div>`
|
|
|
})
|
|
})
|
|
|
return result
|
|
return result
|
|
@@ -530,247 +543,486 @@ const initChart = () => {
|
|
|
},
|
|
},
|
|
|
legend: {
|
|
legend: {
|
|
|
data: ['总数', '已填写', '未填写', '填写中'],
|
|
data: ['总数', '已填写', '未填写', '填写中'],
|
|
|
- top: 25
|
|
|
|
|
|
|
+ top: 8,
|
|
|
|
|
+ textStyle: {
|
|
|
|
|
+ color: CHART_COLORS.axis,
|
|
|
|
|
+ fontSize: 12
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
grid: {
|
|
grid: {
|
|
|
- left: '3%',
|
|
|
|
|
- right: '4%',
|
|
|
|
|
- bottom: '3%',
|
|
|
|
|
|
|
+ left: 24,
|
|
|
|
|
+ right: 24,
|
|
|
|
|
+ top: 48,
|
|
|
|
|
+ bottom: 16,
|
|
|
containLabel: true
|
|
containLabel: true
|
|
|
},
|
|
},
|
|
|
xAxis: {
|
|
xAxis: {
|
|
|
type: 'category',
|
|
type: 'category',
|
|
|
data: deptNames,
|
|
data: deptNames,
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
- rotate: 45,
|
|
|
|
|
- margin: 15,
|
|
|
|
|
- fontSize: 10
|
|
|
|
|
|
|
+ rotate: 0,
|
|
|
|
|
+ interval: 0,
|
|
|
|
|
+ hideOverlap: true,
|
|
|
|
|
+ overflow: 'truncate',
|
|
|
|
|
+ width: 120,
|
|
|
|
|
+ margin: 12,
|
|
|
|
|
+ color: CHART_COLORS.axis,
|
|
|
|
|
+ fontSize: 11
|
|
|
|
|
+ },
|
|
|
|
|
+ axisLine: {
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ color: CHART_COLORS.split
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ axisTick: {
|
|
|
|
|
+ show: false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
yAxis: {
|
|
yAxis: {
|
|
|
type: 'value',
|
|
type: 'value',
|
|
|
axisLabel: {
|
|
axisLabel: {
|
|
|
|
|
+ color: CHART_COLORS.axis,
|
|
|
formatter: (value) => Math.floor(value).toString()
|
|
formatter: (value) => Math.floor(value).toString()
|
|
|
|
|
+ },
|
|
|
|
|
+ splitLine: {
|
|
|
|
|
+ lineStyle: {
|
|
|
|
|
+ color: CHART_COLORS.split
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ axisLine: {
|
|
|
|
|
+ show: false
|
|
|
|
|
+ },
|
|
|
|
|
+ axisTick: {
|
|
|
|
|
+ show: false
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
series: [
|
|
series: [
|
|
|
{
|
|
{
|
|
|
name: '总数',
|
|
name: '总数',
|
|
|
type: 'bar',
|
|
type: 'bar',
|
|
|
- barGap: 0,
|
|
|
|
|
|
|
+ barGap: '8%',
|
|
|
|
|
+ barCategoryGap: '28%',
|
|
|
|
|
+ barWidth: 72,
|
|
|
|
|
+ barMinHeight: 8,
|
|
|
|
|
+ label: barValueLabel,
|
|
|
itemStyle: {
|
|
itemStyle: {
|
|
|
|
|
+ borderRadius: [8, 8, 0, 0],
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
- { offset: 0, color: '#188df0' },
|
|
|
|
|
- { offset: 1, color: '#188df0' }
|
|
|
|
|
|
|
+ { offset: 0, color: CHART_COLORS.blueLight },
|
|
|
|
|
+ { offset: 1, color: CHART_COLORS.blue }
|
|
|
])
|
|
])
|
|
|
},
|
|
},
|
|
|
emphasis: {
|
|
emphasis: {
|
|
|
focus: 'series'
|
|
focus: 'series'
|
|
|
},
|
|
},
|
|
|
- data: totalCounts
|
|
|
|
|
|
|
+ data: toVisibleBarData(totalCounts)
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
name: '已填写',
|
|
name: '已填写',
|
|
|
type: 'bar',
|
|
type: 'bar',
|
|
|
|
|
+ barWidth: 72,
|
|
|
|
|
+ barMinHeight: 8,
|
|
|
|
|
+ label: barValueLabel,
|
|
|
itemStyle: {
|
|
itemStyle: {
|
|
|
|
|
+ borderRadius: [8, 8, 0, 0],
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
- { offset: 0, color: '#d3a137' },
|
|
|
|
|
- { offset: 1, color: '#d3a137' }
|
|
|
|
|
|
|
+ { offset: 0, color: '#a8e0c8' },
|
|
|
|
|
+ { offset: 1, color: CHART_COLORS.green }
|
|
|
])
|
|
])
|
|
|
},
|
|
},
|
|
|
emphasis: {
|
|
emphasis: {
|
|
|
focus: 'series'
|
|
focus: 'series'
|
|
|
},
|
|
},
|
|
|
- data: filledCounts
|
|
|
|
|
|
|
+ data: toVisibleBarData(filledCounts)
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
name: '未填写',
|
|
name: '未填写',
|
|
|
type: 'bar',
|
|
type: 'bar',
|
|
|
|
|
+ barWidth: 72,
|
|
|
|
|
+ barMinHeight: 8,
|
|
|
|
|
+ label: barValueLabel,
|
|
|
itemStyle: {
|
|
itemStyle: {
|
|
|
|
|
+ borderRadius: [8, 8, 0, 0],
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
- { offset: 0, color: 'green' },
|
|
|
|
|
- { offset: 1, color: 'green' }
|
|
|
|
|
|
|
+ { offset: 0, color: '#ffd8a8' },
|
|
|
|
|
+ { offset: 1, color: CHART_COLORS.orange }
|
|
|
])
|
|
])
|
|
|
},
|
|
},
|
|
|
emphasis: {
|
|
emphasis: {
|
|
|
focus: 'series'
|
|
focus: 'series'
|
|
|
},
|
|
},
|
|
|
- data: unfilledCounts
|
|
|
|
|
|
|
+ data: toVisibleBarData(unfilledCounts)
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
name: '填写中',
|
|
name: '填写中',
|
|
|
type: 'bar',
|
|
type: 'bar',
|
|
|
|
|
+ barWidth: 72,
|
|
|
|
|
+ barMinHeight: 8,
|
|
|
|
|
+ label: barValueLabel,
|
|
|
itemStyle: {
|
|
itemStyle: {
|
|
|
|
|
+ borderRadius: [8, 8, 0, 0],
|
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
- { offset: 0, color: 'red' },
|
|
|
|
|
- { offset: 1, color: 'red' }
|
|
|
|
|
|
|
+ { offset: 0, color: '#fdd5d5' },
|
|
|
|
|
+ { offset: 1, color: CHART_COLORS.red }
|
|
|
])
|
|
])
|
|
|
},
|
|
},
|
|
|
emphasis: {
|
|
emphasis: {
|
|
|
focus: 'series'
|
|
focus: 'series'
|
|
|
},
|
|
},
|
|
|
- data: fillingCounts
|
|
|
|
|
|
|
+ data: toVisibleBarData(fillingCounts)
|
|
|
}
|
|
}
|
|
|
]
|
|
]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 初始化图表
|
|
// 初始化图表
|
|
|
|
|
+ chartInstance?.dispose()
|
|
|
chartInstance = echarts.init(chartContainer.value)
|
|
chartInstance = echarts.init(chartContainer.value)
|
|
|
chartInstance.setOption(option)
|
|
chartInstance.setOption(option)
|
|
|
|
|
|
|
|
- // 窗口缩放监听
|
|
|
|
|
- window.addEventListener('resize', handleResize)
|
|
|
|
|
handleResize()
|
|
handleResize()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 自适应调整
|
|
// 自适应调整
|
|
|
const handleResize = () => {
|
|
const handleResize = () => {
|
|
|
- chartInstance?.resize()
|
|
|
|
|
-}
|
|
|
|
|
-const router = useRouter()
|
|
|
|
|
-const route = useRoute()
|
|
|
|
|
-// 监听路由变化
|
|
|
|
|
-const removeBeforeEach = router.beforeEach((to, from, next) => {
|
|
|
|
|
- // 如果是从FillOrderInfo1页面返回
|
|
|
|
|
- if (from.name === 'FillOrderInfo1' && to.name === route.name) {
|
|
|
|
|
- // 恢复查询参数
|
|
|
|
|
- Object.assign(queryParams, initialQueryParams)
|
|
|
|
|
- // 阻止刷新,使用replace而不是push
|
|
|
|
|
- next({ ...to, replace: true })
|
|
|
|
|
- } else {
|
|
|
|
|
- next()
|
|
|
|
|
|
|
+ const instance = chartInstance
|
|
|
|
|
+ if (instance) {
|
|
|
|
|
+ instance.resize()
|
|
|
}
|
|
}
|
|
|
-})
|
|
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
/** 初始化 */
|
|
/** 初始化 */
|
|
|
onMounted(async () => {
|
|
onMounted(async () => {
|
|
|
- queryParams.deptId = useUserStore().getUser.deptId;
|
|
|
|
|
-
|
|
|
|
|
- // 计算近一周时间
|
|
|
|
|
- const end = new Date();
|
|
|
|
|
- const start = new Date();
|
|
|
|
|
- start.setTime(start.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
|
-
|
|
|
|
|
- // 格式化日期为后端需要的格式
|
|
|
|
|
- const formatDate = (date) => {
|
|
|
|
|
- const year = date.getFullYear();
|
|
|
|
|
- const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
|
|
|
- const day = String(date.getDate()).padStart(2, '0');
|
|
|
|
|
- const hours = String(date.getHours()).padStart(2, '0');
|
|
|
|
|
- const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
|
|
|
- const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
|
|
|
- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- queryParams.createTime = [formatDate(start), formatDate(end)];
|
|
|
|
|
|
|
+ queryParams.deptId = useUserStore().getUser.deptId ?? null
|
|
|
|
|
+
|
|
|
|
|
+ Object.assign(initialQueryParams, queryParams)
|
|
|
|
|
+ nextTick(updateScale)
|
|
|
|
|
+ resizeObserver = new ResizeObserver(updateScale)
|
|
|
|
|
+ if (wrapperRef.value) {
|
|
|
|
|
+ resizeObserver.observe(wrapperRef.value)
|
|
|
|
|
+ }
|
|
|
|
|
+ window.addEventListener('resize', updateScale)
|
|
|
|
|
|
|
|
getStats()
|
|
getStats()
|
|
|
- getDevStats();
|
|
|
|
|
|
|
+ getDevStats()
|
|
|
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
|
|
deptList.value = handleTree(await DeptApi.getSimpleDeptList())
|
|
|
-
|
|
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+onUnmounted(() => {
|
|
|
|
|
+ resizeObserver?.disconnect()
|
|
|
|
|
+ window.removeEventListener('resize', updateScale)
|
|
|
|
|
+ cancelAnimationFrame(resizeRaf)
|
|
|
|
|
+ chartInstance?.dispose()
|
|
|
|
|
+ chartInstance = null
|
|
|
|
|
+})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
|
|
|
+@import url('@/styles/kb.scss');
|
|
|
|
|
+
|
|
|
|
|
+.statistics-scale-wrapper {
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.statistics-page {
|
|
.statistics-page {
|
|
|
- padding: 12px;
|
|
|
|
|
- background: linear-gradient(180deg, #f5f8ff 0%, #f8fafc 100%);
|
|
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ color: #24364f;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.statistics-content {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ height: calc(100% - 52px * var(--kb-scale));
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ padding: calc(44px * var(--kb-scale)) calc(20px * var(--kb-scale)) calc(20px * var(--kb-scale));
|
|
|
|
|
+ flex-direction: column;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.section-row {
|
|
.section-row {
|
|
|
- margin-bottom: 12px;
|
|
|
|
|
|
|
+ margin-bottom: calc(14px * var(--kb-scale));
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+
|
|
|
|
|
+ &:last-child {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.trend-row {
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-col) {
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.chart-card {
|
|
.chart-card {
|
|
|
- border: 1px solid #e8edf5;
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- box-shadow: 0 6px 18px rgba(15, 23, 42, 0.04);
|
|
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: linear-gradient(180deg, rgb(235 243 255 / 76%) 0%, rgb(221 233 251 / 58%) 100%);
|
|
|
|
|
+ border: 1px solid rgb(255 255 255 / 58%);
|
|
|
|
|
+ border-radius: calc(20px * var(--kb-scale));
|
|
|
|
|
+ box-shadow:
|
|
|
|
|
+ inset 0 1px 0 rgb(255 255 255 / 72%),
|
|
|
|
|
+ 0 18px 36px rgb(46 90 164 / 12%);
|
|
|
|
|
+ backdrop-filter: blur(12px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.trend-card {
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-card__body) {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ min-height: 0;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.filter-card {
|
|
.filter-card {
|
|
|
:deep(.el-card__body) {
|
|
:deep(.el-card__body) {
|
|
|
- padding: 14px 18px 2px;
|
|
|
|
|
|
|
+ padding-bottom: calc(6px * var(--kb-scale));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.filter-form {
|
|
.filter-form {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ padding: calc(12px * var(--kb-scale)) calc(16px * var(--kb-scale)) 0;
|
|
|
|
|
+ gap: 0 calc(12px * var(--kb-scale));
|
|
|
|
|
+
|
|
|
:deep(.el-form-item) {
|
|
:deep(.el-form-item) {
|
|
|
- margin-bottom: 12px;
|
|
|
|
|
|
|
+ margin-bottom: calc(12px * var(--kb-scale));
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-form-item__label) {
|
|
|
|
|
+ width: calc(86px * var(--kb-scale)) !important;
|
|
|
|
|
+ height: calc(34px * var(--kb-scale));
|
|
|
|
|
+ font-size: calc(14px * var(--kb-scale));
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ line-height: calc(34px * var(--kb-scale));
|
|
|
|
|
+ color: #03409b;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-form-item__content) {
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ line-height: calc(34px * var(--kb-scale));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
:deep(.el-input__wrapper),
|
|
:deep(.el-input__wrapper),
|
|
|
:deep(.el-select__wrapper) {
|
|
:deep(.el-select__wrapper) {
|
|
|
- border-radius: 10px;
|
|
|
|
|
|
|
+ min-height: calc(34px * var(--kb-scale));
|
|
|
|
|
+ padding: 0 calc(11px * var(--kb-scale));
|
|
|
|
|
+ background: linear-gradient(180deg, rgb(255 255 255 / 92%) 0%, rgb(242 247 255 / 82%) 100%);
|
|
|
|
|
+ border: 1px solid rgb(255 255 255 / 88%);
|
|
|
|
|
+ border-radius: calc(8px * var(--kb-scale));
|
|
|
|
|
+ box-shadow:
|
|
|
|
|
+ inset 0 1px 0 rgb(255 255 255 / 95%),
|
|
|
|
|
+ 0 6px 12px rgb(152 181 230 / 12%);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-range-input),
|
|
|
|
|
+ :deep(.el-range-separator) {
|
|
|
|
|
+ font-size: calc(14px * var(--kb-scale));
|
|
|
|
|
+ color: #24364f;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.filter-control {
|
|
|
|
|
+ width: calc(180px * var(--kb-scale));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-control--date {
|
|
|
|
|
+ width: calc(220px * var(--kb-scale));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.filter-actions {
|
|
.filter-actions {
|
|
|
- margin-left: 8px;
|
|
|
|
|
|
|
+ margin-left: calc(8px * var(--kb-scale));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.action-btn {
|
|
.action-btn {
|
|
|
- min-width: 88px;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
|
|
+ height: calc(34px * var(--kb-scale));
|
|
|
|
|
+ min-width: calc(88px * var(--kb-scale));
|
|
|
|
|
+ padding: 0 calc(15px * var(--kb-scale));
|
|
|
|
|
+ font-size: calc(14px * var(--kb-scale));
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ color: #f5f9ff;
|
|
|
|
|
+ background: linear-gradient(180deg, #83bcff 0%, #2f7ee9 58%, #1762d6 100%);
|
|
|
|
|
+ border: 1px solid rgb(255 255 255 / 55%);
|
|
|
|
|
+ border-radius: calc(8px * var(--kb-scale));
|
|
|
|
|
+ box-shadow:
|
|
|
|
|
+ inset 0 1px 0 rgb(255 255 255 / 58%),
|
|
|
|
|
+ 0 4px 8px rgb(30 89 179 / 22%);
|
|
|
|
|
+
|
|
|
|
|
+ &:hover,
|
|
|
|
|
+ &:focus {
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ border-color: rgb(255 255 255 / 72%);
|
|
|
|
|
+ filter: brightness(1.06);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.card-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- min-height: 24px;
|
|
|
|
|
|
|
+.metric-grid {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ min-height: calc(214px * var(--kb-scale));
|
|
|
|
|
+ padding: calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale)) calc(16px * var(--kb-scale));
|
|
|
|
|
+ gap: calc(14px * var(--kb-scale));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.card-title {
|
|
|
|
|
- font-size: 15px;
|
|
|
|
|
- font-weight: 600;
|
|
|
|
|
- color: #334155;
|
|
|
|
|
|
|
+.metric-grid--order {
|
|
|
|
|
+ grid-template-columns: repeat(5, minmax(0, 1fr));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.gauge-row {
|
|
|
|
|
- height: 220px;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- justify-content: space-between;
|
|
|
|
|
|
|
+.metric-grid--device {
|
|
|
|
|
+ grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.gauge-item {
|
|
|
|
|
|
|
+.metric-card {
|
|
|
|
|
+ position: relative;
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- flex-direction: column;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- border-radius: 10px;
|
|
|
|
|
- padding: 4px 0;
|
|
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ min-height: calc(168px * var(--kb-scale));
|
|
|
|
|
+ padding: calc(18px * var(--kb-scale)) calc(18px * var(--kb-scale)) calc(16px * var(--kb-scale));
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ text-align: left;
|
|
|
cursor: pointer;
|
|
cursor: pointer;
|
|
|
- transition: all 0.2s ease;
|
|
|
|
|
|
|
+ background: linear-gradient(135deg, rgb(255 255 255 / 96%) 0%, rgb(239 246 255 / 88%) 100%),
|
|
|
|
|
+ radial-gradient(circle at 90% 12%, var(--metric-glow) 0%, rgb(255 255 255 / 0%) 38%);
|
|
|
|
|
+ border: 1px solid rgb(94 139 203 / 18%);
|
|
|
|
|
+ border-radius: calc(16px * var(--kb-scale));
|
|
|
|
|
+ box-shadow:
|
|
|
|
|
+ inset 0 1px 0 rgb(255 255 255 / 92%),
|
|
|
|
|
+ 0 10px 22px rgb(46 90 164 / 9%);
|
|
|
|
|
+ transition:
|
|
|
|
|
+ transform 0.28s ease,
|
|
|
|
|
+ box-shadow 0.28s ease,
|
|
|
|
|
+ border-color 0.28s ease,
|
|
|
|
|
+ background 0.28s ease;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.gauge-item:hover {
|
|
|
|
|
- background: #f8fbff;
|
|
|
|
|
- box-shadow: inset 0 0 0 1px #dbeafe;
|
|
|
|
|
|
|
+.metric-card::before {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: calc(14px * var(--kb-scale));
|
|
|
|
|
+ left: 0;
|
|
|
|
|
+ width: calc(4px * var(--kb-scale));
|
|
|
|
|
+ height: calc(100% - 28px * var(--kb-scale));
|
|
|
|
|
+ background: var(--metric-accent);
|
|
|
|
|
+ border-radius: 0 999px 999px 0;
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ box-shadow: 0 0 12px var(--metric-glow);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.gauge-label {
|
|
|
|
|
- margin-top: 6px;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- color: #64748b;
|
|
|
|
|
|
|
+.metric-card::after {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: calc(-28px * var(--kb-scale));
|
|
|
|
|
+ bottom: calc(-42px * var(--kb-scale));
|
|
|
|
|
+ width: calc(116px * var(--kb-scale));
|
|
|
|
|
+ height: calc(116px * var(--kb-scale));
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+ background: radial-gradient(circle, var(--metric-glow) 0%, rgb(255 255 255 / 0%) 72%);
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ opacity: 0.7;
|
|
|
|
|
+ transition:
|
|
|
|
|
+ transform 0.28s ease,
|
|
|
|
|
+ opacity 0.28s ease;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.trend-chart {
|
|
|
|
|
- height: 320px;
|
|
|
|
|
|
|
+.metric-card:hover {
|
|
|
|
|
+ background: linear-gradient(135deg, rgb(255 255 255 / 100%) 0%, rgb(232 242 255 / 92%) 100%),
|
|
|
|
|
+ radial-gradient(circle at 90% 12%, var(--metric-glow) 0%, rgb(255 255 255 / 0%) 42%);
|
|
|
|
|
+ border-color: rgb(58 103 180 / 28%);
|
|
|
|
|
+ transform: translateY(calc(-4px * var(--kb-scale)));
|
|
|
|
|
+ box-shadow:
|
|
|
|
|
+ inset 0 1px 0 rgb(255 255 255 / 92%),
|
|
|
|
|
+ 0 16px 28px rgb(41 89 164 / 16%);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-card:hover::after {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ transform: scale(1.12);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-card__label,
|
|
|
|
|
+.metric-card__hint,
|
|
|
|
|
+.metric-card__value,
|
|
|
|
|
+.metric-card__progress {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-:deep(.el-card__header) {
|
|
|
|
|
- border-bottom: 1px solid #eef2f7;
|
|
|
|
|
- padding: 14px 18px;
|
|
|
|
|
|
|
+.metric-card__label {
|
|
|
|
|
+ font-size: calc(14px * var(--kb-scale));
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #18345a;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-card__value {
|
|
|
|
|
+ margin-top: calc(14px * var(--kb-scale));
|
|
|
|
|
+ font-family: YouSheBiaoTiHei, sans-serif;
|
|
|
|
|
+ font-size: calc(38px * var(--kb-scale));
|
|
|
|
|
+ font-weight: normal;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ color: var(--metric-accent);
|
|
|
|
|
+ text-shadow: 0 4px 10px rgb(255 255 255 / 76%);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-card__count,
|
|
|
|
|
+:deep(.metric-card__count span) {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ font-family: YouSheBiaoTiHei, sans-serif;
|
|
|
|
|
+ font-size: inherit;
|
|
|
|
|
+ font-weight: normal;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ letter-spacing: 1px;
|
|
|
|
|
+ color: inherit;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-card__count {
|
|
|
|
|
+ transform: skewX(-6deg);
|
|
|
|
|
+ transform-origin: left center;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-card__hint {
|
|
|
|
|
+ margin-top: calc(8px * var(--kb-scale));
|
|
|
|
|
+ font-size: calc(12px * var(--kb-scale));
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ color: #536986;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-card__progress {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ height: calc(6px * var(--kb-scale));
|
|
|
|
|
+ margin-top: calc(14px * var(--kb-scale));
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: rgb(31 91 184 / 12%);
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.metric-card__progress span {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ width: var(--metric-progress);
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ background: var(--metric-accent);
|
|
|
|
|
+ border-radius: inherit;
|
|
|
|
|
+ box-shadow: 0 0 8px var(--metric-glow);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.trend-chart {
|
|
|
|
|
+ min-height: calc(260px * var(--kb-scale));
|
|
|
|
|
+ padding: calc(8px * var(--kb-scale)) calc(12px * var(--kb-scale)) calc(14px * var(--kb-scale));
|
|
|
|
|
+ flex: 1;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
:deep(.el-card__body) {
|
|
:deep(.el-card__body) {
|
|
|
- padding: 14px 18px 16px;
|
|
|
|
|
|
|
+ padding: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-@media (max-width: 1366px) {
|
|
|
|
|
- .gauge-row {
|
|
|
|
|
- overflow-x: auto;
|
|
|
|
|
- gap: 6px;
|
|
|
|
|
- justify-content: flex-start;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+:deep(.el-card) {
|
|
|
|
|
+ --el-card-bg-color: transparent;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|