rodentAnalysis.vue 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918
  1. <template>
  2. <view class="rodent-analysis">
  3. <!-- 发生趋势分析 -->
  4. <view class="analysis-card">
  5. <view class="analysis-card__header">
  6. <view class="analysis-card__title">发生趋势分析</view>
  7. <!-- <view class="range-tabs">
  8. <view
  9. class="range-tab"
  10. :class="{ active: trendRange === 'week' }"
  11. @click="switchTrendRange('week')"
  12. >周</view>
  13. <view
  14. class="range-tab"
  15. :class="{ active: trendRange === 'month' }"
  16. @click="switchTrendRange('month')"
  17. >月</view>
  18. <view
  19. class="range-tab"
  20. :class="{ active: trendRange === 'year' }"
  21. @click="switchTrendRange('year')"
  22. >年</view>
  23. </view> -->
  24. </view>
  25. <view class="trend-legend">
  26. <view class="trend-legend__item" v-for="item in trendLegend" :key="item.name">
  27. <view class="trend-legend__line" :style="{ background: item.color }">
  28. <view class="trend-legend__dot" :style="{ borderColor: item.color }"></view>
  29. </view>
  30. <text class="trend-legend__text">{{ item.name }}</text>
  31. </view>
  32. </view>
  33. <view class="trend-axis-label">鼠害数量:只</view>
  34. <view class="chart-wrap chart-wrap--trend" v-if="!trendEmpty">
  35. <qiun-data-charts
  36. type="line"
  37. :chartData="trendChart"
  38. :opts="trendOpts"
  39. :reshow="active"
  40. :canvas2d="true"
  41. :inScrollView="true"
  42. :ontouch="true"
  43. />
  44. </view>
  45. <view v-else class="chart-empty">暂无数据</view>
  46. </view>
  47. <!-- 鼠害活动时段分析 -->
  48. <view class="analysis-card">
  49. <view class="analysis-card__header">
  50. <view class="analysis-card__title">鼠害活动时段分析</view>
  51. <view class="analysis-card__extra">
  52. <text class="peak-label">活跃高峰</text>
  53. <text class="peak-value">{{ peakRange }}</text>
  54. </view>
  55. </view>
  56. <view class="chart-legend">
  57. <view class="chart-legend__dot chart-legend__dot--bar"></view>
  58. <text class="chart-legend__text">鼠害数量</text>
  59. </view>
  60. <view class="chart-wrap chart-wrap--rhythm" v-if="!rhythmEmpty">
  61. <qiun-data-charts
  62. type="column"
  63. :chartData="rhythmChart"
  64. :opts="rhythmOpts"
  65. :reshow="active"
  66. :canvas2d="true"
  67. :inScrollView="true"
  68. :ontouch="true"
  69. />
  70. </view>
  71. <view v-else class="chart-empty">暂无数据</view>
  72. </view>
  73. <!-- 鼠害占比 -->
  74. <view class="analysis-card">
  75. <view class="analysis-card__header">
  76. <view class="analysis-card__title">鼠害占比</view>
  77. </view>
  78. <view class="community-content" v-if="!communityEmpty">
  79. <view class="chart-wrap chart-wrap--ring">
  80. <qiun-data-charts
  81. type="ring"
  82. :chartData="communityChart"
  83. :opts="communityOpts"
  84. :reshow="active"
  85. :canvas2d="true"
  86. :inScrollView="true"
  87. :ontouch="true"
  88. :tooltip-show="false"
  89. @getIndex="onRingIndex"
  90. />
  91. </view>
  92. <view class="community-total">
  93. <text>总鼠害数量</text>
  94. <text class="total-value">{{ totalCount }}
  95. <text class="unit">只</text>
  96. </text>
  97. </view>
  98. <view class="community-list">
  99. <view
  100. class="community-item"
  101. :class="{ active: communitySelected === index }"
  102. v-for="(item, index) in communityList"
  103. :key="index"
  104. @click="onCommunityClick(index)"
  105. >
  106. <view class="community-item__dot" :style="{ background: item.color }"></view>
  107. <view class="community-item__name">{{ item.name }}</view>
  108. <view class="community-item__count">{{ item.count }}只</view>
  109. <view class="community-item__percent">{{ item.percent }}%</view>
  110. </view>
  111. </view>
  112. </view>
  113. <view v-else class="chart-empty">暂无数据</view>
  114. </view>
  115. <!-- 鼠情发生量与气象关系 -->
  116. <view class="analysis-card">
  117. <view class="analysis-card__header">
  118. <view class="analysis-card__title">鼠情发生量与气象关系</view>
  119. <!-- <view class="range-tabs">
  120. <view
  121. class="range-tab"
  122. :class="{ active: weatherRange === 'week' }"
  123. @click="switchRange('week')"
  124. >周</view>
  125. <view
  126. class="range-tab"
  127. :class="{ active: weatherRange === 'month' }"
  128. @click="switchRange('month')"
  129. >月</view>
  130. <view
  131. class="range-tab"
  132. :class="{ active: weatherRange === 'year' }"
  133. @click="switchRange('year')"
  134. >年</view>
  135. </view> -->
  136. </view>
  137. <view class="chart-wrap chart-wrap--weather" v-if="!weatherEmpty">
  138. <qiun-data-charts
  139. type="mix"
  140. :chartData="weatherChart"
  141. :opts="weatherOpts"
  142. :reshow="active"
  143. :canvas2d="true"
  144. :inScrollView="true"
  145. :ontouch="true"
  146. />
  147. </view>
  148. <view v-else class="chart-empty">暂无数据</view>
  149. </view>
  150. </view>
  151. </template>
  152. <script>
  153. // 发生趋势分析 / 鼠情发生量与气象关系 共用接口(按天统计)
  154. const ANALYSIS_URL = '/api/api_gateway?method=forecast.shuhai.shuhai_two_camera_photo_day_analysis';
  155. // 鼠害活动时段分析 / 鼠害占比 共用接口(按时段统计)
  156. const PERIOD_URL = '/api/api_gateway?method=forecast.shuhai.shuhai_two_camera_photo_period_analysis';
  157. // 周/月/年 对应的统计天数
  158. const RANGE_DAYS = { week: 7, month: 30, year: 365 };
  159. // 趋势图配色:系列为 target_detail 里的各物种,数量不定,循环取色
  160. const TREND_PALETTE = ['#FF5252', '#0BBC58', '#3D5AFE', '#FFC107', '#9C27B0', '#00BCD4', '#FF7043', '#8BC34A', '#7986CB', '#F06292'];
  161. export default {
  162. name: 'RodentAnalysis',
  163. props: {
  164. // 当前 tab 是否激活(tab 用 v-show 切换时,隐藏状态下 canvas 容器测量为 0,需激活后重绘)
  165. active: {
  166. type: Boolean,
  167. default: true
  168. },
  169. // 设备 id:发生趋势 / 气象关系由页面日期选择驱动
  170. deviceId: {
  171. type: [String, Number],
  172. default: ''
  173. },
  174. // 查询范围(秒级时间戳),由页面日期选择驱动
  175. start: {
  176. type: [String, Number],
  177. default: 0
  178. },
  179. end: {
  180. type: [String, Number],
  181. default: 0
  182. }
  183. },
  184. data() {
  185. return {
  186. // 发生趋势
  187. trendChart: null,
  188. // 无数据空态:为 true 时不渲染对应图表(空 series 会让 qiun-data-charts 一直转 loading)
  189. trendEmpty: false,
  190. trendOpts: {
  191. // 颜色顺序与 series 一一对应
  192. color: ['#FF5252', '#0BBC58', '#3D5AFE'],
  193. dataLabel: false,
  194. // 不绘制折线上的数据点圆点
  195. dataPointShape: false,
  196. // 图例用卡片内自定义样式(见 template 的 trend-legend)
  197. legend: { show: false },
  198. xAxis: {
  199. disableGrid: true,
  200. fontSize: 9,
  201. fontColor: '#656565',
  202. // 'justify' 点落在刻度线上(线条铺满左右);'center' 点位于两刻度中间
  203. boundaryGap: 'justify',
  204. labelCount: 6
  205. },
  206. yAxis: {
  207. disableGrid: false,
  208. gridType: 'dash',
  209. gridColor: '#EEEEEE',
  210. data: [{
  211. min: 0,
  212. max: 50,
  213. splitNumber: 5,
  214. fontSize: 10,
  215. fontColor: '#656565',
  216. tofix: 0
  217. }]
  218. },
  219. extra: {
  220. line: {
  221. type: 'curve',
  222. width: 2,
  223. activeType: 'none'
  224. }
  225. }
  226. },
  227. // 活动节律
  228. peakRange: '--',
  229. rhythmChart: null,
  230. rhythmEmpty: false,
  231. rhythmOpts: {
  232. color: ['#FFC107'],
  233. dataLabel: false,
  234. // 背景轨道系列会进 uCharts 图例,改用卡片内自定义图例(见 template 的 chart-legend)
  235. legend: { show: false },
  236. xAxis: {
  237. disableGrid: true,
  238. fontSize: 9,
  239. fontColor: '#656565'
  240. },
  241. yAxis: {
  242. showTitle: true,
  243. disableGrid: false,
  244. gridType: 'dash',
  245. gridColor: '#EEEEEE',
  246. data: [{
  247. title: '数量:只',
  248. titleFontSize: 10,
  249. titleFontColor: '#656565',
  250. titleOffsetX: -10,
  251. titleOffsetY: -6,
  252. min: 0,
  253. max: 100,
  254. splitNumber: 5,
  255. tofix: 0
  256. }]
  257. },
  258. extra: {
  259. column: {
  260. // 温度计模式:系列[0]绘制为整柱浅色背景轨道,系列[1]叠加实际数据柱
  261. type: 'meter',
  262. meterBorder: 0,
  263. meterFillColor: '#d6dbed66',
  264. width: 14,
  265. barBorderCircle: false,
  266. linearType: 'none'
  267. }
  268. }
  269. },
  270. // 群落结构
  271. communityList: [],
  272. // 当前选中的占比列表项索引(-1 为未选中)
  273. communitySelected: -1,
  274. communityChart: null,
  275. communityEmpty: false,
  276. communityOpts: {
  277. color: ['#E53935', '#FFC107', '#2196F3', '#9E9E9E'],
  278. // 点击联动会触发整图重绘(canvas 无局部更新),关掉动画避免每次点击重放入场动画
  279. animation: false,
  280. legend: { show: false },
  281. // 环形图:中心显示当前占比最高的物种
  282. dataLabel: false,
  283. // uCharts 以 opts.tooltip.index 命中扇区高亮(-1 为不高亮),点击列表时更新
  284. tooltip: { index: -1 },
  285. title: {
  286. name: '0%',
  287. fontSize: 16,
  288. color: '#E53935',
  289. offsetY: -6
  290. },
  291. subtitle: {
  292. name: '--',
  293. fontSize: 10,
  294. color: '#333333',
  295. offsetY: 8
  296. },
  297. extra: {
  298. ring: {
  299. // 环粗细约为半径的三分之一
  300. ringWidth: 16,
  301. offsetAngle: 0,
  302. // 去掉分段之间的白色分隔线(qiun-data-charts 的 ring 默认 border: true)
  303. border: false,
  304. // 关闭点击分段时外扩+半透明的高亮效果(默认 activeRadius: 10 / activeOpacity: 0.5)
  305. activeRadius: 5,
  306. activeOpacity: 1
  307. }
  308. }
  309. },
  310. // 总量
  311. totalCount: 0,
  312. weekOverWeek: '+12.8%',
  313. alarmThreshold: 100,
  314. barMax: 130,
  315. // 鼠情与气象
  316. weatherChart: null,
  317. weatherEmpty: false,
  318. weatherOpts: {
  319. color: ['#FF5252', '#FFC107', '#2196F3'],
  320. dataLabel: false,
  321. legend: {
  322. show: true,
  323. position: 'bottom',
  324. fontSize: 10,
  325. fontColor: '#656565',
  326. itemGap: 15,
  327. lineHeight: 15
  328. },
  329. xAxis: {
  330. disableGrid: true,
  331. fontSize: 9,
  332. fontColor: '#656565'
  333. },
  334. yAxis: {
  335. showTitle: true,
  336. disableGrid: false,
  337. gridType: 'dash',
  338. gridColor: '#EEEEEE',
  339. data: [
  340. {
  341. title: '鼠害数量:只',
  342. titleFontSize: 10,
  343. titleFontColor: '#656565',
  344. titleOffsetX: -10,
  345. titleOffsetY: -6,
  346. position: 'left',
  347. min: 0,
  348. max: 10,
  349. splitNumber: 5,
  350. // util.toFixed 会把 tofix:0 当 falsy 回退成 2 位小数,改用 formatter 保证整数刻度
  351. formatter: val => Math.round(val) + ''
  352. },
  353. {
  354. title: '湿度:%',
  355. titleFontSize: 10,
  356. titleFontColor: '#656565',
  357. titleOffsetX: 10,
  358. titleOffsetY: -6,
  359. position: 'right',
  360. min: 0,
  361. splitNumber: 5,
  362. formatter: val => Math.round(val) + ''
  363. }
  364. ]
  365. },
  366. extra: {
  367. mix: {
  368. line: { width: 2 },
  369. area: { opacity: 0.15, gradient: true }
  370. },
  371. tooltip: {
  372. showBox: true
  373. }
  374. }
  375. }
  376. };
  377. },
  378. computed: {
  379. // 设备或日期范围任一变化即触发重拉
  380. fetchKey() {
  381. return [this.deviceId, this.start, this.end].join('|');
  382. },
  383. // 趋势图图例:跟随接口返回的物种系列
  384. trendLegend() {
  385. if (!this.trendChart) return [];
  386. return this.trendChart.series.map(s => ({ name: s.name, color: s.color }));
  387. },
  388. // 进度条填充:总量/量程
  389. fillPercent() {
  390. return Math.min((this.totalCount / this.barMax) * 100, 100);
  391. },
  392. // 告警阈值标记位置
  393. thresholdPercent() {
  394. return (this.alarmThreshold / this.barMax) * 100;
  395. }
  396. },
  397. mounted() {
  398. // 延迟赋值图表数据:等页面布局稳定后再初始化 canvas,避免容器尺寸测量为 0
  399. setTimeout(() => {
  400. this.initChartData();
  401. }, 300);
  402. },
  403. watch: {
  404. // 设备或日期范围变化时重新拉取
  405. fetchKey() {
  406. this.initChartData();
  407. }
  408. },
  409. methods: {
  410. initChartData() {
  411. this.fetchAnalysis('trend');
  412. this.fetchAnalysis('weather');
  413. this.fetchPeriod();
  414. },
  415. switchRange(range) {
  416. if (this.weatherRange === range) return;
  417. this.weatherRange = range;
  418. this.fetchAnalysis(range, 'weather');
  419. },
  420. switchTrendRange(range) {
  421. if (this.trendRange === range) return;
  422. this.trendRange = range;
  423. this.fetchAnalysis(range, 'trend');
  424. },
  425. // 拉取按天分析数据,chartKey 区分构建趋势图还是气象图
  426. async fetchAnalysis(chartKey) {
  427. if (!this.deviceId || !this.start || !this.end) return;
  428. try {
  429. const res = await this.$myRequest({
  430. url: ANALYSIS_URL,
  431. method: 'POST',
  432. data: {
  433. device_id: this.deviceId,
  434. start_time: this.start,
  435. end_time: this.end
  436. }
  437. });
  438. const dayList = (res && res.day_list) || [];
  439. if (chartKey === 'trend') {
  440. // 无数据时清空旧图,显示空态(不再保留上一次的数据)
  441. this.trendEmpty = !dayList.length;
  442. this.trendChart = dayList.length ? this.buildTrendChart(dayList) : null;
  443. } else {
  444. this.weatherEmpty = !dayList.length;
  445. this.weatherChart = dayList.length ? this.buildWeatherChart(dayList) : null;
  446. }
  447. } catch (e) {
  448. // $myRequest 已统一 toast 错误信息
  449. }
  450. },
  451. // 周/月/年 对应的起止时间戳(秒)
  452. getRangeTime(range) {
  453. const end = Math.floor(Date.now() / 1000);
  454. const start = end - (RANGE_DAYS[range] || 30) * 24 * 60 * 60;
  455. return { start, end };
  456. },
  457. // 发生趋势:x 轴为日期,系列为 target_detail 里的各物种
  458. buildTrendChart(dayList) {
  459. const categories = [];
  460. const detailRows = [];
  461. dayList.forEach(day => {
  462. categories.push((day.date || '').slice(5));
  463. detailRows.push(day.target_detail || {});
  464. });
  465. // 汇总所有出现过的物种(按首次出现顺序)
  466. const names = [];
  467. detailRows.forEach(row => {
  468. Object.keys(row).forEach(name => {
  469. if (names.indexOf(name) === -1) names.push(name);
  470. });
  471. });
  472. const series = names.map((name, index) => ({
  473. name: name,
  474. color: TREND_PALETTE[index % TREND_PALETTE.length],
  475. data: detailRows.map(row => row[name] || 0)
  476. }));
  477. // Y 轴上限随数据量调整,向上取整到 10 的倍数
  478. let maxVal = 0;
  479. series.forEach(s => {
  480. s.data.forEach(v => {
  481. if (v > maxVal) maxVal = v;
  482. });
  483. });
  484. this.trendOpts.yAxis.data[0].max = Math.max(10, Math.ceil(maxVal / 10) * 10);
  485. // 单天数据画不出折线,改为显示数据点;boundaryGap 切为 'center' 让点位/刻度居中,多天时 'justify' 贴边铺满
  486. this.trendOpts.dataPointShape = dayList.length === 1;
  487. this.trendOpts.xAxis.boundaryGap = dayList.length === 1 ? 'center' : 'justify';
  488. return { categories, series };
  489. },
  490. // 气象关系:鼠害总数 + 温度 + 湿度
  491. buildWeatherChart(dayList) {
  492. // mix 图单点数据画不出折线,addPoint 让其显示为数据点
  493. const single = dayList.length === 1;
  494. // 左轴上限向上取整到 5 的倍数,保证 5 等分后刻度为整数
  495. let maxTotal = 0;
  496. dayList.forEach(day => {
  497. if ((day.total || 0) > maxTotal) maxTotal = day.total || 0;
  498. });
  499. this.weatherOpts.yAxis.data[0].max = Math.max(10, Math.ceil(maxTotal / 5) * 5);
  500. return {
  501. categories: dayList.map(day => (day.date || '').slice(5)),
  502. series: [
  503. // area 系列图例默认画三角形,指定 legendShape 为 line(15x4 的扁长方形),与温度/湿度的图例同高
  504. { name: '鼠害总数', type: 'area', style: 'curve', index: 0, addPoint: single, legendShape: 'line', data: dayList.map(day => day.total || 0) },
  505. { name: '温度', type: 'line', style: 'curve', index: 1, addPoint: single, data: dayList.map(day => day.temperature || 0) },
  506. { name: '湿度', type: 'line', style: 'curve', index: 1, addPoint: single, data: dayList.map(day => day.humidity || 0) }
  507. ]
  508. };
  509. },
  510. // 拉取时段分析数据(活动时段 + 鼠害占比共用)
  511. async fetchPeriod() {
  512. if (!this.deviceId || !this.start || !this.end) return;
  513. try {
  514. const res = await this.$myRequest({
  515. url: PERIOD_URL,
  516. method: 'POST',
  517. data: {
  518. device_id: this.deviceId,
  519. start_time: this.start,
  520. end_time: this.end
  521. }
  522. });
  523. if (!res) return;
  524. this.buildRhythmChart(res.period_list || []);
  525. this.buildCommunity(res.total || 0, res.target_detail || {});
  526. } catch (e) {
  527. // $myRequest 已统一 toast 错误信息
  528. }
  529. },
  530. // 活动时段:meter 柱状图,系列[0]为背景轨道,系列[1]为各时段数量
  531. buildRhythmChart(periodList) {
  532. if (!periodList.length) {
  533. // 无数据时清空旧图,显示空态(不再保留上一次的数据)
  534. this.rhythmEmpty = true;
  535. this.rhythmChart = null;
  536. return;
  537. }
  538. this.rhythmEmpty = false;
  539. const categories = [];
  540. const totals = [];
  541. let maxTotal = 0;
  542. let peakIndex = -1;
  543. periodList.forEach((item, index) => {
  544. categories.push(this.formatHour(item.hour));
  545. const total = item.total || 0;
  546. totals.push(total);
  547. if (total > maxTotal) {
  548. maxTotal = total;
  549. peakIndex = index;
  550. }
  551. });
  552. // 活跃高峰:数量最多的时段
  553. this.peakRange = peakIndex > -1 ? (periodList[peakIndex].period || this.formatHour(periodList[peakIndex].hour)) : '--';
  554. // 量程随数据调整,背景轨道与量程等高
  555. const rangeMax = Math.max(10, Math.ceil(maxTotal / 10) * 10);
  556. this.rhythmOpts.yAxis.data[0].max = rangeMax;
  557. this.rhythmChart = {
  558. categories,
  559. series: [
  560. { name: '背景', color: '#D6DBED', data: periodList.map(() => rangeMax) },
  561. { name: '鼠害数量', color: '#FFC107', data: totals }
  562. ]
  563. };
  564. },
  565. // 0 → '00:00'
  566. formatHour(hour) {
  567. const h = hour || 0;
  568. return (h < 10 ? '0' + h : '' + h) + ':00';
  569. },
  570. // 鼠害占比:环形图 + 列表,物种来自 target_detail
  571. buildCommunity(total, detail) {
  572. const names = Object.keys(detail);
  573. // 无数据时显示空态(不再保留上一次的数据)
  574. this.communityEmpty = !names.length || !total;
  575. const list = names.map((name, index) => {
  576. const count = detail[name] || 0;
  577. return {
  578. name: name,
  579. count: count,
  580. percent: total ? Math.round((count / total) * 100) : 0,
  581. color: TREND_PALETTE[index % TREND_PALETTE.length]
  582. };
  583. });
  584. this.communityList = list;
  585. this.communitySelected = -1;
  586. this.totalCount = total;
  587. this.communityOpts.tooltip = { index: -1 };
  588. this.communityChart = {
  589. series: [{
  590. // 每项带上与列表圆点一致的 color,保证环形扇区与下方列表颜色一一对应
  591. data: list.map(item => ({ name: item.name, value: item.count, color: item.color }))
  592. }]
  593. };
  594. // 环形中心显示占比最高的物种
  595. if (list.length) {
  596. const top = list.reduce((a, b) => (b.count > a.count ? b : a), list[0]);
  597. this.communityOpts.title.name = (total ? Math.round((top.count / total) * 100) : 0) + '%';
  598. this.communityOpts.subtitle.name = top.name;
  599. }
  600. },
  601. // 点击占比列表:环形图高亮对应扇区,中心数据联动;再次点击取消选中
  602. onCommunityClick(index) {
  603. const item = this.communityList[index];
  604. if (!item) return;
  605. const next = this.communitySelected === index ? -1 : index;
  606. this.communitySelected = next;
  607. const target = next > -1
  608. ? item
  609. : this.communityList.reduce((a, b) => (b.count > a.count ? b : a), this.communityList[0]);
  610. this.communityOpts.title.name = target.percent + '%';
  611. this.communityOpts.subtitle.name = target.name;
  612. this.communityOpts.tooltip = { index: next };
  613. },
  614. // 点击环形图扇区:中心数据联动,复用列表点击逻辑
  615. onRingIndex(e) {
  616. if (!e || typeof e.currentIndex !== 'number' || e.currentIndex < 0) return;
  617. this.onCommunityClick(e.currentIndex);
  618. }
  619. }
  620. };
  621. </script>
  622. <style lang="scss" scoped>
  623. .rodent-analysis {
  624. width: 100%;
  625. }
  626. .analysis-card {
  627. width: 100%;
  628. padding: 32rpx;
  629. box-sizing: border-box;
  630. background: #fff;
  631. border-radius: 16rpx;
  632. margin-bottom: 24rpx;
  633. &:last-child {
  634. margin-bottom: 0;
  635. }
  636. }
  637. .analysis-card__header {
  638. display: flex;
  639. align-items: center;
  640. justify-content: space-between;
  641. margin-bottom: 20rpx;
  642. }
  643. .analysis-card__title {
  644. position: relative;
  645. font-size: 32rpx;
  646. color: #042118;
  647. font-family: 'Source Han Sans CN VF';
  648. font-weight: 700;
  649. }
  650. .analysis-card__extra {
  651. display: flex;
  652. align-items: center;
  653. .peak-label {
  654. font-size: 24rpx;
  655. color: #656565;
  656. font-family: 'Source Han Sans CN VF';
  657. margin-right: 8rpx;
  658. }
  659. .peak-value {
  660. font-size: 24rpx;
  661. color: #E53935;
  662. font-family: 'Source Han Sans CN VF';
  663. font-weight: 700;
  664. }
  665. }
  666. .chart-legend {
  667. display: flex;
  668. align-items: center;
  669. justify-content: flex-end;
  670. margin-bottom:-20rpx;
  671. .chart-legend__dot--bar {
  672. width: 16rpx;
  673. height: 16rpx;
  674. border-radius: 4rpx;
  675. background: #FFC107;
  676. margin-right: 8rpx;
  677. }
  678. .chart-legend__text {
  679. font-size: 20rpx;
  680. color: #656565;
  681. font-family: 'Source Han Sans CN VF';
  682. }
  683. }
  684. .chart-wrap {
  685. position: relative;
  686. width: 100%;
  687. }
  688. .chart-wrap--rhythm {
  689. height: 400rpx;
  690. }
  691. .chart-wrap--ring {
  692. width: 360rpx;
  693. height: 320rpx;
  694. flex-shrink: 0;
  695. }
  696. .chart-wrap--weather {
  697. height: 420rpx;
  698. }
  699. // 发生趋势分析:自定义图例(曲线图例:短线 + 空心圆点)
  700. .trend-legend {
  701. display: flex;
  702. flex-wrap: wrap;
  703. padding: 20rpx 24rpx;
  704. background: #F5F6FA;
  705. border-radius: 12rpx;
  706. margin-bottom: 20rpx;
  707. }
  708. .trend-legend__item {
  709. display: flex;
  710. align-items: center;
  711. width: 33.33%;
  712. box-sizing: border-box;
  713. padding: 8rpx 0;
  714. }
  715. .trend-legend__line {
  716. position: relative;
  717. width: 40rpx;
  718. height: 4rpx;
  719. margin-right: 12rpx;
  720. flex-shrink: 0;
  721. }
  722. .trend-legend__dot {
  723. position: absolute;
  724. left: 50%;
  725. top: 50%;
  726. transform: translate(-50%, -50%);
  727. width: 16rpx;
  728. height: 16rpx;
  729. border-radius: 50%;
  730. background: #fff;
  731. border: 4rpx solid #fff;
  732. box-sizing: border-box;
  733. }
  734. .trend-legend__text {
  735. font-size: 26rpx;
  736. color: #042118;
  737. font-family: 'Source Han Sans CN VF';
  738. }
  739. .trend-axis-label {
  740. font-size: 22rpx;
  741. color: #B3B9C2;
  742. font-family: 'Source Han Sans CN VF';
  743. margin-bottom: 4rpx;
  744. }
  745. .chart-wrap--trend {
  746. height: 420rpx;
  747. }
  748. // 图表无数据空态
  749. .chart-empty {
  750. display: flex;
  751. align-items: center;
  752. justify-content: center;
  753. min-height: 240rpx;
  754. font-size: 26rpx;
  755. color: #999999;
  756. font-family: 'Source Han Sans CN VF';
  757. }
  758. // 群落结构:上下布局,环形图在上,图例列表在下
  759. .community-content {
  760. display: flex;
  761. flex-direction: column;
  762. align-items: center;
  763. }
  764. .community-total{
  765. width: 90%;
  766. display: flex;
  767. align-items: center;
  768. justify-content: space-between;
  769. font-family: 'Source Han Sans CN VF';
  770. .total-value {
  771. font-size: 32rpx;
  772. color: #042118;
  773. font-weight: 700;
  774. line-height: 1;
  775. }
  776. .unit {
  777. font-size: 28rpx;
  778. font-weight: 400;
  779. color:#999999;
  780. margin-left: 8rpx;
  781. }
  782. }
  783. .community-list {
  784. width: 90%;
  785. margin-top: 16rpx;
  786. }
  787. .community-item {
  788. display: flex;
  789. align-items: center;
  790. padding: 12rpx 12rpx;
  791. border-radius: 8rpx;
  792. font-family: 'Source Han Sans CN VF';
  793. &.active {
  794. background: #F5F6FA;
  795. }
  796. .community-item__dot {
  797. width: 16rpx;
  798. height: 16rpx;
  799. border-radius: 4rpx;
  800. margin-right: 12rpx;
  801. flex-shrink: 0;
  802. }
  803. .community-item__name {
  804. flex: 1;
  805. font-size: 26rpx;
  806. color: #042118;
  807. }
  808. .community-item__count {
  809. font-size: 26rpx;
  810. color: #042118;
  811. margin-right: 16rpx;
  812. }
  813. .community-item__percent {
  814. font-size: 26rpx;
  815. color: #656565;
  816. width: 72rpx;
  817. text-align: right;
  818. }
  819. }
  820. // 总量害虫数量
  821. .total-main {
  822. display: flex;
  823. align-items: flex-end;
  824. margin-bottom: 24rpx;
  825. font-family: 'Source Han Sans CN VF';
  826. .total-value {
  827. font-size: 56rpx;
  828. color: #042118;
  829. font-weight: 700;
  830. line-height: 1;
  831. .total-unit {
  832. font-size: 28rpx;
  833. font-weight: 500;
  834. margin-left: 8rpx;
  835. }
  836. }
  837. .total-compare {
  838. font-size: 24rpx;
  839. color: #656565;
  840. margin-left: 24rpx;
  841. padding-bottom: 4rpx;
  842. .total-compare__value {
  843. color: #E53935;
  844. font-weight: 700;
  845. }
  846. }
  847. }
  848. .total-bar {
  849. .total-bar__track {
  850. position: relative;
  851. height: 16rpx;
  852. border-radius: 8rpx;
  853. background: #F1F4F8;
  854. overflow: visible;
  855. }
  856. .total-bar__fill {
  857. height: 100%;
  858. border-radius: 8rpx;
  859. background: #0BBC58;
  860. }
  861. .total-bar__marker {
  862. position: absolute;
  863. top: -6rpx;
  864. width: 4rpx;
  865. height: 28rpx;
  866. border-radius: 2rpx;
  867. background: #E53935;
  868. transform: translateX(-50%);
  869. }
  870. .total-bar__scale {
  871. display: flex;
  872. justify-content: space-between;
  873. margin-top: 8rpx;
  874. font-size: 22rpx;
  875. color: #999999;
  876. font-family: 'Source Han Sans CN VF';
  877. }
  878. }
  879. // 周/月/年切换
  880. .range-tabs {
  881. display: flex;
  882. align-items: center;
  883. .range-tab {
  884. width: 64rpx;
  885. height: 48rpx;
  886. line-height: 44rpx;
  887. text-align: center;
  888. font-size: 24rpx;
  889. color: #042118;
  890. font-family: 'Source Han Sans CN VF';
  891. background: #fff;
  892. border: 2rpx solid #E4E7ED;
  893. box-sizing: border-box;
  894. &:first-child {
  895. border-radius: 8rpx 0 0 8rpx;
  896. }
  897. &:last-child {
  898. border-radius: 0 8rpx 8rpx 0;
  899. }
  900. &.active {
  901. background: #0BBC58;
  902. border-color: #0BBC58;
  903. color: #fff;
  904. font-weight: 700;
  905. }
  906. }
  907. }
  908. </style>