|
@@ -1,9 +1,53 @@
|
|
|
<template>
|
|
<template>
|
|
|
<view class="rodent-analysis">
|
|
<view class="rodent-analysis">
|
|
|
- <!-- 活动节律分析 -->
|
|
|
|
|
|
|
+ <!-- 发生趋势分析 -->
|
|
|
<view class="analysis-card">
|
|
<view class="analysis-card">
|
|
|
<view class="analysis-card__header">
|
|
<view class="analysis-card__header">
|
|
|
- <view class="analysis-card__title">活动节律分析</view>
|
|
|
|
|
|
|
+ <view class="analysis-card__title">发生趋势分析</view>
|
|
|
|
|
+ <!-- <view class="range-tabs">
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="range-tab"
|
|
|
|
|
+ :class="{ active: trendRange === 'week' }"
|
|
|
|
|
+ @click="switchTrendRange('week')"
|
|
|
|
|
+ >周</view>
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="range-tab"
|
|
|
|
|
+ :class="{ active: trendRange === 'month' }"
|
|
|
|
|
+ @click="switchTrendRange('month')"
|
|
|
|
|
+ >月</view>
|
|
|
|
|
+ <view
|
|
|
|
|
+ class="range-tab"
|
|
|
|
|
+ :class="{ active: trendRange === 'year' }"
|
|
|
|
|
+ @click="switchTrendRange('year')"
|
|
|
|
|
+ >年</view>
|
|
|
|
|
+ </view> -->
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="trend-legend">
|
|
|
|
|
+ <view class="trend-legend__item" v-for="item in trendLegend" :key="item.name">
|
|
|
|
|
+ <view class="trend-legend__line" :style="{ background: item.color }">
|
|
|
|
|
+ <view class="trend-legend__dot" :style="{ borderColor: item.color }"></view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <text class="trend-legend__text">{{ item.name }}</text>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+ <view class="trend-axis-label">鼠害数量:只</view>
|
|
|
|
|
+ <view class="chart-wrap chart-wrap--trend">
|
|
|
|
|
+ <qiun-data-charts
|
|
|
|
|
+ type="line"
|
|
|
|
|
+ :chartData="trendChart"
|
|
|
|
|
+ :opts="trendOpts"
|
|
|
|
|
+ :reshow="active"
|
|
|
|
|
+ :canvas2d="true"
|
|
|
|
|
+ :inScrollView="true"
|
|
|
|
|
+ :ontouch="true"
|
|
|
|
|
+ />
|
|
|
|
|
+ </view>
|
|
|
|
|
+ </view>
|
|
|
|
|
+
|
|
|
|
|
+ <!-- 鼠害活动时段分析 -->
|
|
|
|
|
+ <view class="analysis-card">
|
|
|
|
|
+ <view class="analysis-card__header">
|
|
|
|
|
+ <view class="analysis-card__title">鼠害活动时段分析</view>
|
|
|
<view class="analysis-card__extra">
|
|
<view class="analysis-card__extra">
|
|
|
<text class="peak-label">活跃高峰</text>
|
|
<text class="peak-label">活跃高峰</text>
|
|
|
<text class="peak-value">{{ peakRange }}</text>
|
|
<text class="peak-value">{{ peakRange }}</text>
|
|
@@ -26,10 +70,10 @@
|
|
|
</view>
|
|
</view>
|
|
|
</view>
|
|
</view>
|
|
|
|
|
|
|
|
- <!-- 群落结构分析 -->
|
|
|
|
|
|
|
+ <!-- 鼠害占比 -->
|
|
|
<view class="analysis-card">
|
|
<view class="analysis-card">
|
|
|
<view class="analysis-card__header">
|
|
<view class="analysis-card__header">
|
|
|
- <view class="analysis-card__title">群落结构分析</view>
|
|
|
|
|
|
|
+ <view class="analysis-card__title">鼠害占比</view>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="community-content">
|
|
<view class="community-content">
|
|
|
<view class="chart-wrap chart-wrap--ring">
|
|
<view class="chart-wrap chart-wrap--ring">
|
|
@@ -41,6 +85,8 @@
|
|
|
:canvas2d="true"
|
|
:canvas2d="true"
|
|
|
:inScrollView="true"
|
|
:inScrollView="true"
|
|
|
:ontouch="true"
|
|
:ontouch="true"
|
|
|
|
|
+ :tooltip-show="false"
|
|
|
|
|
+ @getIndex="onRingIndex"
|
|
|
/>
|
|
/>
|
|
|
</view>
|
|
</view>
|
|
|
<view class="community-total">
|
|
<view class="community-total">
|
|
@@ -52,8 +98,10 @@
|
|
|
<view class="community-list">
|
|
<view class="community-list">
|
|
|
<view
|
|
<view
|
|
|
class="community-item"
|
|
class="community-item"
|
|
|
|
|
+ :class="{ active: communitySelected === index }"
|
|
|
v-for="(item, index) in communityList"
|
|
v-for="(item, index) in communityList"
|
|
|
:key="index"
|
|
:key="index"
|
|
|
|
|
+ @click="onCommunityClick(index)"
|
|
|
>
|
|
>
|
|
|
<view class="community-item__dot" :style="{ background: item.color }"></view>
|
|
<view class="community-item__dot" :style="{ background: item.color }"></view>
|
|
|
<view class="community-item__name">{{ item.name }}</view>
|
|
<view class="community-item__name">{{ item.name }}</view>
|
|
@@ -68,7 +116,7 @@
|
|
|
<view class="analysis-card">
|
|
<view class="analysis-card">
|
|
|
<view class="analysis-card__header">
|
|
<view class="analysis-card__header">
|
|
|
<view class="analysis-card__title">鼠情发生量与气象关系</view>
|
|
<view class="analysis-card__title">鼠情发生量与气象关系</view>
|
|
|
- <view class="range-tabs">
|
|
|
|
|
|
|
+ <!-- <view class="range-tabs">
|
|
|
<view
|
|
<view
|
|
|
class="range-tab"
|
|
class="range-tab"
|
|
|
:class="{ active: weatherRange === 'week' }"
|
|
:class="{ active: weatherRange === 'week' }"
|
|
@@ -84,7 +132,7 @@
|
|
|
:class="{ active: weatherRange === 'year' }"
|
|
:class="{ active: weatherRange === 'year' }"
|
|
|
@click="switchRange('year')"
|
|
@click="switchRange('year')"
|
|
|
>年</view>
|
|
>年</view>
|
|
|
- </view>
|
|
|
|
|
|
|
+ </view> -->
|
|
|
</view>
|
|
</view>
|
|
|
<view class="chart-wrap chart-wrap--weather">
|
|
<view class="chart-wrap chart-wrap--weather">
|
|
|
<qiun-data-charts
|
|
<qiun-data-charts
|
|
@@ -102,39 +150,14 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
-// 鼠情发生量与气象关系 mock 数据
|
|
|
|
|
-const WEATHER_MOCK = {
|
|
|
|
|
- month: {
|
|
|
|
|
- categories: ['8-1','8-16','8-20','8-29','9-1','9-28','10-1','10-28','11-1','11-28','12-9'],
|
|
|
|
|
- pest: [12, 18, 25, 15, 32, 28, 40, 35, 22, 45, 38],
|
|
|
|
|
- temp: [28, 30, 26, 22, 25, 18, 15, 20, 24, 12, 8],
|
|
|
|
|
- humidity: [45, 42, 48, 40, 38, 44, 46, 42, 37, 48, 43]
|
|
|
|
|
- },
|
|
|
|
|
- week: {
|
|
|
|
|
- categories: ['8-12','8-13','8-14','8-15','8-16','8-17','8-18'],
|
|
|
|
|
- pest: [8, 15, 22, 12, 28, 35, 20],
|
|
|
|
|
- temp: [26, 28, 30, 29, 27, 25, 26],
|
|
|
|
|
- humidity: [40, 36, 32, 45, 48, 46, 42]
|
|
|
|
|
- },
|
|
|
|
|
- year: {
|
|
|
|
|
- categories: ['1月','2月','3月','4月','5月','6月','7月','8月','9月','10月','11月','12月'],
|
|
|
|
|
- pest: [35, 28, 22, 18, 15, 20, 32, 45, 48, 42, 38, 30],
|
|
|
|
|
- temp: [5, 8, 14, 20, 26, 30, 32, 31, 26, 20, 12, 6],
|
|
|
|
|
- humidity: [30, 34, 40, 44, 48, 46, 48, 46, 42, 40, 36, 32]
|
|
|
|
|
- }
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
-function buildWeatherChart(range) {
|
|
|
|
|
- const mock = WEATHER_MOCK[range];
|
|
|
|
|
- return {
|
|
|
|
|
- categories: mock.categories,
|
|
|
|
|
- series: [
|
|
|
|
|
- { name: '鼠害总数', type: 'area', style: 'curve', index: 0, data: mock.pest },
|
|
|
|
|
- { name: '温度', type: 'line', style: 'curve', index: 1, data: mock.temp },
|
|
|
|
|
- { name: '湿度', type: 'line', style: 'curve', index: 1, data: mock.humidity }
|
|
|
|
|
- ]
|
|
|
|
|
- };
|
|
|
|
|
-}
|
|
|
|
|
|
|
+// 发生趋势分析 / 鼠情发生量与气象关系 共用接口(按天统计)
|
|
|
|
|
+const ANALYSIS_URL = '/api/api_gateway?method=forecast.shuhai.shuhai_two_camera_photo_day_analysis';
|
|
|
|
|
+// 鼠害活动时段分析 / 鼠害占比 共用接口(按时段统计)
|
|
|
|
|
+const PERIOD_URL = '/api/api_gateway?method=forecast.shuhai.shuhai_two_camera_photo_period_analysis';
|
|
|
|
|
+// 周/月/年 对应的统计天数
|
|
|
|
|
+const RANGE_DAYS = { week: 7, month: 30, year: 365 };
|
|
|
|
|
+// 趋势图配色:系列为 target_detail 里的各物种,数量不定,循环取色
|
|
|
|
|
+const TREND_PALETTE = ['#FF5252', '#0BBC58', '#3D5AFE', '#FFC107', '#9C27B0', '#00BCD4', '#FF7043', '#8BC34A', '#7986CB', '#F06292'];
|
|
|
|
|
|
|
|
export default {
|
|
export default {
|
|
|
name: 'RodentAnalysis',
|
|
name: 'RodentAnalysis',
|
|
@@ -143,12 +166,58 @@ export default {
|
|
|
active: {
|
|
active: {
|
|
|
type: Boolean,
|
|
type: Boolean,
|
|
|
default: true
|
|
default: true
|
|
|
|
|
+ },
|
|
|
|
|
+ // 设备 id:发生趋势 / 气象关系接口入参
|
|
|
|
|
+ deviceId: {
|
|
|
|
|
+ type: [String, Number],
|
|
|
|
|
+ default: ''
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
data() {
|
|
data() {
|
|
|
return {
|
|
return {
|
|
|
- // 活动节律(mock)
|
|
|
|
|
- peakRange: '22:00',
|
|
|
|
|
|
|
+ // 发生趋势
|
|
|
|
|
+ trendRange: 'month',
|
|
|
|
|
+ trendChart: null,
|
|
|
|
|
+ trendOpts: {
|
|
|
|
|
+ // 颜色顺序与 series 一一对应
|
|
|
|
|
+ color: ['#FF5252', '#0BBC58', '#3D5AFE'],
|
|
|
|
|
+ dataLabel: false,
|
|
|
|
|
+ // 不绘制折线上的数据点圆点
|
|
|
|
|
+ dataPointShape: false,
|
|
|
|
|
+ // 图例用卡片内自定义样式(见 template 的 trend-legend)
|
|
|
|
|
+ legend: { show: false },
|
|
|
|
|
+ xAxis: {
|
|
|
|
|
+ disableGrid: true,
|
|
|
|
|
+ fontSize: 9,
|
|
|
|
|
+ fontColor: '#656565',
|
|
|
|
|
+ // 'justify' 点落在刻度线上(线条铺满左右);'center' 点位于两刻度中间
|
|
|
|
|
+ boundaryGap: 'justify',
|
|
|
|
|
+ labelCount: 6
|
|
|
|
|
+ },
|
|
|
|
|
+ yAxis: {
|
|
|
|
|
+ disableGrid: false,
|
|
|
|
|
+ gridType: 'dash',
|
|
|
|
|
+ gridColor: '#EEEEEE',
|
|
|
|
|
+ data: [{
|
|
|
|
|
+ min: 0,
|
|
|
|
|
+ max: 50,
|
|
|
|
|
+ splitNumber: 5,
|
|
|
|
|
+ fontSize: 10,
|
|
|
|
|
+ fontColor: '#656565',
|
|
|
|
|
+ tofix: 0
|
|
|
|
|
+ }]
|
|
|
|
|
+ },
|
|
|
|
|
+ extra: {
|
|
|
|
|
+ line: {
|
|
|
|
|
+ type: 'curve',
|
|
|
|
|
+ width: 2,
|
|
|
|
|
+ activeType: 'none'
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ // 活动节律
|
|
|
|
|
+ peakRange: '--',
|
|
|
rhythmChart: null,
|
|
rhythmChart: null,
|
|
|
rhythmOpts: {
|
|
rhythmOpts: {
|
|
|
color: ['#FFC107'],
|
|
color: ['#FFC107'],
|
|
@@ -190,27 +259,28 @@ export default {
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- // 群落结构(mock)
|
|
|
|
|
- communityList: [
|
|
|
|
|
- { name: '褐家鼠', count: 91, percent: 25, color: '#E53935' },
|
|
|
|
|
- { name: '黄胸鼠', count: 91, percent: 25, color: '#FFC107' },
|
|
|
|
|
- { name: '小家鼠', count: 91, percent: 25, color: '#2196F3' },
|
|
|
|
|
- { name: '黑线姬鼠', count: 91, percent: 25, color: '#9E9E9E' }
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ // 群落结构
|
|
|
|
|
+ communityList: [],
|
|
|
|
|
+ // 当前选中的占比列表项索引(-1 为未选中)
|
|
|
|
|
+ communitySelected: -1,
|
|
|
communityChart: null,
|
|
communityChart: null,
|
|
|
communityOpts: {
|
|
communityOpts: {
|
|
|
color: ['#E53935', '#FFC107', '#2196F3', '#9E9E9E'],
|
|
color: ['#E53935', '#FFC107', '#2196F3', '#9E9E9E'],
|
|
|
|
|
+ // 点击联动会触发整图重绘(canvas 无局部更新),关掉动画避免每次点击重放入场动画
|
|
|
|
|
+ animation: false,
|
|
|
legend: { show: false },
|
|
legend: { show: false },
|
|
|
// 环形图:中心显示当前占比最高的物种
|
|
// 环形图:中心显示当前占比最高的物种
|
|
|
dataLabel: false,
|
|
dataLabel: false,
|
|
|
|
|
+ // uCharts 以 opts.tooltip.index 命中扇区高亮(-1 为不高亮),点击列表时更新
|
|
|
|
|
+ tooltip: { index: -1 },
|
|
|
title: {
|
|
title: {
|
|
|
- name: '25%',
|
|
|
|
|
|
|
+ name: '0%',
|
|
|
fontSize: 16,
|
|
fontSize: 16,
|
|
|
color: '#E53935',
|
|
color: '#E53935',
|
|
|
offsetY: -6
|
|
offsetY: -6
|
|
|
},
|
|
},
|
|
|
subtitle: {
|
|
subtitle: {
|
|
|
- name: '褐家鼠',
|
|
|
|
|
|
|
+ name: '--',
|
|
|
fontSize: 10,
|
|
fontSize: 10,
|
|
|
color: '#333333',
|
|
color: '#333333',
|
|
|
offsetY: 8
|
|
offsetY: 8
|
|
@@ -229,8 +299,8 @@ export default {
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- // 总量(mock)
|
|
|
|
|
- totalCount: 126,
|
|
|
|
|
|
|
+ // 总量
|
|
|
|
|
+ totalCount: 0,
|
|
|
weekOverWeek: '+12.8%',
|
|
weekOverWeek: '+12.8%',
|
|
|
alarmThreshold: 100,
|
|
alarmThreshold: 100,
|
|
|
barMax: 130,
|
|
barMax: 130,
|
|
@@ -268,8 +338,10 @@ export default {
|
|
|
titleOffsetY: -6,
|
|
titleOffsetY: -6,
|
|
|
position: 'left',
|
|
position: 'left',
|
|
|
min: 0,
|
|
min: 0,
|
|
|
|
|
+ max: 10,
|
|
|
splitNumber: 5,
|
|
splitNumber: 5,
|
|
|
- tofix: 0
|
|
|
|
|
|
|
+ // util.toFixed 会把 tofix:0 当 falsy 回退成 2 位小数,改用 formatter 保证整数刻度
|
|
|
|
|
+ formatter: val => Math.round(val) + ''
|
|
|
},
|
|
},
|
|
|
{
|
|
{
|
|
|
title: '湿度:%',
|
|
title: '湿度:%',
|
|
@@ -280,7 +352,7 @@ export default {
|
|
|
position: 'right',
|
|
position: 'right',
|
|
|
min: 0,
|
|
min: 0,
|
|
|
splitNumber: 5,
|
|
splitNumber: 5,
|
|
|
- tofix: 0
|
|
|
|
|
|
|
+ formatter: val => Math.round(val) + ''
|
|
|
}
|
|
}
|
|
|
]
|
|
]
|
|
|
},
|
|
},
|
|
@@ -297,6 +369,11 @@ export default {
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
computed: {
|
|
computed: {
|
|
|
|
|
+ // 趋势图图例:跟随接口返回的物种系列
|
|
|
|
|
+ trendLegend() {
|
|
|
|
|
+ if (!this.trendChart) return [];
|
|
|
|
|
+ return this.trendChart.series.map(s => ({ name: s.name, color: s.color }));
|
|
|
|
|
+ },
|
|
|
// 进度条填充:总量/量程
|
|
// 进度条填充:总量/量程
|
|
|
fillPercent() {
|
|
fillPercent() {
|
|
|
return Math.min((this.totalCount / this.barMax) * 100, 100);
|
|
return Math.min((this.totalCount / this.barMax) * 100, 100);
|
|
@@ -312,32 +389,214 @@ export default {
|
|
|
this.initChartData();
|
|
this.initChartData();
|
|
|
}, 300);
|
|
}, 300);
|
|
|
},
|
|
},
|
|
|
|
|
+ watch: {
|
|
|
|
|
+ // 设备信息晚于组件挂载到达时重新拉取
|
|
|
|
|
+ deviceId(val) {
|
|
|
|
|
+ if (!val) return;
|
|
|
|
|
+ this.fetchAnalysis(this.trendRange, 'trend');
|
|
|
|
|
+ this.fetchAnalysis(this.weatherRange, 'weather');
|
|
|
|
|
+ this.fetchPeriod();
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
methods: {
|
|
methods: {
|
|
|
initChartData() {
|
|
initChartData() {
|
|
|
|
|
+ this.fetchAnalysis(this.trendRange, 'trend');
|
|
|
|
|
+ this.fetchAnalysis(this.weatherRange, 'weather');
|
|
|
|
|
+ this.fetchPeriod();
|
|
|
|
|
+ },
|
|
|
|
|
+ switchRange(range) {
|
|
|
|
|
+ if (this.weatherRange === range) return;
|
|
|
|
|
+ this.weatherRange = range;
|
|
|
|
|
+ this.fetchAnalysis(range, 'weather');
|
|
|
|
|
+ },
|
|
|
|
|
+ switchTrendRange(range) {
|
|
|
|
|
+ if (this.trendRange === range) return;
|
|
|
|
|
+ this.trendRange = range;
|
|
|
|
|
+ this.fetchAnalysis(range, 'trend');
|
|
|
|
|
+ },
|
|
|
|
|
+ // 拉取按天分析数据,chartKey 区分构建趋势图还是气象图
|
|
|
|
|
+ async fetchAnalysis(range, chartKey) {
|
|
|
|
|
+ if (!this.deviceId) return;
|
|
|
|
|
+ const { start, end } = this.getRangeTime(range);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await this.$myRequest({
|
|
|
|
|
+ url: ANALYSIS_URL,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ device_id: this.deviceId,
|
|
|
|
|
+ start_time: start,
|
|
|
|
|
+ end_time: end
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ const dayList = (res && res.day_list) || [];
|
|
|
|
|
+ if (!dayList.length) return;
|
|
|
|
|
+ if (chartKey === 'trend') {
|
|
|
|
|
+ this.trendChart = this.buildTrendChart(dayList);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ this.weatherChart = this.buildWeatherChart(dayList);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // $myRequest 已统一 toast 错误信息
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 周/月/年 对应的起止时间戳(秒)
|
|
|
|
|
+ getRangeTime(range) {
|
|
|
|
|
+ const end = Math.floor(Date.now() / 1000);
|
|
|
|
|
+ const start = end - (RANGE_DAYS[range] || 30) * 24 * 60 * 60;
|
|
|
|
|
+ return { start, end };
|
|
|
|
|
+ },
|
|
|
|
|
+ // 发生趋势:x 轴为日期,系列为 target_detail 里的各物种
|
|
|
|
|
+ buildTrendChart(dayList) {
|
|
|
|
|
+ const categories = [];
|
|
|
|
|
+ const detailRows = [];
|
|
|
|
|
+ dayList.forEach(day => {
|
|
|
|
|
+ categories.push((day.date || '').slice(5));
|
|
|
|
|
+ detailRows.push(day.target_detail || {});
|
|
|
|
|
+ });
|
|
|
|
|
+ // 汇总所有出现过的物种(按首次出现顺序)
|
|
|
|
|
+ const names = [];
|
|
|
|
|
+ detailRows.forEach(row => {
|
|
|
|
|
+ Object.keys(row).forEach(name => {
|
|
|
|
|
+ if (names.indexOf(name) === -1) names.push(name);
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ const series = names.map((name, index) => ({
|
|
|
|
|
+ name: name,
|
|
|
|
|
+ color: TREND_PALETTE[index % TREND_PALETTE.length],
|
|
|
|
|
+ data: detailRows.map(row => row[name] || 0)
|
|
|
|
|
+ }));
|
|
|
|
|
+ // Y 轴上限随数据量调整,向上取整到 10 的倍数
|
|
|
|
|
+ let maxVal = 0;
|
|
|
|
|
+ series.forEach(s => {
|
|
|
|
|
+ s.data.forEach(v => {
|
|
|
|
|
+ if (v > maxVal) maxVal = v;
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+ this.trendOpts.yAxis.data[0].max = Math.max(10, Math.ceil(maxVal / 10) * 10);
|
|
|
|
|
+ // 单天数据画不出折线,改为显示数据点;boundaryGap 切为 'center' 让点位/刻度居中,多天时 'justify' 贴边铺满
|
|
|
|
|
+ this.trendOpts.dataPointShape = dayList.length === 1;
|
|
|
|
|
+ this.trendOpts.xAxis.boundaryGap = dayList.length === 1 ? 'center' : 'justify';
|
|
|
|
|
+ return { categories, series };
|
|
|
|
|
+ },
|
|
|
|
|
+ // 气象关系:鼠害总数 + 温度 + 湿度
|
|
|
|
|
+ buildWeatherChart(dayList) {
|
|
|
|
|
+ // mix 图单点数据画不出折线,addPoint 让其显示为数据点
|
|
|
|
|
+ const single = dayList.length === 1;
|
|
|
|
|
+ // 左轴上限向上取整到 5 的倍数,保证 5 等分后刻度为整数
|
|
|
|
|
+ let maxTotal = 0;
|
|
|
|
|
+ dayList.forEach(day => {
|
|
|
|
|
+ if ((day.total || 0) > maxTotal) maxTotal = day.total || 0;
|
|
|
|
|
+ });
|
|
|
|
|
+ this.weatherOpts.yAxis.data[0].max = Math.max(10, Math.ceil(maxTotal / 5) * 5);
|
|
|
|
|
+ return {
|
|
|
|
|
+ categories: dayList.map(day => (day.date || '').slice(5)),
|
|
|
|
|
+ series: [
|
|
|
|
|
+ { name: '鼠害总数', type: 'area', style: 'curve', index: 0, addPoint: single, data: dayList.map(day => day.total || 0) },
|
|
|
|
|
+ { name: '温度', type: 'line', style: 'curve', index: 1, addPoint: single, data: dayList.map(day => day.temperature || 0) },
|
|
|
|
|
+ { name: '湿度', type: 'line', style: 'curve', index: 1, addPoint: single, data: dayList.map(day => day.humidity || 0) }
|
|
|
|
|
+ ]
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+ // 拉取时段分析数据(活动时段 + 鼠害占比共用),默认统计近 30 天
|
|
|
|
|
+ async fetchPeriod() {
|
|
|
|
|
+ if (!this.deviceId) return;
|
|
|
|
|
+ const { start, end } = this.getRangeTime('month');
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await this.$myRequest({
|
|
|
|
|
+ url: PERIOD_URL,
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ data: {
|
|
|
|
|
+ device_id: this.deviceId,
|
|
|
|
|
+ start_time: start,
|
|
|
|
|
+ end_time: end
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ if (!res) return;
|
|
|
|
|
+ this.buildRhythmChart(res.period_list || []);
|
|
|
|
|
+ this.buildCommunity(res.total || 0, res.target_detail || {});
|
|
|
|
|
+ } catch (e) {
|
|
|
|
|
+ // $myRequest 已统一 toast 错误信息
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+ // 活动时段:meter 柱状图,系列[0]为背景轨道,系列[1]为各时段数量
|
|
|
|
|
+ buildRhythmChart(periodList) {
|
|
|
|
|
+ if (!periodList.length) return;
|
|
|
|
|
+ const categories = [];
|
|
|
|
|
+ const totals = [];
|
|
|
|
|
+ let maxTotal = 0;
|
|
|
|
|
+ let peakIndex = -1;
|
|
|
|
|
+ periodList.forEach((item, index) => {
|
|
|
|
|
+ categories.push(this.formatHour(item.hour));
|
|
|
|
|
+ const total = item.total || 0;
|
|
|
|
|
+ totals.push(total);
|
|
|
|
|
+ if (total > maxTotal) {
|
|
|
|
|
+ maxTotal = total;
|
|
|
|
|
+ peakIndex = index;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ // 活跃高峰:数量最多的时段
|
|
|
|
|
+ this.peakRange = peakIndex > -1 ? (periodList[peakIndex].period || this.formatHour(periodList[peakIndex].hour)) : '--';
|
|
|
|
|
+ // 量程随数据调整,背景轨道与量程等高
|
|
|
|
|
+ const rangeMax = Math.max(10, Math.ceil(maxTotal / 10) * 10);
|
|
|
|
|
+ this.rhythmOpts.yAxis.data[0].max = rangeMax;
|
|
|
this.rhythmChart = {
|
|
this.rhythmChart = {
|
|
|
- categories: ['00:00','02:00','04:00','06:00','08:00','10:00','12:00','14:00','16:00','18:00','20:00','22:00','24:00'],
|
|
|
|
|
|
|
+ categories,
|
|
|
series: [
|
|
series: [
|
|
|
- // 背景轨道:满量程高度(meter 模式下系列[0]用 meterFillColor 填充)
|
|
|
|
|
- { name: '背景', color: '#D6DBED', data: Array(13).fill(100) },
|
|
|
|
|
- { name: '鼠害数量', color: '#FFC107', data: [42, 58, 66, 48, 22, 12, 8, 15, 28, 45, 95, 88, 52] }
|
|
|
|
|
|
|
+ { name: '背景', color: '#D6DBED', data: periodList.map(() => rangeMax) },
|
|
|
|
|
+ { name: '鼠害数量', color: '#FFC107', data: totals }
|
|
|
]
|
|
]
|
|
|
};
|
|
};
|
|
|
|
|
+ },
|
|
|
|
|
+ // 0 → '00:00'
|
|
|
|
|
+ formatHour(hour) {
|
|
|
|
|
+ const h = hour || 0;
|
|
|
|
|
+ return (h < 10 ? '0' + h : '' + h) + ':00';
|
|
|
|
|
+ },
|
|
|
|
|
+ // 鼠害占比:环形图 + 列表,物种来自 target_detail
|
|
|
|
|
+ buildCommunity(total, detail) {
|
|
|
|
|
+ const names = Object.keys(detail);
|
|
|
|
|
+ const list = names.map((name, index) => {
|
|
|
|
|
+ const count = detail[name] || 0;
|
|
|
|
|
+ return {
|
|
|
|
|
+ name: name,
|
|
|
|
|
+ count: count,
|
|
|
|
|
+ percent: total ? Math.round((count / total) * 100) : 0,
|
|
|
|
|
+ color: TREND_PALETTE[index % TREND_PALETTE.length]
|
|
|
|
|
+ };
|
|
|
|
|
+ });
|
|
|
|
|
+ this.communityList = list;
|
|
|
|
|
+ this.communitySelected = -1;
|
|
|
|
|
+ this.totalCount = total;
|
|
|
|
|
+ this.communityOpts.tooltip = { index: -1 };
|
|
|
this.communityChart = {
|
|
this.communityChart = {
|
|
|
series: [{
|
|
series: [{
|
|
|
- data: [
|
|
|
|
|
- { name: '褐家鼠', value: 91 },
|
|
|
|
|
- { name: '黄胸鼠', value: 91 },
|
|
|
|
|
- { name: '小家鼠', value: 91 },
|
|
|
|
|
- { name: '黑线姬鼠', value: 91 }
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ data: list.map(item => ({ name: item.name, value: item.count }))
|
|
|
}]
|
|
}]
|
|
|
};
|
|
};
|
|
|
- this.weatherChart = buildWeatherChart(this.weatherRange);
|
|
|
|
|
|
|
+ // 环形中心显示占比最高的物种
|
|
|
|
|
+ if (list.length) {
|
|
|
|
|
+ const top = list.reduce((a, b) => (b.count > a.count ? b : a), list[0]);
|
|
|
|
|
+ this.communityOpts.title.name = (total ? Math.round((top.count / total) * 100) : 0) + '%';
|
|
|
|
|
+ this.communityOpts.subtitle.name = top.name;
|
|
|
|
|
+ }
|
|
|
},
|
|
},
|
|
|
- switchRange(range) {
|
|
|
|
|
- if (this.weatherRange === range) return;
|
|
|
|
|
- this.weatherRange = range;
|
|
|
|
|
- this.weatherChart = buildWeatherChart(range);
|
|
|
|
|
|
|
+ // 点击占比列表:环形图高亮对应扇区,中心数据联动;再次点击取消选中
|
|
|
|
|
+ onCommunityClick(index) {
|
|
|
|
|
+ const item = this.communityList[index];
|
|
|
|
|
+ if (!item) return;
|
|
|
|
|
+ const next = this.communitySelected === index ? -1 : index;
|
|
|
|
|
+ this.communitySelected = next;
|
|
|
|
|
+ const target = next > -1
|
|
|
|
|
+ ? item
|
|
|
|
|
+ : this.communityList.reduce((a, b) => (b.count > a.count ? b : a), this.communityList[0]);
|
|
|
|
|
+ this.communityOpts.title.name = target.percent + '%';
|
|
|
|
|
+ this.communityOpts.subtitle.name = target.name;
|
|
|
|
|
+ this.communityOpts.tooltip = { index: next };
|
|
|
|
|
+ },
|
|
|
|
|
+ // 点击环形图扇区:中心数据联动,复用列表点击逻辑
|
|
|
|
|
+ onRingIndex(e) {
|
|
|
|
|
+ if (!e || typeof e.currentIndex !== 'number' || e.currentIndex < 0) return;
|
|
|
|
|
+ this.onCommunityClick(e.currentIndex);
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
@@ -420,6 +679,55 @@ export default {
|
|
|
.chart-wrap--weather {
|
|
.chart-wrap--weather {
|
|
|
height: 420rpx;
|
|
height: 420rpx;
|
|
|
}
|
|
}
|
|
|
|
|
+// 发生趋势分析:自定义图例(曲线图例:短线 + 空心圆点)
|
|
|
|
|
+.trend-legend {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
|
|
+ background: #F5F6FA;
|
|
|
|
|
+ border-radius: 12rpx;
|
|
|
|
|
+ margin-bottom: 20rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.trend-legend__item {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ width: 33.33%;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ padding: 8rpx 0;
|
|
|
|
|
+}
|
|
|
|
|
+.trend-legend__line {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ width: 40rpx;
|
|
|
|
|
+ height: 4rpx;
|
|
|
|
|
+ margin-right: 12rpx;
|
|
|
|
|
+ flex-shrink: 0;
|
|
|
|
|
+}
|
|
|
|
|
+.trend-legend__dot {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ left: 50%;
|
|
|
|
|
+ top: 50%;
|
|
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
|
|
+ width: 16rpx;
|
|
|
|
|
+ height: 16rpx;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ border: 4rpx solid #fff;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+.trend-legend__text {
|
|
|
|
|
+ font-size: 26rpx;
|
|
|
|
|
+ color: #042118;
|
|
|
|
|
+ font-family: 'Source Han Sans CN VF';
|
|
|
|
|
+}
|
|
|
|
|
+.trend-axis-label {
|
|
|
|
|
+ font-size: 22rpx;
|
|
|
|
|
+ color: #B3B9C2;
|
|
|
|
|
+ font-family: 'Source Han Sans CN VF';
|
|
|
|
|
+ margin-bottom: 4rpx;
|
|
|
|
|
+}
|
|
|
|
|
+.chart-wrap--trend {
|
|
|
|
|
+ height: 420rpx;
|
|
|
|
|
+}
|
|
|
// 群落结构:上下布局,环形图在上,图例列表在下
|
|
// 群落结构:上下布局,环形图在上,图例列表在下
|
|
|
.community-content {
|
|
.community-content {
|
|
|
display: flex;
|
|
display: flex;
|
|
@@ -452,8 +760,12 @@ export default {
|
|
|
.community-item {
|
|
.community-item {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- padding: 12rpx 0;
|
|
|
|
|
|
|
+ padding: 12rpx 12rpx;
|
|
|
|
|
+ border-radius: 8rpx;
|
|
|
font-family: 'Source Han Sans CN VF';
|
|
font-family: 'Source Han Sans CN VF';
|
|
|
|
|
+ &.active {
|
|
|
|
|
+ background: #F5F6FA;
|
|
|
|
|
+ }
|
|
|
.community-item__dot {
|
|
.community-item__dot {
|
|
|
width: 16rpx;
|
|
width: 16rpx;
|
|
|
height: 16rpx;
|
|
height: 16rpx;
|