|
@@ -0,0 +1,713 @@
|
|
|
|
+<template>
|
|
|
|
+ <!-- 第二行:图表行 -->
|
|
|
|
+ <el-row :gutter="16" class="mb-4">
|
|
|
|
+ <el-col :span="6">
|
|
|
|
+ <el-card class="chart-card" shadow="never">
|
|
|
|
+ <template #header>
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
+ <span class="text-base font-medium text-gray-600">设备状态统计</span>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ <div ref="statusChartRef" class="h-[290px]"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+ </el-col>
|
|
|
|
+ <el-col :span="8">
|
|
|
|
+ <el-card class="chart-card" shadow="never">
|
|
|
|
+ <template #header>
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
+ <span class="text-base font-medium text-gray-600">设备类别TOP5数量</span>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ <div ref="topContainer" class="h-[290px]"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+ </el-col>
|
|
|
|
+ <el-col :span="10">
|
|
|
|
+ <el-card class="chart-card" shadow="never">
|
|
|
|
+ <template #header>
|
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
|
+ <span class="text-base font-medium text-gray-600">当日运维成本</span>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ <div ref="chartContainer" class="h-[290px]"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+ </el-col>
|
|
|
|
+ </el-row>
|
|
|
|
+
|
|
|
|
+ <!-- 第三行:消息统计行 -->
|
|
|
|
+ <el-row :gutter="16" class="mb-1">
|
|
|
|
+ <el-col :span="6">
|
|
|
|
+ <el-card class="chart-card" shadow="never">
|
|
|
|
+ <template #header>
|
|
|
|
+ <div class="flex items-center">
|
|
|
|
+ <span class="text-base font-medium text-gray-600">近7天物料消耗TOP5</span>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ <div ref="materialChartRef" class="h-[320px]"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+ </el-col>
|
|
|
|
+ <el-col :span="18">
|
|
|
|
+ <el-card class="chart-card" shadow="never">
|
|
|
|
+ <template #header>
|
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
|
+ <span class="text-base font-medium text-gray-600">工单数量情况</span>
|
|
|
|
+ </div>
|
|
|
|
+ </template>
|
|
|
|
+ <div ref="qxRef" class="h-[320px]"></div>
|
|
|
|
+ </el-card>
|
|
|
|
+ </el-col>
|
|
|
|
+ </el-row>
|
|
|
|
+
|
|
|
|
+ <!-- TODO 第四行:地图 -->
|
|
|
|
+</template>
|
|
|
|
+
|
|
|
|
+<script setup lang="ts" name="Index">
|
|
|
|
+import * as echarts from 'echarts/core'
|
|
|
|
+import { BarChart, GaugeChart, LineChart, PieChart } from 'echarts/charts' // 显式导入柱状图模块
|
|
|
|
+import {
|
|
|
|
+ GridComponent,
|
|
|
|
+ LegendComponent,
|
|
|
|
+ TitleComponent,
|
|
|
|
+ ToolboxComponent,
|
|
|
|
+ TooltipComponent
|
|
|
|
+} from 'echarts/components'
|
|
|
|
+import { LabelLayout, UniversalTransition } from 'echarts/features'
|
|
|
|
+import { CanvasRenderer } from 'echarts/renderers'
|
|
|
|
+import { useElementSize } from '@vueuse/core'
|
|
|
|
+import {
|
|
|
|
+ IotStatisticsDeviceMessageSummaryRespVO,
|
|
|
|
+ IotStatisticsSummaryRespVO
|
|
|
|
+} from '@/api/iot/statistics'
|
|
|
|
+import { formatDate } from '@/utils/formatTime'
|
|
|
|
+import { IotStatApi } from '@/api/pms/stat'
|
|
|
|
+
|
|
|
|
+// TODO @super:参考下 /Users/yunai/Java/yudao-ui-admin-vue3/src/views/mall/home/index.vue,拆一拆组件
|
|
|
|
+
|
|
|
|
+/** IoT 首页 */
|
|
|
|
+defineOptions({ name: 'IotRhStat' })
|
|
|
|
+
|
|
|
|
+// TODO @super:使用下 Echart 组件,参考 yudao-ui-admin-vue3/src/views/mall/home/components/TradeTrendCard.vue 等
|
|
|
|
+echarts.use([
|
|
|
|
+ TooltipComponent,
|
|
|
|
+ LegendComponent,
|
|
|
|
+ PieChart,
|
|
|
|
+ CanvasRenderer,
|
|
|
|
+ LabelLayout,
|
|
|
|
+ TitleComponent,
|
|
|
|
+ ToolboxComponent,
|
|
|
|
+ GridComponent,
|
|
|
|
+ LineChart,
|
|
|
|
+ UniversalTransition,
|
|
|
|
+ GaugeChart,
|
|
|
|
+ BarChart
|
|
|
|
+])
|
|
|
|
+
|
|
|
|
+const dateRange = ref<[Date, Date] | null>(null)
|
|
|
|
+
|
|
|
|
+const queryParams = reactive({
|
|
|
|
+ startTime: Date.now() - 7 * 24 * 60 * 60 * 1000, // 设置默认开始时间为 7 天前
|
|
|
|
+ endTime: Date.now() // 设置默认结束时间为当前时间
|
|
|
|
+})
|
|
|
|
+const backendData = ref([])
|
|
|
|
+const statusChartRef = ref() // 设备数量统计的图表
|
|
|
|
+const materialChartRef = ref() // 设备数量统计的图表
|
|
|
|
+// 基础统计数据
|
|
|
|
+// TODO @super:初始为 -1,然后界面展示先是加载中?试试用 cursor 改哈
|
|
|
|
+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 device = ref({
|
|
|
|
+ total: undefined,
|
|
|
|
+ today: undefined
|
|
|
|
+})
|
|
|
|
+const maintain = ref({
|
|
|
|
+ total: undefined,
|
|
|
|
+ today: undefined
|
|
|
|
+})
|
|
|
|
+const work = ref({
|
|
|
|
+ total: undefined,
|
|
|
|
+ today: undefined
|
|
|
|
+})
|
|
|
|
+const inspect = ref({
|
|
|
|
+ total: undefined,
|
|
|
|
+ today: undefined
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+const status = ref({
|
|
|
|
+ finished: 0,
|
|
|
|
+ todo: 0
|
|
|
|
+})
|
|
|
|
+const todayStatus = ref({
|
|
|
|
+ finished: 0,
|
|
|
|
+ todo: 0
|
|
|
|
+})
|
|
|
|
+const typeData = ref({})
|
|
|
|
+const materialData = ref({})
|
|
|
|
+const orderSevenData = ref({})
|
|
|
|
+const safe = ref()
|
|
|
|
+/** 获取统计数据 */
|
|
|
|
+const getStats = async () => {
|
|
|
|
+ // 获取基础统计数据
|
|
|
|
+ IotStatApi.getDeviceCount().then((res) => {
|
|
|
|
+ device.value = res
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getMaintainCount().then((res) => {
|
|
|
|
+ maintain.value = res
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getMainWorkCount().then((res) => {
|
|
|
|
+ work.value = res
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getInspectCount().then((res) => {
|
|
|
|
+ inspect.value = res
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getMaintenanceStatus().then((res) => {
|
|
|
|
+ status.value = res
|
|
|
|
+ initCharts()
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getMaintenanceTodayStatus().then((res) => {
|
|
|
|
+ todayStatus.value = res
|
|
|
|
+ initCharts()
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getDeviceStatusCount().then((res) => {
|
|
|
|
+ typeData.value = res
|
|
|
|
+ initCharts()
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getSafeCount().then((res) => {
|
|
|
|
+ safe.value = res
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getMaterial().then((res) => {
|
|
|
|
+ materialData.value = res
|
|
|
|
+ initCharts();
|
|
|
|
+ })
|
|
|
|
+ IotStatApi.getOrderSeven().then((res) => {
|
|
|
|
+ debugger
|
|
|
|
+ orderSevenData.value = res
|
|
|
|
+ initQxChart();
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/** 初始化图表 */
|
|
|
|
+const initCharts = () => {
|
|
|
|
+ // 设备数量统计
|
|
|
|
+ echarts.init(statusChartRef.value).setOption({
|
|
|
|
+ tooltip: {
|
|
|
|
+ trigger: 'item'
|
|
|
|
+ },
|
|
|
|
+ legend: {
|
|
|
|
+ orient: 'horizontal', // 水平排列图例项
|
|
|
|
+ bottom: '0%', // 放置在底部
|
|
|
|
+ icon: 'circle'
|
|
|
|
+ },
|
|
|
|
+ series: [
|
|
|
|
+ {
|
|
|
|
+ name: '',
|
|
|
|
+ type: 'pie',
|
|
|
|
+ radius: ['50%', '80%'],
|
|
|
|
+ avoidLabelOverlap: false,
|
|
|
|
+ center: ['50%', '44%'],
|
|
|
|
+ label: {
|
|
|
|
+ show: false,
|
|
|
|
+ position: 'outside'
|
|
|
|
+ },
|
|
|
|
+ emphasis: {
|
|
|
|
+ label: {
|
|
|
|
+ show: true,
|
|
|
|
+ fontSize: 15,
|
|
|
|
+ fontWeight: 'bold'
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ labelLine: {
|
|
|
|
+ show: false
|
|
|
|
+ },
|
|
|
|
+ data: typeData.value
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ })
|
|
|
|
+
|
|
|
|
+ echarts.init(materialChartRef.value).setOption({
|
|
|
|
+ tooltip: {
|
|
|
|
+ trigger: 'item'
|
|
|
|
+ },
|
|
|
|
+ legend: {
|
|
|
|
+ // top: '5%',
|
|
|
|
+ // right: '10%',
|
|
|
|
+ // align: 'left',
|
|
|
|
+ // orient: 'vertical',
|
|
|
|
+ // icon: 'circle'
|
|
|
|
+ orient: 'horizontal', // 水平排列图例项
|
|
|
|
+ bottom: '0%', // 放置在底部
|
|
|
|
+ icon: 'circle'
|
|
|
|
+ },
|
|
|
|
+ series: [
|
|
|
|
+ {
|
|
|
|
+ name: '',
|
|
|
|
+ type: 'pie',
|
|
|
|
+ radius: ['50%', '80%'],
|
|
|
|
+ avoidLabelOverlap: false,
|
|
|
|
+ center: ['50%', '44%'],
|
|
|
|
+ label: {
|
|
|
|
+ show: false,
|
|
|
|
+ position: 'outside'
|
|
|
|
+ },
|
|
|
|
+ emphasis: {
|
|
|
|
+ label: {
|
|
|
|
+ show: true,
|
|
|
|
+ fontSize: 15,
|
|
|
|
+ fontWeight: 'bold'
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ labelLine: {
|
|
|
|
+ show: false
|
|
|
|
+ },
|
|
|
|
+ data: materialData.value
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/** 初始化消息统计图表 */
|
|
|
|
+const chartContainer = ref(null)
|
|
|
|
+let chartInstance = null
|
|
|
|
+
|
|
|
|
+// 生成过去12个月份的标签 (格式: YYYY-MM)
|
|
|
|
+const generateMonthLabels = () => {
|
|
|
|
+ const months = []
|
|
|
|
+ const date = new Date()
|
|
|
|
+ for (let i = 11; i >= 0; i--) {
|
|
|
|
+ const tempDate = new Date(date.getFullYear(), date.getMonth() - i, 1)
|
|
|
|
+ const year = tempDate.getFullYear()
|
|
|
|
+ const month = String(tempDate.getMonth() + 1).padStart(2, '0')
|
|
|
|
+ months.push(`${year}-${month}`)
|
|
|
|
+ }
|
|
|
|
+ return months
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 模拟数据获取
|
|
|
|
+const fetchChartData = async () => {
|
|
|
|
+ // 模拟异步请求
|
|
|
|
+ return new Promise((resolve) => {
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ resolve({
|
|
|
|
+ months: ['空压机','增压机','提纯撬'],
|
|
|
|
+ repairs: [10, 30, 90 ]
|
|
|
|
+ })
|
|
|
|
+ }, 300)
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 初始化图表配置
|
|
|
|
+const initChart = async () => {
|
|
|
|
+ if (!chartContainer.value) return
|
|
|
|
+
|
|
|
|
+ // 获取数据
|
|
|
|
+ const { months, faults, repairs } = await fetchChartData()
|
|
|
|
+ // const months = ['空压机','增压机','提纯撬']
|
|
|
|
+ // ECharts配置
|
|
|
|
+ const option = {
|
|
|
|
+ tooltip: {
|
|
|
|
+ trigger: 'axis',
|
|
|
|
+ axisPointer: {
|
|
|
|
+ type: 'shadow'
|
|
|
|
+ },
|
|
|
|
+ formatter: (params) => {
|
|
|
|
+ return `${params[0].axisValue}<br/>
|
|
|
|
+ ${params[0].marker} ${params[0].seriesName}: ${params[0].value}`
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ legend: {
|
|
|
|
+ data: ['当日运维成本'],
|
|
|
|
+ top: 1
|
|
|
|
+ },
|
|
|
|
+ grid: {
|
|
|
|
+ left: '3%',
|
|
|
|
+ right: '4%',
|
|
|
|
+ bottom: '1%',
|
|
|
|
+ containLabel: true
|
|
|
|
+ },
|
|
|
|
+ xAxis: {
|
|
|
|
+ type: 'category',
|
|
|
|
+ data: months,
|
|
|
|
+ axisLabel: {
|
|
|
|
+ rotate: 45,
|
|
|
|
+ margin: 15
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ yAxis: {
|
|
|
|
+ type: 'value',
|
|
|
|
+ axisLabel: {
|
|
|
|
+ formatter: (value) => Math.floor(value).toString()
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ series: [
|
|
|
|
+ {
|
|
|
|
+ name: '当日运维成本',
|
|
|
|
+ type: 'bar',
|
|
|
|
+ itemStyle: {
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
+ { offset: 0, color: '#f69606' },
|
|
|
|
+ ])
|
|
|
|
+ },
|
|
|
|
+ emphasis: {
|
|
|
|
+ focus: 'series'
|
|
|
|
+ },
|
|
|
|
+ data: repairs
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 初始化图表
|
|
|
|
+ chartInstance = echarts.init(chartContainer.value)
|
|
|
|
+ chartInstance.setOption(option)
|
|
|
|
+
|
|
|
|
+ // 窗口缩放监听
|
|
|
|
+ window.addEventListener('resize', handleResize)
|
|
|
|
+ handleResize()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 自适应调整
|
|
|
|
+const handleResize = () => {
|
|
|
|
+ chartInstance?.resize()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const topContainer = ref(null)
|
|
|
|
+let topInstance = null
|
|
|
|
+// 响应式容器尺寸
|
|
|
|
+const { width, height } = useElementSize(topContainer)
|
|
|
|
+// 处理数据(排序+限制5条)
|
|
|
|
+const processedData = () => {
|
|
|
|
+ const data = IotStatApi.getDeviceTypeCount()
|
|
|
|
+ backendData.value = data
|
|
|
|
+ return [...backendData.value].sort((a, b) => a.value - b.value)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const fetchTop = () => {
|
|
|
|
+ IotStatApi.getDeviceTypeCount().then((res) => {
|
|
|
|
+ backendData.value = res
|
|
|
|
+ })
|
|
|
|
+}
|
|
|
|
+// 初始化图表配置
|
|
|
|
+const getTopOption = () => {
|
|
|
|
+ // backendData.value = data
|
|
|
|
+ const data = backendData.value.sort((a, b) => a.value - b.value)
|
|
|
|
+ return {
|
|
|
|
+ tooltip: {
|
|
|
|
+ trigger: 'axis',
|
|
|
|
+ axisPointer: { type: 'shadow' },
|
|
|
|
+ formatter: (params) => {
|
|
|
|
+ const item = params[0]
|
|
|
|
+ return `${item.name}<br/>${item.marker} ${item.value.toLocaleString()}`
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ grid: {
|
|
|
|
+ height: '200px',
|
|
|
|
+ left: '6%',
|
|
|
|
+ right: '6%',
|
|
|
|
+ bottom: '18%',
|
|
|
|
+ containLabel: true
|
|
|
|
+ },
|
|
|
|
+ xAxis: {
|
|
|
|
+ type: 'value',
|
|
|
|
+ axisLabel: {
|
|
|
|
+ formatter: (value) => {
|
|
|
|
+ if (value >= 10000) return `${(value / 10000).toFixed(1)}万`
|
|
|
|
+ return value.toLocaleString()
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ yAxis: {
|
|
|
|
+ type: 'category',
|
|
|
|
+ data: data.map((item) => item.category),
|
|
|
|
+ axisTick: { show: false },
|
|
|
|
+ axisLabel: {}
|
|
|
|
+ },
|
|
|
|
+ series: [
|
|
|
|
+ {
|
|
|
|
+ type: 'bar',
|
|
|
|
+ data: data.map((item) => item.value),
|
|
|
|
+ itemStyle: {
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
|
|
|
+ { offset: 0, color: '#83bff6' },
|
|
|
|
+ { offset: 0.7, color: '#188df0' },
|
|
|
|
+ { offset: 1, color: '#188df0' }
|
|
|
|
+ ]),
|
|
|
|
+ borderRadius: [0, 8, 8, 0]
|
|
|
|
+ },
|
|
|
|
+ label: {
|
|
|
|
+ show: true,
|
|
|
|
+ position: 'right',
|
|
|
|
+ formatter: '{@value}',
|
|
|
|
+ color: '#333',
|
|
|
|
+ fontWeight: 'bold'
|
|
|
|
+ },
|
|
|
|
+ emphasis: {
|
|
|
|
+ itemStyle: {
|
|
|
|
+ shadowBlur: 10,
|
|
|
|
+ shadowColor: 'rgba(0, 0, 0, 0.5)'
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 初始化图表
|
|
|
|
+const initTopChart = async () => {
|
|
|
|
+ await IotStatApi.getDeviceTypeCount().then((res) => {
|
|
|
|
+ backendData.value = res
|
|
|
|
+ })
|
|
|
|
+ if (!topContainer.value) return
|
|
|
|
+ topInstance = echarts.init(topContainer.value)
|
|
|
|
+ updateTopChart()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 更新图表
|
|
|
|
+const updateTopChart = () => {
|
|
|
|
+ if (!topInstance) return
|
|
|
|
+ topInstance.setOption(getTopOption())
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 自适应调整
|
|
|
|
+watch([width, height], () => {
|
|
|
|
+ topInstance?.resize()
|
|
|
|
+})
|
|
|
|
+
|
|
|
|
+// 监听数据变化
|
|
|
|
+watch(
|
|
|
|
+ backendData,
|
|
|
|
+ () => {
|
|
|
|
+ updateTopChart()
|
|
|
|
+ },
|
|
|
|
+ { deep: true }
|
|
|
|
+)
|
|
|
|
+
|
|
|
|
+const activeDom = ref(null)
|
|
|
|
+let activeInstance = null
|
|
|
|
+
|
|
|
|
+const activeData = ref([])
|
|
|
|
+const initActiveChart = async () => {
|
|
|
|
+ if (!activeDom.value) return
|
|
|
|
+ activeData.value = await IotStatApi.getDeptCount()
|
|
|
|
+ activeInstance = echarts.init(activeDom.value)
|
|
|
|
+
|
|
|
|
+ const option = {
|
|
|
|
+ tooltip: {
|
|
|
|
+ trigger: 'axis',
|
|
|
|
+ axisPointer: { type: 'shadow' }, //:ml-citation{ref="1,7" data="citationList"}
|
|
|
|
+ formatter: (params) => `
|
|
|
|
+ ${params[0].name}<br/>
|
|
|
|
+ ${params[0].marker} 总人数: ${params[0].value}<br/>
|
|
|
|
+ ${params[1].marker} 活跃人数: ${params[1].value}
|
|
|
|
+ `
|
|
|
|
+ },
|
|
|
|
+ legend: {
|
|
|
|
+ data: ['总人数', '活跃人数'],
|
|
|
|
+ top: 30
|
|
|
|
+ },
|
|
|
|
+ grid: {
|
|
|
|
+ left: '3%',
|
|
|
|
+ right: '4%',
|
|
|
|
+ bottom: '3%',
|
|
|
|
+ containLabel: true //:ml-citation{ref="2,7" data="citationList"}
|
|
|
|
+ },
|
|
|
|
+ xAxis: {
|
|
|
|
+ type: 'category',
|
|
|
|
+ data: activeData.value.map((item) => item.department),
|
|
|
|
+ axisLabel: {
|
|
|
|
+ interval: 0,
|
|
|
|
+ rotate: 0 //:ml-citation{ref="5" data="citationList"}
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ yAxis: {
|
|
|
|
+ type: 'value',
|
|
|
|
+ name: '人数',
|
|
|
|
+ splitLine: {
|
|
|
|
+ show: true,
|
|
|
|
+ lineStyle: { type: 'dashed' }
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ series: [
|
|
|
|
+ {
|
|
|
|
+ name: '总人数',
|
|
|
|
+ type: 'bar',
|
|
|
|
+ data: activeData.value.map((item) => item.total),
|
|
|
|
+ itemStyle: {
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
+ { offset: 0, color: '#5470c6' },
|
|
|
|
+ { offset: 1, color: '#83bff6' }
|
|
|
|
+ ])
|
|
|
|
+ },
|
|
|
|
+ barWidth: 30
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ name: '活跃人数',
|
|
|
|
+ type: 'bar',
|
|
|
|
+ data: activeData.value.map((item) => item.active),
|
|
|
|
+ itemStyle: {
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
+ { offset: 0, color: '#91cc75' },
|
|
|
|
+ { offset: 1, color: '#e6f4d2' }
|
|
|
|
+ ])
|
|
|
|
+ },
|
|
|
|
+ barWidth: 30
|
|
|
|
+ }
|
|
|
|
+ ]
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ activeInstance.setOption(option)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const qxRef = ref(null)
|
|
|
|
+let qxInstance = null
|
|
|
|
+
|
|
|
|
+// 生成近12个月份 (包含当年和去年)
|
|
|
|
+const generateMonths = () => {
|
|
|
|
+ const months = []
|
|
|
|
+ const date = new Date()
|
|
|
|
+ date.setMonth(date.getMonth() + 1, 1) // 从下个月开始倒推
|
|
|
|
+
|
|
|
|
+ for (let i = 0; i < 12; i++) {
|
|
|
|
+ date.setMonth(date.getMonth() - 1)
|
|
|
|
+ months.push(`${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}`)
|
|
|
|
+ }
|
|
|
|
+ return months.reverse()
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+const initQxChart = () => {
|
|
|
|
+ if (!qxRef.value) return
|
|
|
|
+debugger
|
|
|
|
+ qxInstance = echarts.init(qxRef.value)
|
|
|
|
+ debugger
|
|
|
|
+ const option = {
|
|
|
|
+ tooltip: {
|
|
|
|
+ trigger: 'axis',
|
|
|
|
+ axisPointer: {
|
|
|
|
+ type: 'cross',
|
|
|
|
+ label: {
|
|
|
|
+ backgroundColor: '#6a7985'
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ legend: {
|
|
|
|
+ data: orderSevenData.value.series.map((item) => item.name),
|
|
|
|
+ top: 30
|
|
|
|
+ },
|
|
|
|
+ grid: {
|
|
|
|
+ left: '3%',
|
|
|
|
+ right: '4%',
|
|
|
|
+ bottom: '3%',
|
|
|
|
+ containLabel: true
|
|
|
|
+ },
|
|
|
|
+ xAxis: {
|
|
|
|
+ type: 'category',
|
|
|
|
+ boundaryGap: false,
|
|
|
|
+ data: orderSevenData.value.xAxis,
|
|
|
|
+ axisLabel: {
|
|
|
|
+ formatter: (value) => value.split('-').join('/') // 显示为 2023/01
|
|
|
|
+ }
|
|
|
|
+ },
|
|
|
|
+ // yAxis: {
|
|
|
|
+ // type: 'value',
|
|
|
|
+ // axisLabel: {
|
|
|
|
+ // formatter: '{value}'
|
|
|
|
+ // }
|
|
|
|
+ // },
|
|
|
|
+ yAxis: [
|
|
|
|
+ {
|
|
|
|
+ type: 'value',
|
|
|
|
+ axisLabel: {
|
|
|
|
+ formatter: '{value}'
|
|
|
|
+ },
|
|
|
|
+ position: 'left' // 左侧 Y 轴
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ type: 'value',
|
|
|
|
+ axisLabel: {
|
|
|
|
+ formatter: '{value}'
|
|
|
|
+ },
|
|
|
|
+ position: 'right', // 右侧 Y 轴
|
|
|
|
+ splitLine: {
|
|
|
|
+ show: false // 隐藏右侧 Y 轴的分割线
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ ],
|
|
|
|
+
|
|
|
|
+ // series: orderSevenData.value.series.map((item, index) => ({
|
|
|
|
+ // name: item.name,
|
|
|
|
+ // type: 'line',
|
|
|
|
+ // smooth: true,
|
|
|
|
+ // symbol: 'circle',
|
|
|
|
+ // symbolSize: 8,
|
|
|
|
+ // itemStyle: {
|
|
|
|
+ // color: ['#5470c6', '#f1d209', '#e14f0f','#09a134'][index] // 蓝、绿、黄
|
|
|
|
+ // },
|
|
|
|
+ // areaStyle: {
|
|
|
|
+ // color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
+ // { offset: 0, color: 'rgba(84,112,198,0.4)' },
|
|
|
|
+ // { offset: 1, color: 'rgba(84,112,198,0.1)' }
|
|
|
|
+ // ])
|
|
|
|
+ // },
|
|
|
|
+ // data: item.data
|
|
|
|
+ // }))
|
|
|
|
+ series: orderSevenData.value.series.map((item, index) => {
|
|
|
|
+ // 假设前两条曲线使用左侧 Y 轴,后两条曲线使用右侧 Y 轴
|
|
|
|
+ const yAxisIndex = index < 2 ? 0 : 1;
|
|
|
|
+ return {
|
|
|
|
+ name: item.name,
|
|
|
|
+ type: 'line',
|
|
|
|
+ smooth: true,
|
|
|
|
+ symbol: 'circle',
|
|
|
|
+ symbolSize: 8,
|
|
|
|
+ itemStyle: {
|
|
|
|
+ color: ['#5470c6', '#f1d209', '#e14f0f', '#91cc75'][index]
|
|
|
|
+ },
|
|
|
|
+ areaStyle: {
|
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
|
+ { offset: 0, color: 'rgba(84,112,198,0.4)' },
|
|
|
|
+ { offset: 1, color: 'rgba(84,112,198,0.1)' }
|
|
|
|
+ ])
|
|
|
|
+ },
|
|
|
|
+ data: item.data,
|
|
|
|
+ yAxisIndex: yAxisIndex // 指定使用的 Y 轴
|
|
|
|
+ }
|
|
|
|
+ })
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ qxInstance.setOption(option)
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+// 响应式调整
|
|
|
|
+const resizeQxChart = () => qxInstance?.resize()
|
|
|
|
+
|
|
|
|
+/** 初始化 */
|
|
|
|
+onMounted(() => {
|
|
|
|
+ getStats()
|
|
|
|
+ initChart()
|
|
|
|
+ initTopChart()
|
|
|
|
+ initActiveChart()
|
|
|
|
+ initQxChart()
|
|
|
|
+ window.addEventListener('resize', resizeQxChart)
|
|
|
|
+ // fetchTop()
|
|
|
|
+ window.addEventListener('resize', () => topInstance?.resize())
|
|
|
|
+})
|
|
|
|
+onBeforeUnmount(() => {
|
|
|
|
+ chartInstance?.dispose()
|
|
|
|
+ window.removeEventListener('resize', () => chartInstance?.resize())
|
|
|
|
+ topInstance?.dispose()
|
|
|
|
+ window.removeEventListener('resize', handleResize)
|
|
|
|
+ qxInstance?.dispose()
|
|
|
|
+ window.removeEventListener('resize', resizeQxChart)
|
|
|
|
+})
|
|
|
|
+</script>
|
|
|
|
+
|
|
|
|
+<style lang="scss" scoped></style>
|