|
|
@@ -0,0 +1,307 @@
|
|
|
+<script lang="ts" setup>
|
|
|
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
|
|
|
+import { IotStatApi } from '@/api/pms/stat'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
|
|
+import * as echarts from 'echarts'
|
|
|
+
|
|
|
+dayjs.extend(quarterOfYear)
|
|
|
+
|
|
|
+const chartRef = ref(null)
|
|
|
+const myChart = ref<echarts.EChartsType | null>(null)
|
|
|
+const currentTimeType = ref('month')
|
|
|
+
|
|
|
+const timeOptions = [
|
|
|
+ { label: '本月', value: 'month' },
|
|
|
+ { label: '本季度', value: 'quarter' },
|
|
|
+ { label: '本年', value: 'year' }
|
|
|
+]
|
|
|
+
|
|
|
+const fieldConfig = [
|
|
|
+ { key: 'ylWellCount', name: '压裂井数' },
|
|
|
+ { key: 'lyWellCount', name: '连油井数' },
|
|
|
+ { key: 'cumulativePumpTrips', name: '泵车台次' },
|
|
|
+ { key: 'cumulativeWorkingLayers', name: '压裂层数' }
|
|
|
+]
|
|
|
+
|
|
|
+const colorPalette = ['#5470c6', '#f1d209', '#e14f0f', '#91cc75']
|
|
|
+const hexToRgba = (hex: string, opacity: number) => {
|
|
|
+ let c: any
|
|
|
+ if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) {
|
|
|
+ c = hex.substring(1).split('')
|
|
|
+ if (c.length === 3) {
|
|
|
+ c = [c[0], c[0], c[1], c[1], c[2], c[2]]
|
|
|
+ }
|
|
|
+ c = '0x' + c.join('')
|
|
|
+ return 'rgba(' + [(c >> 16) & 255, (c >> 8) & 255, c & 255].join(',') + ',' + opacity + ')'
|
|
|
+ }
|
|
|
+ return hex
|
|
|
+}
|
|
|
+
|
|
|
+const getDateRange = (type: 'year' | 'quarter' | 'month') => {
|
|
|
+ const now = dayjs().subtract(1, 'month')
|
|
|
+ let start: dayjs.Dayjs, end: dayjs.Dayjs
|
|
|
+
|
|
|
+ if (type === 'year') {
|
|
|
+ start = now.startOf('year')
|
|
|
+ end = now.endOf('year')
|
|
|
+ } else if (type === 'quarter') {
|
|
|
+ start = now.startOf('quarter')
|
|
|
+ end = now.endOf('quarter')
|
|
|
+ } else {
|
|
|
+ start = now.startOf('month')
|
|
|
+ end = now.endOf('month')
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ start: start.format('YYYY-MM-DD HH:mm:ss'),
|
|
|
+ end: end.format('YYYY-MM-DD HH:mm:ss')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fetchData = async () => {
|
|
|
+ if (myChart.value) {
|
|
|
+ myChart.value.showLoading({
|
|
|
+ text: '',
|
|
|
+ color: '#409eff',
|
|
|
+ textColor: '#B6C8DA',
|
|
|
+ maskColor: 'rgba(0, 0, 0, 0.2)'
|
|
|
+ })
|
|
|
+ }
|
|
|
+
|
|
|
+ const { start, end } = getDateRange(currentTimeType.value as 'year' | 'quarter' | 'month')
|
|
|
+
|
|
|
+ const params = {
|
|
|
+ deptId: 163,
|
|
|
+ 'createTime[0]': start,
|
|
|
+ 'createTime[1]': end,
|
|
|
+ timeType: currentTimeType.value
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ let list: any[] = []
|
|
|
+
|
|
|
+ if (currentTimeType.value === 'year') {
|
|
|
+ const res = await IotStatApi.getRdWorkloadYear(params)
|
|
|
+ if (res && Array.isArray(res)) list = res
|
|
|
+ } else {
|
|
|
+ const res = await IotStatApi.getRdWorkload(params)
|
|
|
+ if (res && res.list) list = res.list
|
|
|
+ }
|
|
|
+
|
|
|
+ renderChart(list)
|
|
|
+ } catch (error) {
|
|
|
+ console.error('Workload API Error:', error)
|
|
|
+ } finally {
|
|
|
+ myChart.value?.hideLoading()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const renderChart = (data: any[]) => {
|
|
|
+ if (!myChart.value) return
|
|
|
+
|
|
|
+ const isYear = currentTimeType.value === 'year'
|
|
|
+
|
|
|
+ const xAxisData = data.map((item) => (isYear ? item.reportDate : item.projectDeptName))
|
|
|
+
|
|
|
+ const series = fieldConfig.map((field, index) => {
|
|
|
+ const color = colorPalette[index % colorPalette.length]
|
|
|
+
|
|
|
+ // 数据清洗,防止 null/undefined 报错
|
|
|
+ const seriesData = data.map((item) => {
|
|
|
+ const val = item[field.key]
|
|
|
+ return val === null || val === undefined || isNaN(val) ? 0 : val
|
|
|
+ })
|
|
|
+
|
|
|
+ if (isYear) {
|
|
|
+ return {
|
|
|
+ name: field.name,
|
|
|
+ type: 'line',
|
|
|
+ smooth: true,
|
|
|
+ symbol: 'circle',
|
|
|
+ symbolSize: 6,
|
|
|
+ showSymbol: false,
|
|
|
+ itemStyle: {
|
|
|
+ color: color
|
|
|
+ },
|
|
|
+ lineStyle: {
|
|
|
+ width: 2
|
|
|
+ },
|
|
|
+ areaStyle: {
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: hexToRgba(color, 0.4) }, // 顶部半透明
|
|
|
+ { offset: 1, color: hexToRgba(color, 0.05) } // 底部几近透明
|
|
|
+ ])
|
|
|
+ },
|
|
|
+ data: seriesData
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // --- 柱状图模式 (增加立体感) ---
|
|
|
+ return {
|
|
|
+ name: field.name,
|
|
|
+ type: 'bar',
|
|
|
+ barMaxWidth: 16, // 柱子不要太宽
|
|
|
+ itemStyle: {
|
|
|
+ borderRadius: [4, 4, 0, 0], // 顶部圆角
|
|
|
+ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
|
+ { offset: 0, color: color }, // 顶部纯色
|
|
|
+ { offset: 1, color: hexToRgba(color, 0.4) } // 底部半透明
|
|
|
+ ])
|
|
|
+ },
|
|
|
+ data: seriesData
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const option = {
|
|
|
+ // 提示框样式优化
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ backgroundColor: 'rgba(50,50,50,0.8)', // 深灰色背景
|
|
|
+ borderColor: '#457794', // 边框呼应网格颜色
|
|
|
+ textStyle: {
|
|
|
+ color: '#fff'
|
|
|
+ },
|
|
|
+ axisPointer: {
|
|
|
+ type: 'cross',
|
|
|
+ label: {
|
|
|
+ backgroundColor: '#6a7985'
|
|
|
+ },
|
|
|
+ lineStyle: {
|
|
|
+ color: '#B6C8DA',
|
|
|
+ type: 'dashed'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: fieldConfig.map((f) => f.name),
|
|
|
+ top: 0, // 紧贴顶部
|
|
|
+ icon: isYear ? 'circle' : 'roundRect', // 图例图标随图表类型变化
|
|
|
+ textStyle: {
|
|
|
+ color: '#B6C8DA'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '2%',
|
|
|
+ right: '3%',
|
|
|
+ bottom: '3%',
|
|
|
+ top: '12%', // 留出一点空间给图例
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: {
|
|
|
+ type: 'category',
|
|
|
+ // 折线图 false (贴边),柱状图 true (留白)
|
|
|
+ boundaryGap: !isYear,
|
|
|
+ data: xAxisData,
|
|
|
+ axisLabel: {
|
|
|
+ color: '#B6C8DA',
|
|
|
+ formatter: (val: string) => {
|
|
|
+ // 如果名字太长,可以截断或者换行,这里简单处理
|
|
|
+ return val.length > 6 ? val.slice(0, 6) + '...' : val
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ lineStyle: {
|
|
|
+ color: '#B6C8DA'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisTick: {
|
|
|
+ show: false // 隐藏刻度线,更简洁
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 单 Y 轴配置
|
|
|
+ yAxis: {
|
|
|
+ type: 'value',
|
|
|
+ axisLabel: {
|
|
|
+ color: '#B6C8DA',
|
|
|
+ formatter: '{value}'
|
|
|
+ },
|
|
|
+ // 复刻原图的虚线网格
|
|
|
+ splitLine: {
|
|
|
+ show: true,
|
|
|
+ lineStyle: {
|
|
|
+ color: '#457794', // 蓝灰色
|
|
|
+ type: 'dashed', // 虚线
|
|
|
+ opacity: 0.5 // 半透明
|
|
|
+ }
|
|
|
+ },
|
|
|
+ axisLine: {
|
|
|
+ show: true, // 显示轴线
|
|
|
+ lineStyle: {
|
|
|
+ color: '#B6C8DA'
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ series: series
|
|
|
+ }
|
|
|
+
|
|
|
+ myChart.value.setOption(option, true)
|
|
|
+}
|
|
|
+
|
|
|
+const handleTimeChange = () => {
|
|
|
+ fetchData()
|
|
|
+}
|
|
|
+
|
|
|
+const resizeChart = () => myChart.value?.resize()
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ nextTick(() => {
|
|
|
+ myChart.value = echarts.init(chartRef.value)
|
|
|
+ fetchData()
|
|
|
+ window.addEventListener('resize', resizeChart)
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', resizeChart)
|
|
|
+ myChart.value?.dispose()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="card size-full rounded-lg p-4 flex flex-col">
|
|
|
+ <div class="flex justify-between items-center mb-4">
|
|
|
+ <div class="text-[#b6c8da] text-lg font-bold">工单数量统计</div>
|
|
|
+ <el-segmented
|
|
|
+ size="default"
|
|
|
+ v-model="currentTimeType"
|
|
|
+ :options="timeOptions"
|
|
|
+ @change="handleTimeChange"
|
|
|
+ class="dark-segmented w-50!"
|
|
|
+ block
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div ref="chartRef" class="flex-1 w-full min-h-0"></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.card {
|
|
|
+ background-color: rgb(0 0 0 / 30%);
|
|
|
+ box-shadow: 0 2px 12px rgb(0 0 0 / 50%);
|
|
|
+ transition: all 0.3s ease;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.dark-segmented {
|
|
|
+ --el-segmented-item-selected-color: #e5eaf3;
|
|
|
+ --el-border-radius-base: 16px;
|
|
|
+ --el-segmented-color: #cfd3dc;
|
|
|
+ --el-segmented-bg-color: #262727;
|
|
|
+ --el-segmented-item-selected-bg-color: #409eff;
|
|
|
+ --el-segmented-item-selected-disabled-bg-color: rgb(42 89 138);
|
|
|
+ --el-segmented-item-hover-color: #e5eaf3;
|
|
|
+ --el-segmented-item-hover-bg-color: #39393a;
|
|
|
+ --el-segmented-item-active-bg-color: #424243;
|
|
|
+ --el-segmented-item-disabled-color: #8d9095;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-segmented__item) {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+</style>
|