pestEchart.vue 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <template>
  2. <view class="pest-echart" v-show="tabs.length">
  3. <view class="pest-echart__header">
  4. <view class="tab-list">
  5. <view
  6. v-for="(tab, index) in tabs"
  7. :key="index"
  8. class="tab-item"
  9. :class="{ active: activeTab === index }"
  10. @click="switchTab(index)"
  11. >
  12. {{ tab.name }}
  13. </view>
  14. </view>
  15. </view>
  16. <view class="pest-echart__content" v-show="dayData.length">
  17. <!-- 三个关键时期 -->
  18. <view class="period-section">
  19. <view class="period-item">
  20. <view class="period-label">始见期</view>
  21. <view class="period-value">{{ periodData.firstDate }}</view>
  22. </view>
  23. <view class="period-item">
  24. <view class="period-label">高峰期</view>
  25. <view class="period-value">{{ periodData.peakDate }}</view>
  26. </view>
  27. <view class="period-item">
  28. <view class="period-label">终见期</view>
  29. <view class="period-value">{{ periodData.lastDate }}</view>
  30. </view>
  31. </view>
  32. <!-- 图表区域 -->
  33. <view class="chart-container">
  34. <!-- <canvas
  35. canvas-id="pestChart"
  36. id="pestChart"
  37. class="charts"
  38. :style="{'width':cWidth*pixelRatio+'px','height':cHeight*pixelRatio+'px', 'transform': 'scale('+(1/pixelRatio)+')','margin-left':-cWidth*(pixelRatio-1)/2+'px','margin-top':-cHeight*(pixelRatio-1)/2+'px'}"
  39. @touchstart="touchChart($event)"
  40. @touchmove="moveChart($event)"
  41. @touchend="touchEndChart($event)"
  42. disable-scroll=true
  43. ></canvas> -->
  44. <qiun-data-charts type="line" :chartData="chartData" :opts="opts" :canvas2d="true" :inScrollView="true" :ontouch="true" />
  45. </view>
  46. </view>
  47. </view>
  48. </template>
  49. <script>
  50. import uCharts from '../../../components/js_sdk/u-charts/u-charts/u-charts.js';
  51. let chartInstance = null;
  52. export default {
  53. name: 'PestEchart',
  54. props:{
  55. pest_order:{
  56. type: Object,
  57. default: () => ({})
  58. },
  59. endDate: {
  60. type: String,
  61. default: ''
  62. },
  63. d_id: {
  64. type: String | Number,
  65. default: ''
  66. },
  67. day:{
  68. type: Array,
  69. default: () => []
  70. },
  71. pest:{
  72. type: Array,
  73. default: () => []
  74. },
  75. },
  76. data() {
  77. return {
  78. tabs: [],
  79. currentPest:'',
  80. activeTab: 0,
  81. dayData:[],
  82. // 三个关键时期数据
  83. periodData: {
  84. firstDate: '-',
  85. peakDate: '-',
  86. lastDate: '-'
  87. },
  88. chartData: {},
  89. // canvas 尺寸配置
  90. cWidth: 650,
  91. cHeight: 400,
  92. pixelRatio: 1,
  93. opts: {
  94. type: 'line',
  95. xAxis: {
  96. disableGrid: true,
  97. itemCount: 3,
  98. scrollShow: true
  99. },
  100. yAxis: {
  101. disableGrid: true,
  102. gridType: 'dash',
  103. splitNumber: 5,
  104. min: 0,
  105. format: (val) => {
  106. return Math.round(val)
  107. }
  108. },
  109. extra: {
  110. line: {
  111. type: 'curve'
  112. },
  113. tooltip: {
  114. format: {
  115. name: '',
  116. value: (val) => Math.round(val)
  117. }
  118. }
  119. },
  120. legend: {
  121. },
  122. enableScroll: true
  123. },
  124. };
  125. },
  126. watch:{
  127. pest_order:{
  128. handler(val){
  129. this.tabs = [];
  130. for(let key in val){
  131. this.tabs.push({
  132. name: key,
  133. })
  134. }
  135. const name = this.tabs[0]?.name;
  136. this.currentPest = name;
  137. if(this.currentPest){
  138. this.getPestNameDetail(name);
  139. this.setChartData();
  140. }
  141. },
  142. deep: true
  143. },
  144. d_id:{
  145. handler(val){
  146. val && this.setChartData();
  147. },
  148. deep: true
  149. },
  150. endDate:{
  151. handler(val){
  152. val && this.setChartData();
  153. },
  154. deep: true
  155. },
  156. day:{
  157. handler(){
  158. this.initChart();
  159. },
  160. deep: true,
  161. immediate: true
  162. },
  163. },
  164. mounted() {
  165. this.cWidth = uni.upx2px(650);
  166. this.cHeight = uni.upx2px(400);
  167. this.pixelRatio = uni.getSystemInfoSync().pixelRatio;
  168. },
  169. methods: {
  170. async getPestNameDetail(name){
  171. try{
  172. const res = await this.$myRequest({
  173. url: '/api/pest_name_detail',
  174. method: 'POST',
  175. data: {
  176. name
  177. },
  178. });
  179. this.deviceInfo = res
  180. this.$emit('getInfo',res)
  181. }catch(err){
  182. this.$emit('getInfo',{})
  183. }
  184. },
  185. async setChartData(){
  186. if(!this.currentPest){
  187. return;
  188. }
  189. if(!this.d_id || !this.endDate){
  190. return;
  191. }
  192. const res = await this.$myRequest({
  193. url: '/api/api_gateway?method=forecast.cbd_analysis.pest_predict_time',
  194. method: 'POST',
  195. data: {
  196. model:'B',
  197. d_id: this.d_id,
  198. year: this.endDate.split('-')[0],
  199. pest: this.currentPest,
  200. },
  201. });
  202. this.periodData = {
  203. firstDate: res[0][0],
  204. peakDate: res[1][0],
  205. lastDate: res[2][0],
  206. }
  207. },
  208. initChart() {
  209. this.$nextTick(() => {
  210. this.updateChartsData(0);
  211. });
  212. },
  213. drawChart(index) {
  214. const dayData = this.day || [];
  215. this.dayData = dayData;
  216. const pestData = this.pest[index];
  217. const ctx = uni.createCanvasContext('pestChart', this);
  218. chartInstance = new uCharts({
  219. context: ctx,
  220. type: 'line',
  221. fontSize: 11,
  222. legend: {
  223. show: false
  224. },
  225. background: '#FFFFFF',
  226. pixelRatio: this.pixelRatio,
  227. animation: true,
  228. dataLabel: false,
  229. categories: dayData,
  230. series: [{
  231. name: '',
  232. data: pestData
  233. }],
  234. color: ['#0085FF'],
  235. xAxis: {
  236. disableGrid: false,
  237. boundaryGap: 'justify',
  238. axisLine: true,
  239. lineColor: '#CCCCCC',
  240. fontColor: '#999999'
  241. },
  242. yAxis: {
  243. min: 0,
  244. minInterval: 1,
  245. splitNumber: 4,
  246. axisLine: true,
  247. lineColor: '#CCCCCC',
  248. fontColor: '#999999',
  249. gridType: 'dash',
  250. gridColor: '#E5E5E5'
  251. },
  252. width: this.cWidth * this.pixelRatio,
  253. height: this.cHeight * this.pixelRatio,
  254. extra: {
  255. line: {
  256. type: 'curve',
  257. width: 2,
  258. activeType: 'hollow'
  259. },
  260. tooltip: {
  261. showBox: true,
  262. bgOpacity: 0.7
  263. }
  264. }
  265. });
  266. },
  267. touchChart(e) {
  268. if (chartInstance) {
  269. chartInstance.scrollStart(e);
  270. }
  271. },
  272. moveChart(e) {
  273. if (chartInstance) {
  274. chartInstance.scroll(e);
  275. }
  276. },
  277. touchEndChart(e) {
  278. if (chartInstance) {
  279. chartInstance.scrollEnd(e);
  280. chartInstance.showToolTip(e, {
  281. format: function(item, category) {
  282. return category + ' ' + item.name + ':' + item.data
  283. }
  284. });
  285. }
  286. },
  287. switchTab(index) {
  288. this.activeTab = index;
  289. const name = this.tabs[index]?.name;
  290. this.currentPest = name
  291. this.getPestNameDetail(name);
  292. this.setChartData();
  293. this.$nextTick(() => {
  294. this.updateChartsData(index);
  295. });
  296. },
  297. updateChartsData(index){
  298. const dayData = this.day || [];
  299. this.dayData = dayData;
  300. const pestData = this.pest[index];
  301. this.updateYAxisSplitNumber(pestData);
  302. const lineData = {
  303. categories: dayData,
  304. series: [
  305. {
  306. name: '',
  307. data: pestData
  308. }
  309. ]
  310. };
  311. this.chartData = lineData;
  312. },
  313. // 根据数据范围动态计算 y 轴分段数,保证每段刻度间距 >= 1,
  314. // 避免数据较小时刻度被取整后出现重复(如 0,0,0,1,1,1)
  315. updateYAxisSplitNumber(pestData){
  316. const numList = (pestData || [])
  317. .map(v => Number(v))
  318. .filter(v => !isNaN(v));
  319. let dataMin = numList.length ? Math.min(...numList) : 0;
  320. let dataMax = numList.length ? Math.max(...numList) : 0;
  321. // 与 u-charts 内部逻辑一致:数据全部相同时 min 归 0(全为 0 时 max 取 10)
  322. if (dataMin === dataMax) {
  323. if (dataMax === 0) {
  324. dataMax = 10;
  325. }
  326. dataMin = 0;
  327. }
  328. const span = dataMax - dataMin;
  329. // 数据范围 >= 5 时固定 5 段(每段间距自然 >= 1);否则按范围取分段数,使每段正好为 1
  330. const splitNumber = span >= 5 ? 5 : Math.max(1, Math.floor(span));
  331. this.opts.yAxis = { ...this.opts.yAxis, splitNumber };
  332. }
  333. }
  334. };
  335. </script>
  336. <style lang="scss" scoped>
  337. .pest-echart {
  338. margin-top: 24rpx;
  339. &__header{
  340. margin-bottom: 24rpx;
  341. }
  342. }
  343. .tab-list {
  344. width: 100%;
  345. overflow-x: scroll;
  346. white-space: nowrap;
  347. overflow-y: hidden;
  348. box-sizing: border-box;
  349. -ms-overflow-style: none;
  350. scrollbar-width: none;
  351. }
  352. .tab-item {
  353. margin-right: 16rpx;
  354. font-size: 28rpx;
  355. font-family: 'Source Han Sans CN VF', sans-serif;
  356. font-weight: 500;
  357. color: #999999;
  358. display: inline-block;
  359. box-sizing: border-box;
  360. background: #ffffff;
  361. font-family: "Source Han Sans CN VF";
  362. font-size: 28rpx;
  363. padding: 10rpx 32rpx;
  364. border-radius: 8rpx;
  365. &:last-child {
  366. margin-right: 0;
  367. }
  368. &.active {
  369. color: #0BBC58;
  370. font-weight: 700;
  371. }
  372. &:first-child.active {
  373. color: #0BBC58;
  374. }
  375. }
  376. .pest-echart__content {
  377. background: #FFFFFF;
  378. border-radius: 16rpx;
  379. padding: 0 32rpx 32rpx;
  380. }
  381. .period-section {
  382. display: flex;
  383. justify-content: space-between;
  384. padding: 24rpx;
  385. background: #ffffff;
  386. border-radius: 12rpx;
  387. }
  388. .period-item {
  389. flex: 1;
  390. display: flex;
  391. flex-direction: column;
  392. align-items: center;
  393. }
  394. .period-label {
  395. font-size: 28rpx;
  396. color: #303133;
  397. font-family: 'Source Han Sans CN VF', sans-serif;
  398. font-weight: 700;
  399. margin-bottom: 8rpx;
  400. }
  401. .period-value {
  402. font-size: 24rpx;
  403. color: #999999;
  404. font-family: 'Source Han Sans CN VF', sans-serif;
  405. font-weight: 400;
  406. }
  407. .chart-container {
  408. position: relative;
  409. width: 100%;
  410. height: 400rpx;
  411. border-radius: 12rpx;
  412. }
  413. .charts {
  414. width: 100%;
  415. height: 100%;
  416. }
  417. </style>