xsh_1997 il y a 4 semaines
Parent
commit
b21b1a327c

+ 89 - 76
ruoyi-screen/src/views/home/chartOptions.js

@@ -6,7 +6,6 @@ const MONTH_AXIS_LABEL = { color: '#9fb0c3', fontSize: 9, margin: 6 }
6
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
6
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
7
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
7
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
8
 const GRID = { left: 22, right: 8, top: 24, bottom: 6, containLabel: true }
8
 const GRID = { left: 22, right: 8, top: 24, bottom: 6, containLabel: true }
9
-const MONTHS = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
10
 
9
 
11
 function emptyOption(text = '暂无数据') {
10
 function emptyOption(text = '暂无数据') {
12
   return {
11
   return {
@@ -19,10 +18,6 @@ function emptyOption(text = '暂无数据') {
19
   }
18
   }
20
 }
19
 }
21
 
20
 
22
-function sortByMonth(list) {
23
-  return [...(list || [])].sort((a, b) => (a.month || 0) - (b.month || 0))
24
-}
25
-
26
 function pickNumber(row, fields) {
21
 function pickNumber(row, fields) {
27
   for (const key of fields) {
22
   for (const key of fields) {
28
     const raw = row?.[key]
23
     const raw = row?.[key]
@@ -36,22 +31,59 @@ function pickNumber(row, fields) {
36
   return 0
31
   return 0
37
 }
32
 }
38
 
33
 
39
-function monthSeries(list, field, altFields = []) {
34
+/** 滚动最近 12 月:按 statYear、month 升序 */
35
+function sortRollingMonths(list) {
36
+  return [...(list || [])].sort((a, b) => {
37
+    const ak = (Number(a.statYear) || 0) * 100 + (Number(a.month) || 0)
38
+    const bk = (Number(b.statYear) || 0) * 100 + (Number(b.month) || 0)
39
+    return ak - bk
40
+  })
41
+}
42
+
43
+function rollingMonthLabel(row) {
44
+  return `${Number(row.month)}月`
45
+}
46
+
47
+function rollingTooltipTitle(row) {
48
+  if (!row) {
49
+    return ''
50
+  }
51
+  return `${row.statYear}年${Number(row.month)}月`
52
+}
53
+
54
+function buildRollingAxis(list) {
55
+  const rows = sortRollingMonths(list)
56
+  return {
57
+    rows,
58
+    categories: rows.map(rollingMonthLabel),
59
+    titleAt: (dataIndex) => rollingTooltipTitle(rows[dataIndex])
60
+  }
61
+}
62
+
63
+function rollingSeries(list, field, altFields = []) {
40
   const fields = [field, ...altFields]
64
   const fields = [field, ...altFields]
41
-  const map = new Map(
42
-    sortByMonth(list).map((row) => [Number(row.month), pickNumber(row, fields)])
43
-  )
44
-  return MONTHS.map((_, i) => map.get(i + 1) ?? 0)
65
+  return sortRollingMonths(list).map((row) => pickNumber(row, fields))
45
 }
66
 }
46
 
67
 
47
-/** 牦牛存栏变动:公牛 / 母牛折线 */
68
+/** 牦牛存栏变动:公牛 / 母牛折线(滚动 12 月) */
48
 export function buildInventoryTrendOption(monthlyTrend) {
69
 export function buildInventoryTrendOption(monthlyTrend) {
49
   if (!monthlyTrend?.length) {
70
   if (!monthlyTrend?.length) {
50
     return emptyOption()
71
     return emptyOption()
51
   }
72
   }
73
+  const axis = buildRollingAxis(monthlyTrend)
52
   return {
74
   return {
53
     color: ['#5ef0c8', '#ecd27b'],
75
     color: ['#5ef0c8', '#ecd27b'],
54
-    tooltip: { trigger: 'axis' },
76
+    tooltip: {
77
+      trigger: 'axis',
78
+      formatter: (params) => {
79
+        if (!Array.isArray(params) || !params.length) {
80
+          return ''
81
+        }
82
+        const title = axis.titleAt(params[0].dataIndex)
83
+        const lines = params.map((p) => `${p.marker}${p.seriesName} ${p.value} 头`)
84
+        return `${title}<br/>${lines.join('<br/>')}`
85
+      }
86
+    },
55
     legend: {
87
     legend: {
56
       top: 0,
88
       top: 0,
57
       right: 8,
89
       right: 8,
@@ -62,7 +94,7 @@ export function buildInventoryTrendOption(monthlyTrend) {
62
     grid: GRID,
94
     grid: GRID,
63
     xAxis: {
95
     xAxis: {
64
       type: 'category',
96
       type: 'category',
65
-      data: MONTHS,
97
+      data: axis.categories,
66
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
98
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
67
       axisLine: AXIS_LINE,
99
       axisLine: AXIS_LINE,
68
       axisTick: { show: false }
100
       axisTick: { show: false }
@@ -80,7 +112,7 @@ export function buildInventoryTrendOption(monthlyTrend) {
80
         smooth: true,
112
         smooth: true,
81
         symbol: 'circle',
113
         symbol: 'circle',
82
         symbolSize: 4,
114
         symbolSize: 4,
83
-        data: monthSeries(monthlyTrend, 'bullCount')
115
+        data: rollingSeries(monthlyTrend, 'bullCount')
84
       },
116
       },
85
       {
117
       {
86
         name: '母牛',
118
         name: '母牛',
@@ -88,20 +120,21 @@ export function buildInventoryTrendOption(monthlyTrend) {
88
         smooth: true,
120
         smooth: true,
89
         symbol: 'circle',
121
         symbol: 'circle',
90
         symbolSize: 4,
122
         symbolSize: 4,
91
-        data: monthSeries(monthlyTrend, 'cowCount')
123
+        data: rollingSeries(monthlyTrend, 'cowCount')
92
       }
124
       }
93
     ]
125
     ]
94
   }
126
   }
95
 }
127
 }
96
 
128
 
97
-/** 年龄三档配色(对齐 doc/首页/耗牛年龄结构.png) */
129
+/** 年龄四档配色(母牛分龄:1岁以下 / 1~3岁 / 3~12岁 / 12岁以上) */
98
 const AGE_BAND_COLOR = {
130
 const AGE_BAND_COLOR = {
99
-  M1_6: '#45c997',
100
-  M7_12: '#6eb5ff',
101
-  M13_PLUS: '#ecd27b'
131
+  COW_U1: '#45c997',
132
+  COW_1_3: '#6eb5ff',
133
+  COW_3_12: '#547233',
134
+  COW_O12: '#ecd27b'
102
 }
135
 }
103
 
136
 
104
-const AGE_BAND_ORDER = ['M1_6', 'M7_12', 'M13_PLUS']
137
+const AGE_BAND_ORDER = ['COW_U1', 'COW_1_3', 'COW_3_12', 'COW_O12']
105
 
138
 
106
 function sortAgeBands(ageStructure) {
139
 function sortAgeBands(ageStructure) {
107
   const map = new Map((ageStructure || []).map((item) => [item.bandCode, item]))
140
   const map = new Map((ageStructure || []).map((item) => [item.bandCode, item]))
@@ -116,7 +149,7 @@ function formatAgeRatio(ratio, percent) {
116
   return `${Math.round(percent)}%`
149
   return `${Math.round(percent)}%`
117
 }
150
 }
118
 
151
 
119
-/** 牦牛年龄结构(环形图 + 右侧图例,固定档) */
152
+/** 牦牛年龄结构(环形图 + 右侧图例,固定档) */
120
 export function buildAgeStructureOption(ageStructure) {
153
 export function buildAgeStructureOption(ageStructure) {
121
   if (!ageStructure?.length) {
154
   if (!ageStructure?.length) {
122
     return emptyOption()
155
     return emptyOption()
@@ -167,7 +200,7 @@ export function buildAgeStructureOption(ageStructure) {
167
       icon: 'circle',
200
       icon: 'circle',
168
       itemWidth: 8,
201
       itemWidth: 8,
169
       itemHeight: 8,
202
       itemHeight: 8,
170
-      itemGap: 12,
203
+      itemGap: 10,
171
       textStyle: { color: '#e8eef5', fontSize: 10 }
204
       textStyle: { color: '#e8eef5', fontSize: 10 }
172
     },
205
     },
173
     graphic: [
206
     graphic: [
@@ -227,7 +260,7 @@ export function buildAgeStructureOption(ageStructure) {
227
   }
260
   }
228
 }
261
 }
229
 
262
 
230
-/** 出栏柱图金色渐变(对齐 doc/首页/耗牛出栏统计.png) */
263
+/** 出栏柱图金色渐变 */
231
 const OUTBOUND_BAR_GRADIENT = {
264
 const OUTBOUND_BAR_GRADIENT = {
232
   type: 'linear',
265
   type: 'linear',
233
   x: 0,
266
   x: 0,
@@ -246,33 +279,30 @@ const OUTBOUND_SPLIT_LINE = {
246
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
279
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
247
 }
280
 }
248
 
281
 
249
-/** 牦牛出栏按月(柱 + 折线,设计稿样式) */
282
+/** 牦牛出栏按月(纯柱状图,滚动 12 月) */
250
 export function buildOutboundOption(outboundMonthly) {
283
 export function buildOutboundOption(outboundMonthly) {
251
   if (!outboundMonthly?.length) {
284
   if (!outboundMonthly?.length) {
252
     return emptyOption()
285
     return emptyOption()
253
   }
286
   }
254
-  const values = monthSeries(outboundMonthly, 'outboundCount', ['outbound_count', 'count'])
287
+  const axis = buildRollingAxis(outboundMonthly)
288
+  const values = rollingSeries(outboundMonthly, 'outboundCount', ['outbound_count', 'count'])
255
   return {
289
   return {
256
     tooltip: {
290
     tooltip: {
257
       trigger: 'axis',
291
       trigger: 'axis',
258
       axisPointer: { type: 'shadow' },
292
       axisPointer: { type: 'shadow' },
259
       formatter: (params) => {
293
       formatter: (params) => {
260
-        const bar = params.find((p) => p.seriesType === 'bar') || params[0]
261
-        return `${bar.name}<br/>出栏 ${bar.value} 头`
294
+        const p = Array.isArray(params) ? params[0] : params
295
+        if (!p) {
296
+          return ''
297
+        }
298
+        const title = axis.titleAt(p.dataIndex)
299
+        return `${title}<br/>出栏 ${p.value} 头`
262
       }
300
       }
263
     },
301
     },
264
-    legend: {
265
-      top: 4,
266
-      left: 8,
267
-      itemWidth: 8,
268
-      itemHeight: 8,
269
-      textStyle: { color: '#a8d4c8', fontSize: 10 },
270
-      data: ['出栏', '趋势']
271
-    },
272
     grid: OUTBOUND_GRID,
302
     grid: OUTBOUND_GRID,
273
     xAxis: {
303
     xAxis: {
274
       type: 'category',
304
       type: 'category',
275
-      data: MONTHS,
305
+      data: axis.categories,
276
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
306
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
277
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
307
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
278
       axisTick: { show: false }
308
       axisTick: { show: false }
@@ -290,27 +320,17 @@ export function buildOutboundOption(outboundMonthly) {
290
         name: '出栏',
320
         name: '出栏',
291
         type: 'bar',
321
         type: 'bar',
292
         barWidth: '46%',
322
         barWidth: '46%',
293
-        z: 1,
294
         itemStyle: {
323
         itemStyle: {
295
           color: OUTBOUND_BAR_GRADIENT,
324
           color: OUTBOUND_BAR_GRADIENT,
296
           borderRadius: [10, 10, 10, 10]
325
           borderRadius: [10, 10, 10, 10]
297
         },
326
         },
298
         data: values
327
         data: values
299
-      },
300
-      {
301
-        name: '趋势',
302
-        type: 'line',
303
-        smooth: true,
304
-        symbol: 'none',
305
-        z: 2,
306
-        lineStyle: { color: '#b8e86a', width: 2 },
307
-        data: values
308
       }
328
       }
309
     ]
329
     ]
310
   }
330
   }
311
 }
331
 }
312
 
332
 
313
-/** 草场退化五档配色(对齐 doc/首页/草场类型占比.png) */
333
+/** 草场退化五档配色 */
314
 const GRASSLAND_DEGRADATION_COLOR = {
334
 const GRASSLAND_DEGRADATION_COLOR = {
315
   1: '#3b66d1',
335
   1: '#3b66d1',
316
   2: '#5dc3a1',
336
   2: '#5dc3a1',
@@ -400,22 +420,6 @@ export function buildGrasslandOption(grassland) {
400
   }
420
   }
401
 }
421
 }
402
 
422
 
403
-/** 预约堆叠柱配色(对齐 doc/首页/线下服务预约.png,自下而上:机构蓝 / 兽医青 / 专家金) */
404
-const APPOINTMENT_MONTHS = [
405
-  '一月',
406
-  '二月',
407
-  '三月',
408
-  '四月',
409
-  '五月',
410
-  '六月',
411
-  '七月',
412
-  '八月',
413
-  '九月',
414
-  '十月',
415
-  '十一月',
416
-  '十二月'
417
-]
418
-
419
 const APPOINTMENT_ORG_GRADIENT = {
423
 const APPOINTMENT_ORG_GRADIENT = {
420
   type: 'linear',
424
   type: 'linear',
421
   x: 0,
425
   x: 0,
@@ -445,18 +449,23 @@ const APPOINTMENT_SPLIT_LINE = {
445
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
449
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
446
 }
450
 }
447
 
451
 
448
-/** 线下服务预约堆叠柱 */
452
+/** 线下服务预约堆叠柱(滚动 12 月) */
449
 export function buildAppointmentStackedOption(monthlyStacked) {
453
 export function buildAppointmentStackedOption(monthlyStacked) {
450
   if (!monthlyStacked?.length) {
454
   if (!monthlyStacked?.length) {
451
     return emptyOption()
455
     return emptyOption()
452
   }
456
   }
457
+  const axis = buildRollingAxis(monthlyStacked)
453
   return {
458
   return {
454
     tooltip: {
459
     tooltip: {
455
       trigger: 'axis',
460
       trigger: 'axis',
456
       axisPointer: { type: 'shadow' },
461
       axisPointer: { type: 'shadow' },
457
       formatter: (params) => {
462
       formatter: (params) => {
463
+        if (!Array.isArray(params) || !params.length) {
464
+          return ''
465
+        }
466
+        const title = axis.titleAt(params[0].dataIndex)
458
         const lines = params.map((p) => `${p.marker}${p.seriesName} ${p.value} 人次`)
467
         const lines = params.map((p) => `${p.marker}${p.seriesName} ${p.value} 人次`)
459
-        return `${params[0]?.name}<br/>${lines.join('<br/>')}`
468
+        return `${title}<br/>${lines.join('<br/>')}`
460
       }
469
       }
461
     },
470
     },
462
     legend: {
471
     legend: {
@@ -470,7 +479,7 @@ export function buildAppointmentStackedOption(monthlyStacked) {
470
     grid: APPOINTMENT_GRID,
479
     grid: APPOINTMENT_GRID,
471
     xAxis: {
480
     xAxis: {
472
       type: 'category',
481
       type: 'category',
473
-      data: APPOINTMENT_MONTHS,
482
+      data: axis.categories,
474
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
483
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
475
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
484
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
476
       axisTick: { show: false }
485
       axisTick: { show: false }
@@ -490,7 +499,7 @@ export function buildAppointmentStackedOption(monthlyStacked) {
490
         stack: 'apt',
499
         stack: 'apt',
491
         barMaxWidth: 10,
500
         barMaxWidth: 10,
492
         itemStyle: { color: APPOINTMENT_ORG_GRADIENT },
501
         itemStyle: { color: APPOINTMENT_ORG_GRADIENT },
493
-        data: monthSeries(monthlyStacked, 'orgCount', ['org_count'])
502
+        data: rollingSeries(monthlyStacked, 'orgCount', ['org_count'])
494
       },
503
       },
495
       {
504
       {
496
         name: '兽医',
505
         name: '兽医',
@@ -498,7 +507,7 @@ export function buildAppointmentStackedOption(monthlyStacked) {
498
         stack: 'apt',
507
         stack: 'apt',
499
         barMaxWidth: 10,
508
         barMaxWidth: 10,
500
         itemStyle: { color: '#52b89f' },
509
         itemStyle: { color: '#52b89f' },
501
-        data: monthSeries(monthlyStacked, 'vetCount', ['vet_count'])
510
+        data: rollingSeries(monthlyStacked, 'vetCount', ['vet_count'])
502
       },
511
       },
503
       {
512
       {
504
         name: '专家',
513
         name: '专家',
@@ -506,13 +515,13 @@ export function buildAppointmentStackedOption(monthlyStacked) {
506
         stack: 'apt',
515
         stack: 'apt',
507
         barMaxWidth: 10,
516
         barMaxWidth: 10,
508
         itemStyle: { color: APPOINTMENT_EXPERT_GRADIENT },
517
         itemStyle: { color: APPOINTMENT_EXPERT_GRADIENT },
509
-        data: monthSeries(monthlyStacked, 'expertCount', ['expert_count'])
518
+        data: rollingSeries(monthlyStacked, 'expertCount', ['expert_count'])
510
       }
519
       }
511
     ]
520
     ]
512
   }
521
   }
513
 }
522
 }
514
 
523
 
515
-/** 三类柱图配色(与线下服务预约堆叠柱一致:蓝 / 青 / 金) */
524
+/** 三类柱图配色 */
516
 const STACK_BAR_COLORS = [APPOINTMENT_ORG_GRADIENT, '#52b89f', APPOINTMENT_EXPERT_GRADIENT]
525
 const STACK_BAR_COLORS = [APPOINTMENT_ORG_GRADIENT, '#52b89f', APPOINTMENT_EXPERT_GRADIENT]
517
 
526
 
518
 /** 惠农补贴(万元) */
527
 /** 惠农补贴(万元) */
@@ -569,7 +578,6 @@ export function buildSubsidyOption(typeStats, hasSubsidyData) {
569
   }
578
   }
570
 }
579
 }
571
 
580
 
572
-/** 实战培训面积图配色(对齐 doc/首页/实战培训占比.png) */
573
 const TRAINING_ACTIVITY_LINE = '#ff9f43'
581
 const TRAINING_ACTIVITY_LINE = '#ff9f43'
574
 const TRAINING_ACTIVITY_AREA = {
582
 const TRAINING_ACTIVITY_AREA = {
575
   type: 'linear',
583
   type: 'linear',
@@ -601,12 +609,16 @@ const TRAINING_SPLIT_LINE = {
601
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
609
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
602
 }
610
 }
603
 
611
 
604
-/** 实战培训报名趋势(双面积折线:活动数 + 报名人数) */
612
+/** 实战培训报名趋势(双面积折线,滚动 12 月) */
605
 export function buildTrainingOption(trainingTrend) {
613
 export function buildTrainingOption(trainingTrend) {
606
   if (!trainingTrend?.hasTrainingData) {
614
   if (!trainingTrend?.hasTrainingData) {
607
     return emptyOption('暂无培训数据')
615
     return emptyOption('暂无培训数据')
608
   }
616
   }
609
   const monthly = trainingTrend.monthly || []
617
   const monthly = trainingTrend.monthly || []
618
+  if (!monthly.length) {
619
+    return emptyOption('暂无培训数据')
620
+  }
621
+  const axis = buildRollingAxis(monthly)
610
   return {
622
   return {
611
     tooltip: {
623
     tooltip: {
612
       trigger: 'axis',
624
       trigger: 'axis',
@@ -614,11 +626,12 @@ export function buildTrainingOption(trainingTrend) {
614
         if (!Array.isArray(params) || !params.length) {
626
         if (!Array.isArray(params) || !params.length) {
615
           return ''
627
           return ''
616
         }
628
         }
629
+        const title = axis.titleAt(params[0].dataIndex)
617
         const lines = params.map((p) => {
630
         const lines = params.map((p) => {
618
           const unit = p.seriesName === '活动数' ? '个' : '人'
631
           const unit = p.seriesName === '活动数' ? '个' : '人'
619
           return `${p.marker}${p.seriesName} ${p.value ?? 0} ${unit}`
632
           return `${p.marker}${p.seriesName} ${p.value ?? 0} ${unit}`
620
         })
633
         })
621
-        return `${params[0].name}<br/>${lines.join('<br/>')}`
634
+        return `${title}<br/>${lines.join('<br/>')}`
622
       }
635
       }
623
     },
636
     },
624
     legend: {
637
     legend: {
@@ -631,7 +644,7 @@ export function buildTrainingOption(trainingTrend) {
631
     grid: TRAINING_GRID,
644
     grid: TRAINING_GRID,
632
     xAxis: {
645
     xAxis: {
633
       type: 'category',
646
       type: 'category',
634
-      data: MONTHS,
647
+      data: axis.categories,
635
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0, color: '#e8eef5' },
648
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0, color: '#e8eef5' },
636
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
649
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
637
       axisTick: { show: false }
650
       axisTick: { show: false }
@@ -672,7 +685,7 @@ export function buildTrainingOption(trainingTrend) {
672
           shadowBlur: 10
685
           shadowBlur: 10
673
         },
686
         },
674
         areaStyle: { color: TRAINING_ACTIVITY_AREA },
687
         areaStyle: { color: TRAINING_ACTIVITY_AREA },
675
-        data: monthSeries(monthly, 'activityCount', ['activity_count'])
688
+        data: rollingSeries(monthly, 'activityCount', ['activity_count'])
676
       },
689
       },
677
       {
690
       {
678
         name: '报名人数',
691
         name: '报名人数',
@@ -687,7 +700,7 @@ export function buildTrainingOption(trainingTrend) {
687
           shadowBlur: 10
700
           shadowBlur: 10
688
         },
701
         },
689
         areaStyle: { color: TRAINING_ENROLL_AREA },
702
         areaStyle: { color: TRAINING_ENROLL_AREA },
690
-        data: monthSeries(monthly, 'enrolledCount', ['enrolled_count'])
703
+        data: rollingSeries(monthly, 'enrolledCount', ['enrolled_count'])
691
       }
704
       }
692
     ]
705
     ]
693
   }
706
   }

+ 37 - 14
ruoyi-screen/src/views/home/index.vue

@@ -1,6 +1,6 @@
1
 <template>
1
 <template>
2
   <div class="screen-page screen-page--home" :class="{ 'is-loading': loading }">
2
   <div class="screen-page screen-page--home" :class="{ 'is-loading': loading }">
3
-    <!-- <div v-if="loadError" class="home-error">{{ loadError }}</div>
3
+    <div v-if="loadError" class="home-error" @click="fetchDashboard">{{ loadError }}(点击重试)</div>
4
 
4
 
5
     <div class="home-year-bar">
5
     <div class="home-year-bar">
6
       <label class="home-year-bar__label">统计年份</label>
6
       <label class="home-year-bar__label">统计年份</label>
@@ -12,11 +12,12 @@
12
       >
12
       >
13
         <option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
13
         <option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
14
       </select>
14
       </select>
15
-    </div> -->
15
+      <span v-if="statDate" class="home-year-bar__date">统计日 {{ statDate }}</span>
16
+    </div>
16
 
17
 
17
     <div class="screen-page--home-column">
18
     <div class="screen-page--home-column">
18
       <div class="screen-page--home-column-top">
19
       <div class="screen-page--home-column-top">
19
-        <div class="top_title">产总览 ལས་རྩོམ་ཐོག་མའི་དོ་དམ།</div>
20
+        <div class="top_title">产总览 ལས་རྩོམ་ཐོག་མའི་དོ་དམ།</div>
20
         <div class="top_content">
21
         <div class="top_content">
21
           <div class="content_1">
22
           <div class="content_1">
22
             <div class="content_num">
23
             <div class="content_num">
@@ -74,7 +75,12 @@
74
 
75
 
75
       <div class="screen-page--home-column-flex">
76
       <div class="screen-page--home-column-flex">
76
         <div class="screen-page--home-column-flex-item">
77
         <div class="screen-page--home-column-flex-item">
77
-          <div class="flex_title">牦牛存栏变动 གཡག་གསོལ་གྱི་འགྱུར་བ།</div>
78
+          <div class="flex_title flex_title--with-meta">
79
+            <span>牦牛存栏变动 གཡག་གསོལ་གྱི་འགྱུར་བ།</span>
80
+            <span v-if="inventoryChange" class="flex_title__meta">
81
+              公牛 {{ display(inventoryChange.bullCount) }} 头 · 母牛 {{ display(inventoryChange.cowCount) }} 头
82
+            </span>
83
+          </div>
78
           <div class="flex_content">
84
           <div class="flex_content">
79
             <ScreenChart :option="inventoryChartOption" />
85
             <ScreenChart :option="inventoryChartOption" />
80
           </div>
86
           </div>
@@ -103,7 +109,7 @@
103
                 <div class="grassland-stat__value">{{ display(dashboardGrassland?.availableCount) }}</div>
109
                 <div class="grassland-stat__value">{{ display(dashboardGrassland?.availableCount) }}</div>
104
               </div>
110
               </div>
105
               <div class="grassland-stat grassland-stat--inuse">
111
               <div class="grassland-stat grassland-stat--inuse">
106
-                <div class="grassland-stat__label">已用草场数</div>
112
+                <div class="grassland-stat__label">使用中草场数</div>
107
                 <div class="grassland-stat__value">{{ display(dashboardGrassland?.inUseCount) }}</div>
113
                 <div class="grassland-stat__value">{{ display(dashboardGrassland?.inUseCount) }}</div>
108
               </div>
114
               </div>
109
             </div>
115
             </div>
@@ -123,22 +129,19 @@
123
             <div class="yu">
129
             <div class="yu">
124
               <div class="yu_title">兽医人员</div>
130
               <div class="yu_title">兽医人员</div>
125
               <div class="yu_num">
131
               <div class="yu_num">
126
-                <!-- <strong>{{ display(appointment?.annualVetCount) }}</strong> 人 -->
127
-                <strong>150</strong> 人
132
+                <strong>{{ display(appointment?.annualVetCount) }}</strong> 人
128
               </div>
133
               </div>
129
             </div>
134
             </div>
130
             <div class="yu">
135
             <div class="yu">
131
               <div class="yu_title">专家人员</div>
136
               <div class="yu_title">专家人员</div>
132
               <div class="yu_num">
137
               <div class="yu_num">
133
-                <!-- <strong>{{ display(appointment?.annualExpertCount) }}</strong> 人 -->
134
-                <strong>1</strong> 人
138
+                <strong>{{ display(appointment?.annualExpertCount) }}</strong> 人
135
               </div>
139
               </div>
136
             </div>
140
             </div>
137
             <div class="yu">
141
             <div class="yu">
138
               <div class="yu_title">诊疗机构</div>
142
               <div class="yu_title">诊疗机构</div>
139
               <div class="yu_num">
143
               <div class="yu_num">
140
-                <!-- <strong>{{ display(appointment?.annualOrgCount) }}</strong> 个 -->
141
-                <strong>12</strong> 个
144
+                <strong>{{ display(appointment?.annualOrgCount) }}</strong> 个
142
               </div>
145
               </div>
143
             </div>
146
             </div>
144
           </div>
147
           </div>
@@ -237,12 +240,10 @@ const STANDARD_TABS = [
237
 
240
 
238
 const STANDARD_FETCH_PAGE_SIZE = 50
241
 const STANDARD_FETCH_PAGE_SIZE = 50
239
 
242
 
240
-/** 产值总览:农牧户固定展示值(不走接口) */
241
-const FARMER_COUNT = 8988
242
-
243
 const loading = ref(false)
243
 const loading = ref(false)
244
 const loadError = ref('')
244
 const loadError = ref('')
245
 const statYear = ref(String(new Date().getFullYear()))
245
 const statYear = ref(String(new Date().getFullYear()))
246
+const statDate = ref('')
246
 const availableYears = ref([])
247
 const availableYears = ref([])
247
 const overview = ref(null)
248
 const overview = ref(null)
248
 const inventoryChange = ref(null)
249
 const inventoryChange = ref(null)
@@ -307,6 +308,7 @@ function applyDashboard(data) {
307
   if (data.statYear) {
308
   if (data.statYear) {
308
     statYear.value = String(data.statYear)
309
     statYear.value = String(data.statYear)
309
   }
310
   }
311
+  statDate.value = data.statDate || ''
310
   availableYears.value = data.availableYears?.length
312
   availableYears.value = data.availableYears?.length
311
     ? [...data.availableYears]
313
     ? [...data.availableYears]
312
     : [Number(statYear.value)]
314
     : [Number(statYear.value)]
@@ -436,6 +438,11 @@ onMounted(() => {
436
   outline: none;
438
   outline: none;
437
 }
439
 }
438
 
440
 
441
+.home-year-bar__date {
442
+  font-size: 11px;
443
+  color: var(--screen-text-secondary, #a8d4c8);
444
+}
445
+
439
 .screen-page--home-column {
446
 .screen-page--home-column {
440
   width: 617px;
447
   width: 617px;
441
   height: 100%;
448
   height: 100%;
@@ -555,6 +562,22 @@ onMounted(() => {
555
   margin-bottom: 5px;
562
   margin-bottom: 5px;
556
 }
563
 }
557
 
564
 
565
+.flex_title--with-meta {
566
+  display: flex;
567
+  align-items: center;
568
+  justify-content: space-between;
569
+  padding-right: 8px;
570
+}
571
+
572
+.flex_title__meta {
573
+  flex-shrink: 0;
574
+  font-size: 10px;
575
+  color: #a8d4c8;
576
+  line-height: 1.2;
577
+  text-align: right;
578
+  max-width: 48%;
579
+}
580
+
558
 .flex_content {
581
 .flex_content {
559
   width: 100%;
582
   width: 100%;
560
   height: 180px;
583
   height: 180px;

+ 13 - 4
ruoyi-ui/src/api/dataModel/yakHerdInventory.js

@@ -51,6 +51,14 @@ export function listYakHerdInventoryYearOptions() {
51
   })
51
   })
52
 }
52
 }
53
 
53
 
54
+/** 可选月份(1~12) */
55
+export function listYakHerdInventoryMonthOptions() {
56
+  return request({
57
+    url: "/dataModel/yakHerdInventory/monthOptions",
58
+    method: "get"
59
+  })
60
+}
61
+
54
 /** 乡镇下拉 */
62
 /** 乡镇下拉 */
55
 export function listYakHerdInventoryTownOptions() {
63
 export function listYakHerdInventoryTownOptions() {
56
   return request({
64
   return request({
@@ -60,10 +68,11 @@ export function listYakHerdInventoryTownOptions() {
60
 }
68
 }
61
 
69
 
62
 /** Excel 导入(multipart/form-data,file 为 el-upload 的 raw File) */
70
 /** Excel 导入(multipart/form-data,file 为 el-upload 的 raw File) */
63
-export function importYakHerdInventory(file, statYear) {
71
+export function importYakHerdInventory(file, statYear, statMonth) {
64
   const formData = new FormData()
72
   const formData = new FormData()
65
   formData.append("file", file, file.name)
73
   formData.append("file", file, file.name)
66
   formData.append("statYear", String(statYear))
74
   formData.append("statYear", String(statYear))
75
+  formData.append("statMonth", String(statMonth))
67
   return request({
76
   return request({
68
     url: "/dataModel/yakHerdInventory/importData",
77
     url: "/dataModel/yakHerdInventory/importData",
69
     method: "post",
78
     method: "post",
@@ -83,12 +92,12 @@ export function downloadYakHerdInventoryTemplate() {
83
   })
92
   })
84
 }
93
 }
85
 
94
 
86
-/** 按年导出(GET,后端 @GetMapping("/export")) */
87
-export function exportYakHerdInventory(statYear) {
95
+/** 按年导出(GET,后端 @GetMapping("/export")) */
96
+export function exportYakHerdInventory(statYear, statMonth) {
88
   return request({
97
   return request({
89
     url: "/dataModel/yakHerdInventory/export",
98
     url: "/dataModel/yakHerdInventory/export",
90
     method: "get",
99
     method: "get",
91
-    params: { statYear },
100
+    params: { statYear, statMonth },
92
     responseType: "blob"
101
     responseType: "blob"
93
   })
102
   })
94
 }
103
 }

+ 13 - 4
ruoyi-ui/src/api/dataModel/yakOutboundReport.js

@@ -51,6 +51,14 @@ export function listYakOutboundReportYearOptions() {
51
   })
51
   })
52
 }
52
 }
53
 
53
 
54
+/** 可选月份(1~12) */
55
+export function listYakOutboundReportMonthOptions() {
56
+  return request({
57
+    url: "/dataModel/yakOutboundReport/monthOptions",
58
+    method: "get"
59
+  })
60
+}
61
+
54
 /** 乡镇下拉 */
62
 /** 乡镇下拉 */
55
 export function listYakOutboundReportTownOptions() {
63
 export function listYakOutboundReportTownOptions() {
56
   return request({
64
   return request({
@@ -60,10 +68,11 @@ export function listYakOutboundReportTownOptions() {
60
 }
68
 }
61
 
69
 
62
 /** Excel 导入(multipart/form-data,file 为 el-upload 的 raw File) */
70
 /** Excel 导入(multipart/form-data,file 为 el-upload 的 raw File) */
63
-export function importYakOutboundReport(file, statYear) {
71
+export function importYakOutboundReport(file, statYear, statMonth) {
64
   const formData = new FormData()
72
   const formData = new FormData()
65
   formData.append("file", file, file.name)
73
   formData.append("file", file, file.name)
66
   formData.append("statYear", String(statYear))
74
   formData.append("statYear", String(statYear))
75
+  formData.append("statMonth", String(statMonth))
67
   return request({
76
   return request({
68
     url: "/dataModel/yakOutboundReport/importData",
77
     url: "/dataModel/yakOutboundReport/importData",
69
     method: "post",
78
     method: "post",
@@ -83,12 +92,12 @@ export function downloadYakOutboundReportTemplate() {
83
   })
92
   })
84
 }
93
 }
85
 
94
 
86
-/** 按年导出(GET) */
87
-export function exportYakOutboundReport(statYear) {
95
+/** 按年导出(GET) */
96
+export function exportYakOutboundReport(statYear, statMonth) {
88
   return request({
97
   return request({
89
     url: "/dataModel/yakOutboundReport/export",
98
     url: "/dataModel/yakOutboundReport/export",
90
     method: "get",
99
     method: "get",
91
-    params: { statYear },
100
+    params: { statYear, statMonth },
92
     responseType: "blob"
101
     responseType: "blob"
93
   })
102
   })
94
 }
103
 }

+ 38 - 11
ruoyi-ui/src/components/EzvizHlsPlayer/index.vue

@@ -19,14 +19,15 @@ export default {
19
     containerSeq += 1
19
     containerSeq += 1
20
     return {
20
     return {
21
       containerId: `ezviz-hls-player-${containerSeq}`,
21
       containerId: `ezviz-hls-player-${containerSeq}`,
22
-      player: null,
23
       resizeObserver: null
22
       resizeObserver: null
24
     }
23
     }
25
   },
24
   },
26
   computed: {
25
   computed: {
27
     decoderStaticPath() {
26
     decoderStaticPath() {
28
-      const base = process.env.BASE_URL || "/"
29
-      return `${base}ezuikit-player-hls/`
27
+      if (process.env.NODE_ENV === "production") {
28
+        return "/client/"
29
+      }
30
+      return "/"
30
     }
31
     }
31
   },
32
   },
32
   watch: {
33
   watch: {
@@ -34,6 +35,10 @@ export default {
34
       this.rebuildPlayer(val)
35
       this.rebuildPlayer(val)
35
     }
36
     }
36
   },
37
   },
38
+  created() {
39
+    this.hlsPlayer = null
40
+    this.onPlayerParsed = null
41
+  },
37
   mounted() {
42
   mounted() {
38
     this.bindResizeObserver()
43
     this.bindResizeObserver()
39
     if (this.url) {
44
     if (this.url) {
@@ -78,22 +83,42 @@ export default {
78
       }
83
       }
79
     },
84
     },
80
     syncPlayerSize() {
85
     syncPlayerSize() {
81
-      if (!this.player || typeof this.player.resize !== "function") {
86
+      if (!this.hlsPlayer || typeof this.hlsPlayer.resize !== "function") {
82
         return
87
         return
83
       }
88
       }
84
       const { width, height } = this.getContainerSize()
89
       const { width, height } = this.getContainerSize()
85
       if (width > 0 && height > 0) {
90
       if (width > 0 && height > 0) {
86
-        this.player.resize(width, height)
91
+        this.hlsPlayer.resize(width, height)
92
+      }
93
+    },
94
+    bindPlayerEvents() {
95
+      if (!this.hlsPlayer || !this.hlsPlayer.event) {
96
+        return
97
+      }
98
+      this.onPlayerParsed = () => {
99
+        if (typeof this.hlsPlayer.play === "function") {
100
+          this.hlsPlayer.play().catch(() => {
101
+            this.$emit("error")
102
+          })
103
+        }
104
+      }
105
+      this.hlsPlayer.event.on("parsed", this.onPlayerParsed)
106
+    },
107
+    unbindPlayerEvents() {
108
+      if (this.hlsPlayer && this.hlsPlayer.event && this.onPlayerParsed) {
109
+        this.hlsPlayer.event.off("parsed", this.onPlayerParsed)
87
       }
110
       }
111
+      this.onPlayerParsed = null
88
     },
112
     },
89
     destroyPlayer() {
113
     destroyPlayer() {
90
-      if (this.player) {
114
+      this.unbindPlayerEvents()
115
+      if (this.hlsPlayer) {
91
         try {
116
         try {
92
-          this.player.destroy()
117
+          this.hlsPlayer.destroy()
93
         } catch (e) {
118
         } catch (e) {
94
           // ignore teardown errors
119
           // ignore teardown errors
95
         }
120
         }
96
-        this.player = null
121
+        this.hlsPlayer = null
97
       }
122
       }
98
       const el = this.$refs.containerRef
123
       const el = this.$refs.containerRef
99
       if (el) {
124
       if (el) {
@@ -127,15 +152,17 @@ export default {
127
         }
152
         }
128
         return
153
         return
129
       }
154
       }
130
-      this.player = new HlsPlayer({
155
+      this.hlsPlayer = new HlsPlayer({
131
         id: this.containerId,
156
         id: this.containerId,
132
         url: text,
157
         url: text,
133
-        staticPath: '/',
158
+        staticPath: this.decoderStaticPath,
134
         isLive: true,
159
         isLive: true,
160
+        autoPlay: true,
135
         width,
161
         width,
136
         height
162
         height
137
       })
163
       })
138
-      this.player.play().catch(() => {
164
+      this.bindPlayerEvents()
165
+      this.hlsPlayer.play().catch(() => {
139
         this.$emit("error")
166
         this.$emit("error")
140
       })
167
       })
141
     }
168
     }

+ 26 - 5
ruoyi-ui/src/lang/bo/dataModel.js

@@ -276,15 +276,19 @@ export default {
276
   },
276
   },
277
   yakHerdInventory: {
277
   yakHerdInventory: {
278
     queryStatYear: "ལོ་ཚད།",
278
     queryStatYear: "ལོ་ཚད།",
279
+    queryStatMonth: "ཟླ་ཚད།",
279
     queryTown: "ཤང་གྲོང་།",
280
     queryTown: "ཤང་གྲོང་།",
280
     colStatYear: "ལོ་ཚད།",
281
     colStatYear: "ལོ་ཚད།",
282
+    colStatMonth: "ཟླ་ཚད།",
281
     colTownName: "ཤང་གྲོང་།",
283
     colTownName: "ཤང་གྲོང་།",
284
+    statMonthLabel: "{month}ཟླ།",
282
     colVillageCount: "གྲོང་ཚོ་གྲངས།",
285
     colVillageCount: "གྲོང་ཚོ་གྲངས།",
283
     colHerdsmanHouseholdCount: "འབྲོག་པ་ཁྱིམ་གྲངས།",
286
     colHerdsmanHouseholdCount: "འབྲོག་པ་ཁྱིམ་གྲངས།",
284
     colYakTotal: "གནས་ཚད།",
287
     colYakTotal: "གནས་ཚད།",
285
     colBullTotal: "ཕོ་བ་གནས་ཚད།",
288
     colBullTotal: "ཕོ་བ་གནས་ཚད།",
286
     colCowTotal: "མོ་བ་གནས་ཚད།",
289
     colCowTotal: "མོ་བ་གནས་ཚད།",
287
     formStatYear: "ལོ་ཚད།",
290
     formStatYear: "ལོ་ཚད།",
291
+    formStatMonth: "ཟླ་ཚད།",
288
     formTown: "ཤང་གྲོང་།",
292
     formTown: "ཤང་གྲོང་།",
289
     formVillageCount: "གྲོང་ཚོ་གྲངས།",
293
     formVillageCount: "གྲོང་ཚོ་གྲངས།",
290
     formHerdsmanHouseholdCount: "འབྲོག་པ་ཁྱིམ་གྲངས།",
294
     formHerdsmanHouseholdCount: "འབྲོག་པ་ཁྱིམ་གྲངས།",
@@ -309,14 +313,19 @@ export default {
309
     exportTitle: "Excel 导出",
313
     exportTitle: "Excel 导出",
310
     exportStatYear: "所属年份",
314
     exportStatYear: "所属年份",
311
     exportStatYearPh: "请选择所属年份",
315
     exportStatYearPh: "请选择所属年份",
316
+    exportStatMonth: "所属月份",
317
+    exportStatMonthPh: "请选择所属月份",
312
     exportStatYearRequired: "请选择所属年份",
318
     exportStatYearRequired: "请选择所属年份",
313
-    exportStatYearInvalid: "年份不在可选范围",
319
+    exportStatMonthRequired: "请选择所属月份",
320
+    exportStatYearInvalid: "年月不在可选范围",
314
     exportLoading: "正在导出...",
321
     exportLoading: "正在导出...",
315
     exportFail: "导出失败",
322
     exportFail: "导出失败",
316
     importTitle: "Excel 导入",
323
     importTitle: "Excel 导入",
317
     importStatYear: "填报年份",
324
     importStatYear: "填报年份",
318
     importStatYearPh: "请选择年份",
325
     importStatYearPh: "请选择年份",
319
-    importTip: "先选年份再上传 .xlsx",
326
+    importStatMonth: "填报月份",
327
+    importStatMonthPh: "请选择月份",
328
+    importTip: "先选年月再上传 .xlsx",
320
     importLoading: "正在导入,请稍候...",
329
     importLoading: "正在导入,请稍候...",
321
     importSummary: "导入:新增 {insert} 更新 {update} 失败 {fail}",
330
     importSummary: "导入:新增 {insert} 更新 {update} 失败 {fail}",
322
     importWarnTitle: "警告",
331
     importWarnTitle: "警告",
@@ -329,6 +338,8 @@ export default {
329
     unitHousehold: "户",
338
     unitHousehold: "户",
330
     unitHead: "头",
339
     unitHead: "头",
331
     ruleStatYearRequired: "请选择年份",
340
     ruleStatYearRequired: "请选择年份",
341
+    ruleStatMonthRequired: "请选择月份",
342
+    ruleStatMonthFuture: "未来月份不可填",
332
     ruleTownRequired: "请选择乡镇",
343
     ruleTownRequired: "请选择乡镇",
333
     ruleCountRequired: "请填写",
344
     ruleCountRequired: "请填写",
334
     ruleCountNonNegative: "≥0 整数",
345
     ruleCountNonNegative: "≥0 整数",
@@ -338,14 +349,18 @@ export default {
338
   },
349
   },
339
   yakOutboundReport: {
350
   yakOutboundReport: {
340
     queryStatYear: "所属年份",
351
     queryStatYear: "所属年份",
352
+    queryStatMonth: "所属月份",
341
     queryTown: "所属乡镇",
353
     queryTown: "所属乡镇",
342
     colStatYear: "所属年份",
354
     colStatYear: "所属年份",
355
+    colStatMonth: "所属月份",
356
+    statMonthLabel: "{month}月",
343
     colTownName: "所属乡镇",
357
     colTownName: "所属乡镇",
344
     colFarmerHouseholdCount: "农牧户户数",
358
     colFarmerHouseholdCount: "农牧户户数",
345
     colFarmerPopulationCount: "农牧户人数",
359
     colFarmerPopulationCount: "农牧户人数",
346
     colYakOutboundCount: "牦牛出栏数",
360
     colYakOutboundCount: "牦牛出栏数",
347
     colSelfConsumptionCattle: "自食情况(牛)",
361
     colSelfConsumptionCattle: "自食情况(牛)",
348
     formStatYear: "所属年份",
362
     formStatYear: "所属年份",
363
+    formStatMonth: "所属月份",
349
     formTown: "所属乡镇",
364
     formTown: "所属乡镇",
350
     formFarmerHouseholdCount: "农牧户户数",
365
     formFarmerHouseholdCount: "农牧户户数",
351
     formFarmerPopulationCount: "农牧户人数",
366
     formFarmerPopulationCount: "农牧户人数",
@@ -369,14 +384,19 @@ export default {
369
     exportTitle: "Excel 导出",
384
     exportTitle: "Excel 导出",
370
     exportStatYear: "所属年份",
385
     exportStatYear: "所属年份",
371
     exportStatYearPh: "请选择所属年份",
386
     exportStatYearPh: "请选择所属年份",
387
+    exportStatMonth: "所属月份",
388
+    exportStatMonthPh: "请选择所属月份",
372
     exportStatYearRequired: "请选择所属年份",
389
     exportStatYearRequired: "请选择所属年份",
373
-    exportStatYearInvalid: "年份不在可选范围",
390
+    exportStatMonthRequired: "请选择所属月份",
391
+    exportStatYearInvalid: "导出年月不在可选范围",
374
     exportLoading: "正在导出...",
392
     exportLoading: "正在导出...",
375
     exportFail: "导出失败",
393
     exportFail: "导出失败",
376
     importTitle: "Excel 导入",
394
     importTitle: "Excel 导入",
377
     importStatYear: "填报年份",
395
     importStatYear: "填报年份",
378
     importStatYearPh: "请选择年份",
396
     importStatYearPh: "请选择年份",
379
-    importTip: "请先选择填报年份,再上传 .xlsx 文件",
397
+    importStatMonth: "填报月份",
398
+    importStatMonthPh: "请选择月份",
399
+    importTip: "请先选择填报年月,再上传 .xlsx 文件",
380
     importLoading: "正在导入...",
400
     importLoading: "正在导入...",
381
     importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
401
     importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
382
     importWarnTitle: "导入警告",
402
     importWarnTitle: "导入警告",
@@ -391,8 +411,9 @@ export default {
391
     unitSheep: "只",
411
     unitSheep: "只",
392
     unitHorse: "匹",
412
     unitHorse: "匹",
393
     ruleStatYearRequired: "请选择年份",
413
     ruleStatYearRequired: "请选择年份",
414
+    ruleStatMonthRequired: "请选择月份",
415
+    ruleStatMonthFuture: "不可填报未来月份",
394
     ruleTownRequired: "请选择乡镇",
416
     ruleTownRequired: "请选择乡镇",
395
-    ruleCountRequired: "请填写",
396
     ruleCountNonNegative: "≥0 整数"
417
     ruleCountNonNegative: "≥0 整数"
397
   }
418
   }
398
 }
419
 }

+ 2 - 1
ruoyi-ui/src/lang/bo/livestockIndustry.js

@@ -26,8 +26,9 @@ export default {
26
     bullCount: "ཕོ་གཡག་གྲངས།",
26
     bullCount: "ཕོ་གཡག་གྲངས།",
27
     cowCount: "མོ་གཡག་གྲངས།",
27
     cowCount: "མོ་གཡག་གྲངས།",
28
     unknownGenderHint: "ཕོ་མོ་མིང་བཀོད་མེད:{count} ཀོང་།",
28
     unknownGenderHint: "ཕོ་མོ་མིང་བཀོད་མེད:{count} ཀོང་།",
29
+    seriesBullCount: "ཕོ་གཡག",
30
+    seriesCowCount: "མོ་གཡག",
29
     chartInventoryTrend: "གནས་ཁུངས་འགྱུར་རྒྱས།",
31
     chartInventoryTrend: "གནས་ཁུངས་འགྱུར་རྒྱས།",
30
-    seriesInventoryCount: "གནས་ཁུངས་གྲངས།",
31
     chartAgeStructure: "ལོ་ཚད་གྲུབ་ཆ།",
32
     chartAgeStructure: "ལོ་ཚད་གྲུབ་ཆ།",
32
     grassAvailableCount: "བཀོལ་ཆོག་རྩྭ་ཁང་།",
33
     grassAvailableCount: "བཀོལ་ཆོག་རྩྭ་ཁང་།",
33
     grassInUseCount: "བཀོལ་སྤྱོད་རྩྭ་ཁང་།",
34
     grassInUseCount: "བཀོལ་སྤྱོད་རྩྭ་ཁང་།",

+ 26 - 5
ruoyi-ui/src/lang/zh/dataModel.js

@@ -276,15 +276,19 @@ export default {
276
   },
276
   },
277
   yakHerdInventory: {
277
   yakHerdInventory: {
278
     queryStatYear: "所属年份",
278
     queryStatYear: "所属年份",
279
+    queryStatMonth: "所属月份",
279
     queryTown: "所属乡镇",
280
     queryTown: "所属乡镇",
280
     colStatYear: "所属年份",
281
     colStatYear: "所属年份",
282
+    colStatMonth: "所属月份",
281
     colTownName: "乡镇",
283
     colTownName: "乡镇",
284
+    statMonthLabel: "{month}月",
282
     colVillageCount: "村居数",
285
     colVillageCount: "村居数",
283
     colHerdsmanHouseholdCount: "牧户数",
286
     colHerdsmanHouseholdCount: "牧户数",
284
     colYakTotal: "存栏数",
287
     colYakTotal: "存栏数",
285
     colBullTotal: "公牛存栏数",
288
     colBullTotal: "公牛存栏数",
286
     colCowTotal: "母牛存栏数",
289
     colCowTotal: "母牛存栏数",
287
     formStatYear: "所属年份",
290
     formStatYear: "所属年份",
291
+    formStatMonth: "所属月份",
288
     formTown: "所属乡镇",
292
     formTown: "所属乡镇",
289
     formVillageCount: "村居数",
293
     formVillageCount: "村居数",
290
     formHerdsmanHouseholdCount: "牧户数",
294
     formHerdsmanHouseholdCount: "牧户数",
@@ -309,14 +313,19 @@ export default {
309
     exportTitle: "Excel 导出",
313
     exportTitle: "Excel 导出",
310
     exportStatYear: "所属年份",
314
     exportStatYear: "所属年份",
311
     exportStatYearPh: "请选择所属年份",
315
     exportStatYearPh: "请选择所属年份",
316
+    exportStatMonth: "所属月份",
317
+    exportStatMonthPh: "请选择所属月份",
312
     exportStatYearRequired: "请选择所属年份",
318
     exportStatYearRequired: "请选择所属年份",
313
-    exportStatYearInvalid: "导出年份不在可选范围内",
319
+    exportStatMonthRequired: "请选择所属月份",
320
+    exportStatYearInvalid: "导出年月不在可选范围内",
314
     exportLoading: "正在导出,请稍候...",
321
     exportLoading: "正在导出,请稍候...",
315
     exportFail: "导出失败",
322
     exportFail: "导出失败",
316
     importTitle: "Excel 导入",
323
     importTitle: "Excel 导入",
317
     importStatYear: "填报年份",
324
     importStatYear: "填报年份",
318
     importStatYearPh: "请选择填报年份",
325
     importStatYearPh: "请选择填报年份",
319
-    importTip: "请先选择填报年份,再上传与模板格式一致的 .xlsx 文件",
326
+    importStatMonth: "填报月份",
327
+    importStatMonthPh: "请选择填报月份",
328
+    importTip: "请先选择填报年月,再上传与模板格式一致的 .xlsx 文件",
320
     importLoading: "正在导入,请稍候...",
329
     importLoading: "正在导入,请稍候...",
321
     importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
330
     importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
322
     importWarnTitle: "导入警告",
331
     importWarnTitle: "导入警告",
@@ -329,6 +338,8 @@ export default {
329
     unitHousehold: "户",
338
     unitHousehold: "户",
330
     unitHead: "头",
339
     unitHead: "头",
331
     ruleStatYearRequired: "请选择所属年份",
340
     ruleStatYearRequired: "请选择所属年份",
341
+    ruleStatMonthRequired: "请选择所属月份",
342
+    ruleStatMonthFuture: "不可填报未来月份",
332
     ruleTownRequired: "请选择所属乡镇",
343
     ruleTownRequired: "请选择所属乡镇",
333
     ruleCountRequired: "请填写该项",
344
     ruleCountRequired: "请填写该项",
334
     ruleCountNonNegative: "须为大于等于 0 的整数",
345
     ruleCountNonNegative: "须为大于等于 0 的整数",
@@ -338,14 +349,18 @@ export default {
338
   },
349
   },
339
   yakOutboundReport: {
350
   yakOutboundReport: {
340
     queryStatYear: "所属年份",
351
     queryStatYear: "所属年份",
352
+    queryStatMonth: "所属月份",
341
     queryTown: "所属乡镇",
353
     queryTown: "所属乡镇",
342
     colStatYear: "所属年份",
354
     colStatYear: "所属年份",
355
+    colStatMonth: "所属月份",
356
+    statMonthLabel: "{month}月",
343
     colTownName: "所属乡镇",
357
     colTownName: "所属乡镇",
344
     colFarmerHouseholdCount: "农牧户户数",
358
     colFarmerHouseholdCount: "农牧户户数",
345
     colFarmerPopulationCount: "农牧户人数",
359
     colFarmerPopulationCount: "农牧户人数",
346
     colYakOutboundCount: "牦牛出栏数",
360
     colYakOutboundCount: "牦牛出栏数",
347
     colSelfConsumptionCattle: "自食情况(牛)",
361
     colSelfConsumptionCattle: "自食情况(牛)",
348
     formStatYear: "所属年份",
362
     formStatYear: "所属年份",
363
+    formStatMonth: "所属月份",
349
     formTown: "所属乡镇",
364
     formTown: "所属乡镇",
350
     formFarmerHouseholdCount: "农牧户户数",
365
     formFarmerHouseholdCount: "农牧户户数",
351
     formFarmerPopulationCount: "农牧户人数",
366
     formFarmerPopulationCount: "农牧户人数",
@@ -369,14 +384,19 @@ export default {
369
     exportTitle: "Excel 导出",
384
     exportTitle: "Excel 导出",
370
     exportStatYear: "所属年份",
385
     exportStatYear: "所属年份",
371
     exportStatYearPh: "请选择所属年份",
386
     exportStatYearPh: "请选择所属年份",
387
+    exportStatMonth: "所属月份",
388
+    exportStatMonthPh: "请选择所属月份",
372
     exportStatYearRequired: "请选择所属年份",
389
     exportStatYearRequired: "请选择所属年份",
373
-    exportStatYearInvalid: "导出年份不在可选范围内",
390
+    exportStatMonthRequired: "请选择所属月份",
391
+    exportStatYearInvalid: "导出年月不在可选范围内",
374
     exportLoading: "正在导出,请稍候...",
392
     exportLoading: "正在导出,请稍候...",
375
     exportFail: "导出失败",
393
     exportFail: "导出失败",
376
     importTitle: "Excel 导入",
394
     importTitle: "Excel 导入",
377
     importStatYear: "填报年份",
395
     importStatYear: "填报年份",
378
     importStatYearPh: "请选择填报年份",
396
     importStatYearPh: "请选择填报年份",
379
-    importTip: "请先选择填报年份,再上传与模板格式一致的 .xlsx 文件",
397
+    importStatMonth: "填报月份",
398
+    importStatMonthPh: "请选择填报月份",
399
+    importTip: "请先选择填报年月,再上传与模板格式一致的 .xlsx 文件",
380
     importLoading: "正在导入,请稍候...",
400
     importLoading: "正在导入,请稍候...",
381
     importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
401
     importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
382
     importWarnTitle: "导入警告",
402
     importWarnTitle: "导入警告",
@@ -391,8 +411,9 @@ export default {
391
     unitSheep: "只",
411
     unitSheep: "只",
392
     unitHorse: "匹",
412
     unitHorse: "匹",
393
     ruleStatYearRequired: "请选择所属年份",
413
     ruleStatYearRequired: "请选择所属年份",
414
+    ruleStatMonthRequired: "请选择所属月份",
415
+    ruleStatMonthFuture: "不可填报未来月份",
394
     ruleTownRequired: "请选择所属乡镇",
416
     ruleTownRequired: "请选择所属乡镇",
395
-    ruleCountRequired: "请填写该项",
396
     ruleCountNonNegative: "须为大于等于 0 的整数"
417
     ruleCountNonNegative: "须为大于等于 0 的整数"
397
   }
418
   }
398
 }
419
 }

+ 2 - 1
ruoyi-ui/src/lang/zh/livestockIndustry.js

@@ -26,8 +26,9 @@ export default {
26
     bullCount: "公牛数量",
26
     bullCount: "公牛数量",
27
     cowCount: "母牛数量",
27
     cowCount: "母牛数量",
28
     unknownGenderHint: "未标注性别:{count} 头",
28
     unknownGenderHint: "未标注性别:{count} 头",
29
+    seriesBullCount: "公牛",
30
+    seriesCowCount: "母牛",
29
     chartInventoryTrend: "存栏趋势",
31
     chartInventoryTrend: "存栏趋势",
30
-    seriesInventoryCount: "存栏数量",
31
     chartAgeStructure: "年龄结构占比",
32
     chartAgeStructure: "年龄结构占比",
32
     grassAvailableCount: "可用草场",
33
     grassAvailableCount: "可用草场",
33
     grassInUseCount: "使用中草场",
34
     grassInUseCount: "使用中草场",

+ 141 - 12
ruoyi-ui/src/views/dataModel/yakHerdInventory/index.vue

@@ -8,6 +8,12 @@
8
             <el-option v-for="y in yearOptions" :key="y" :label="String(y)" :value="y" />
8
             <el-option v-for="y in yearOptions" :key="y" :label="String(y)" :value="y" />
9
           </el-select>
9
           </el-select>
10
         </el-form-item>
10
         </el-form-item>
11
+        <el-form-item prop="statMonth">
12
+          <template slot="label">{{ dmT("queryStatMonth") }}</template>
13
+          <el-select v-model="queryParams.statMonth" :placeholder="dmCommon('pleaseSelect')" clearable style="width: 120px">
14
+            <el-option v-for="m in monthOptions" :key="'q-' + m" :label="formatStatMonth(m)" :value="m" />
15
+          </el-select>
16
+        </el-form-item>
11
         <el-form-item prop="townDeptId">
17
         <el-form-item prop="townDeptId">
12
           <template slot="label">{{ dmT("queryTown") }}</template>
18
           <template slot="label">{{ dmT("queryTown") }}</template>
13
           <el-select v-model="queryParams.townDeptId" :placeholder="dmCommon('pleaseSelect')" clearable filterable style="width: 180px">
19
           <el-select v-model="queryParams.townDeptId" :placeholder="dmCommon('pleaseSelect')" clearable filterable style="width: 180px">
@@ -53,6 +59,9 @@
53
           <span>{{ dmT("emptyList") }}</span>
59
           <span>{{ dmT("emptyList") }}</span>
54
         </template>
60
         </template>
55
         <el-table-column :label="dmT('colStatYear')" prop="statYear" align="center" width="100" />
61
         <el-table-column :label="dmT('colStatYear')" prop="statYear" align="center" width="100" />
62
+        <el-table-column :label="dmT('colStatMonth')" align="center" width="90">
63
+          <template slot-scope="scope">{{ formatStatMonth(scope.row.statMonth) }}</template>
64
+        </el-table-column>
56
         <el-table-column :label="dmT('colTownName')" prop="townName" align="center" min-width="120" :show-overflow-tooltip="true" />
65
         <el-table-column :label="dmT('colTownName')" prop="townName" align="center" min-width="120" :show-overflow-tooltip="true" />
57
         <el-table-column :label="dmT('colVillageCount')" align="center" width="90">
66
         <el-table-column :label="dmT('colVillageCount')" align="center" width="90">
58
           <template slot-scope="scope">{{ formatCount(scope.row.villageCount) }}</template>
67
           <template slot-scope="scope">{{ formatCount(scope.row.villageCount) }}</template>
@@ -90,7 +99,7 @@
90
       <el-form :key="formKey" ref="form" :model="form" label-width="150px" size="small">
99
       <el-form :key="formKey" ref="form" :model="form" label-width="150px" size="small">
91
         <div class="yhi-section-title">{{ dmT("sectionBasic") }}</div>
100
         <div class="yhi-section-title">{{ dmT("sectionBasic") }}</div>
92
         <el-row :gutter="16">
101
         <el-row :gutter="16">
93
-          <el-col :span="12">
102
+          <el-col :span="8">
94
             <el-form-item prop="statYear" :rules="formRules.statYear">
103
             <el-form-item prop="statYear" :rules="formRules.statYear">
95
               <template slot="label">{{ dmT("formStatYear") }}</template>
104
               <template slot="label">{{ dmT("formStatYear") }}</template>
96
               <el-select v-model="form.statYear" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
105
               <el-select v-model="form.statYear" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
@@ -98,7 +107,15 @@
98
               </el-select>
107
               </el-select>
99
             </el-form-item>
108
             </el-form-item>
100
           </el-col>
109
           </el-col>
101
-          <el-col :span="12">
110
+          <el-col :span="8">
111
+            <el-form-item prop="statMonth" :rules="formRules.statMonth">
112
+              <template slot="label">{{ dmT("formStatMonth") }}</template>
113
+              <el-select v-model="form.statMonth" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
114
+                <el-option v-for="m in monthOptions" :key="'fm-' + m" :label="formatStatMonth(m)" :value="m" />
115
+              </el-select>
116
+            </el-form-item>
117
+          </el-col>
118
+          <el-col :span="8">
102
             <el-form-item prop="townDeptId" :rules="formRules.townDeptId">
119
             <el-form-item prop="townDeptId" :rules="formRules.townDeptId">
103
               <template slot="label">{{ dmT("formTown") }}</template>
120
               <template slot="label">{{ dmT("formTown") }}</template>
104
               <el-select v-model="form.townDeptId" :placeholder="dmCommon('pleaseSelect')" filterable style="width: 100%">
121
               <el-select v-model="form.townDeptId" :placeholder="dmCommon('pleaseSelect')" filterable style="width: 100%">
@@ -145,11 +162,26 @@
145
               <span>{{ viewRow.statYear || dash }}</span>
162
               <span>{{ viewRow.statYear || dash }}</span>
146
             </el-form-item>
163
             </el-form-item>
147
           </el-col>
164
           </el-col>
165
+          <el-col :span="12">
166
+            <el-form-item :label="dmT('formStatMonth')">
167
+              <span>{{ formatStatMonth(viewRow.statMonth) }}</span>
168
+            </el-form-item>
169
+          </el-col>
148
           <el-col :span="12">
170
           <el-col :span="12">
149
             <el-form-item :label="dmT('formTown')">
171
             <el-form-item :label="dmT('formTown')">
150
               <span>{{ viewRow.townName || dash }}</span>
172
               <span>{{ viewRow.townName || dash }}</span>
151
             </el-form-item>
173
             </el-form-item>
152
           </el-col>
174
           </el-col>
175
+          <el-col :span="12">
176
+            <el-form-item :label="dmT('formVillageDeptId')">
177
+              <span>{{ viewRow.villageDeptId || dash }}</span>
178
+            </el-form-item>
179
+          </el-col>
180
+          <el-col :span="12">
181
+            <el-form-item :label="dmT('formVillageName')">
182
+              <span>{{ viewRow.villageName || dash }}</span>
183
+            </el-form-item>
184
+          </el-col>
153
           <el-col v-for="field in countFields" :key="'v-' + field.prop" :span="12">
185
           <el-col v-for="field in countFields" :key="'v-' + field.prop" :span="12">
154
             <el-form-item :label="dmT(field.labelKey)">
186
             <el-form-item :label="dmT(field.labelKey)">
155
               <span>{{ formatCount(viewRow[field.prop]) }}</span>
187
               <span>{{ formatCount(viewRow[field.prop]) }}</span>
@@ -200,6 +232,12 @@
200
             <el-option v-for="y in yearOptions" :key="'i-' + y" :label="String(y)" :value="y" />
232
             <el-option v-for="y in yearOptions" :key="'i-' + y" :label="String(y)" :value="y" />
201
           </el-select>
233
           </el-select>
202
         </el-form-item>
234
         </el-form-item>
235
+        <el-form-item prop="statMonth" :rules="importRules.statMonth">
236
+          <template slot="label">{{ dmT("importStatMonth") }}</template>
237
+          <el-select v-model="importForm.statMonth" :placeholder="dmT('importStatMonthPh')" style="width: 100%">
238
+            <el-option v-for="m in monthOptions" :key="'im-' + m" :label="formatStatMonth(m)" :value="m" />
239
+          </el-select>
240
+        </el-form-item>
203
         <p class="yhi-import-tip">{{ dmT("importTip") }}</p>
241
         <p class="yhi-import-tip">{{ dmT("importTip") }}</p>
204
         <el-upload
242
         <el-upload
205
           ref="importUpload"
243
           ref="importUpload"
@@ -230,6 +268,12 @@
230
             <el-option v-for="y in yearOptions" :key="'e-' + y" :label="String(y)" :value="y" />
268
             <el-option v-for="y in yearOptions" :key="'e-' + y" :label="String(y)" :value="y" />
231
           </el-select>
269
           </el-select>
232
         </el-form-item>
270
         </el-form-item>
271
+        <el-form-item prop="statMonth" :rules="exportRules.statMonth">
272
+          <template slot="label">{{ dmT("exportStatMonth") }}</template>
273
+          <el-select v-model="exportForm.statMonth" :placeholder="dmT('exportStatMonthPh')" style="width: 100%">
274
+            <el-option v-for="m in monthOptions" :key="'em-' + m" :label="formatStatMonth(m)" :value="m" />
275
+          </el-select>
276
+        </el-form-item>
233
       </el-form>
277
       </el-form>
234
       <div slot="footer" class="dialog-footer">
278
       <div slot="footer" class="dialog-footer">
235
         <el-button type="primary" :loading="exporting" @click="handleExportSubmit">{{ dmCommon("ok") }}</el-button>
279
         <el-button type="primary" :loading="exporting" @click="handleExportSubmit">{{ dmCommon("ok") }}</el-button>
@@ -248,6 +292,7 @@ import {
248
   updateYakHerdInventory,
292
   updateYakHerdInventory,
249
   delYakHerdInventory,
293
   delYakHerdInventory,
250
   listYakHerdInventoryYearOptions,
294
   listYakHerdInventoryYearOptions,
295
+  listYakHerdInventoryMonthOptions,
251
   listYakHerdInventoryTownOptions,
296
   listYakHerdInventoryTownOptions,
252
   importYakHerdInventory,
297
   importYakHerdInventory,
253
   downloadYakHerdInventoryTemplate,
298
   downloadYakHerdInventoryTemplate,
@@ -270,10 +315,19 @@ const COUNT_FIELDS = [
270
   { prop: "cowOver12Count", labelKey: "formCowOver12Count", unitKey: "unitHead" }
315
   { prop: "cowOver12Count", labelKey: "formCowOver12Count", unitKey: "unitHead" }
271
 ]
316
 ]
272
 
317
 
318
+function getCurrentYearMonth() {
319
+  const now = new Date()
320
+  return {
321
+    statYear: now.getFullYear(),
322
+    statMonth: now.getMonth() + 1
323
+  }
324
+}
325
+
273
 function createEmptyForm() {
326
 function createEmptyForm() {
274
   const form = {
327
   const form = {
275
     id: undefined,
328
     id: undefined,
276
     statYear: undefined,
329
     statYear: undefined,
330
+    statMonth: undefined,
277
     townDeptId: undefined,
331
     townDeptId: undefined,
278
     remark: undefined
332
     remark: undefined
279
   }
333
   }
@@ -310,18 +364,22 @@ export default {
310
       total: 0,
364
       total: 0,
311
       tableList: [],
365
       tableList: [],
312
       yearOptions: [],
366
       yearOptions: [],
367
+      monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
313
       townOptions: [],
368
       townOptions: [],
314
       viewRow: {},
369
       viewRow: {},
315
       importForm: {
370
       importForm: {
316
-        statYear: undefined
371
+        statYear: undefined,
372
+        statMonth: undefined
317
       },
373
       },
318
       exportForm: {
374
       exportForm: {
319
-        statYear: undefined
375
+        statYear: undefined,
376
+        statMonth: undefined
320
       },
377
       },
321
       queryParams: {
378
       queryParams: {
322
         pageNum: 1,
379
         pageNum: 1,
323
         pageSize: 20,
380
         pageSize: 20,
324
         statYear: undefined,
381
         statYear: undefined,
382
+        statMonth: undefined,
325
         townDeptId: undefined
383
         townDeptId: undefined
326
       },
384
       },
327
       form: createEmptyForm()
385
       form: createEmptyForm()
@@ -340,6 +398,10 @@ export default {
340
     formRules() {
398
     formRules() {
341
       return {
399
       return {
342
         statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }],
400
         statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }],
401
+        statMonth: [
402
+          { required: true, message: this.dmT("ruleStatMonthRequired"), trigger: "change" },
403
+          { validator: this.validateFormStatMonth, trigger: "change" }
404
+        ],
343
         townDeptId: [{ required: true, message: this.dmT("ruleTownRequired"), trigger: "change" }],
405
         townDeptId: [{ required: true, message: this.dmT("ruleTownRequired"), trigger: "change" }],
344
         count: [
406
         count: [
345
           { required: true, message: this.dmT("ruleCountRequired"), trigger: "change" },
407
           { required: true, message: this.dmT("ruleCountRequired"), trigger: "change" },
@@ -350,12 +412,20 @@ export default {
350
     },
412
     },
351
     importRules() {
413
     importRules() {
352
       return {
414
       return {
353
-        statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }]
415
+        statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }],
416
+        statMonth: [
417
+          { required: true, message: this.dmT("ruleStatMonthRequired"), trigger: "change" },
418
+          { validator: this.validateImportStatMonth, trigger: "change" }
419
+        ]
354
       }
420
       }
355
     },
421
     },
356
     exportRules() {
422
     exportRules() {
357
       return {
423
       return {
358
-        statYear: [{ required: true, message: this.dmT("exportStatYearRequired"), trigger: "change" }]
424
+        statYear: [{ required: true, message: this.dmT("exportStatYearRequired"), trigger: "change" }],
425
+        statMonth: [
426
+          { required: true, message: this.dmT("exportStatMonthRequired"), trigger: "change" },
427
+          { validator: this.validateExportStatMonth, trigger: "change" }
428
+        ]
359
       }
429
       }
360
     }
430
     }
361
   },
431
   },
@@ -368,10 +438,54 @@ export default {
368
       listYakHerdInventoryYearOptions().then((res) => {
438
       listYakHerdInventoryYearOptions().then((res) => {
369
         this.yearOptions = Array.isArray(res.data) ? res.data : []
439
         this.yearOptions = Array.isArray(res.data) ? res.data : []
370
       })
440
       })
441
+      listYakHerdInventoryMonthOptions().then((res) => {
442
+        if (Array.isArray(res.data) && res.data.length) {
443
+          this.monthOptions = res.data
444
+        }
445
+      })
371
       listYakHerdInventoryTownOptions().then((res) => {
446
       listYakHerdInventoryTownOptions().then((res) => {
372
         this.townOptions = Array.isArray(res.data) ? res.data : []
447
         this.townOptions = Array.isArray(res.data) ? res.data : []
373
       })
448
       })
374
     },
449
     },
450
+    formatStatMonth(month) {
451
+      if (month == null || month === "") {
452
+        return this.dash
453
+      }
454
+      return this.dmT("statMonthLabel", { month })
455
+    },
456
+    isFutureStatPeriod(statYear, statMonth) {
457
+      if (statYear == null || statMonth == null) {
458
+        return false
459
+      }
460
+      const now = new Date()
461
+      const currentYear = now.getFullYear()
462
+      const currentMonth = now.getMonth() + 1
463
+      if (statYear > currentYear) {
464
+        return true
465
+      }
466
+      return statYear === currentYear && statMonth > currentMonth
467
+    },
468
+    validateFormStatMonth(rule, value, callback) {
469
+      if (this.isFutureStatPeriod(this.form.statYear, value)) {
470
+        callback(new Error(this.dmT("ruleStatMonthFuture")))
471
+        return
472
+      }
473
+      callback()
474
+    },
475
+    validateImportStatMonth(rule, value, callback) {
476
+      if (this.isFutureStatPeriod(this.importForm.statYear, value)) {
477
+        callback(new Error(this.dmT("ruleStatMonthFuture")))
478
+        return
479
+      }
480
+      callback()
481
+    },
482
+    validateExportStatMonth(rule, value, callback) {
483
+      if (this.isFutureStatPeriod(this.exportForm.statYear, value)) {
484
+        callback(new Error(this.dmT("ruleStatMonthFuture")))
485
+        return
486
+      }
487
+      callback()
488
+    },
375
     formatCount(val) {
489
     formatCount(val) {
376
       if (val == null || val === "") {
490
       if (val == null || val === "") {
377
         return this.dash
491
         return this.dash
@@ -397,6 +511,9 @@ export default {
397
       if (this.queryParams.statYear != null) {
511
       if (this.queryParams.statYear != null) {
398
         q.statYear = this.queryParams.statYear
512
         q.statYear = this.queryParams.statYear
399
       }
513
       }
514
+      if (this.queryParams.statMonth != null) {
515
+        q.statMonth = this.queryParams.statMonth
516
+      }
400
       if (this.queryParams.townDeptId != null) {
517
       if (this.queryParams.townDeptId != null) {
401
         q.townDeptId = this.queryParams.townDeptId
518
         q.townDeptId = this.queryParams.townDeptId
402
       }
519
       }
@@ -406,6 +523,7 @@ export default {
406
       const remarkText = this.form.remark != null ? String(this.form.remark).trim() : ""
523
       const remarkText = this.form.remark != null ? String(this.form.remark).trim() : ""
407
       const payload = {
524
       const payload = {
408
         statYear: this.form.statYear,
525
         statYear: this.form.statYear,
526
+        statMonth: this.form.statMonth,
409
         townDeptId: this.form.townDeptId,
527
         townDeptId: this.form.townDeptId,
410
         remark: remarkText !== "" ? remarkText : undefined
528
         remark: remarkText !== "" ? remarkText : undefined
411
       }
529
       }
@@ -431,13 +549,17 @@ export default {
431
     },
549
     },
432
     resetQuery() {
550
     resetQuery() {
433
       this.queryParams.statYear = undefined
551
       this.queryParams.statYear = undefined
552
+      this.queryParams.statMonth = undefined
434
       this.queryParams.townDeptId = undefined
553
       this.queryParams.townDeptId = undefined
435
       this.resetForm("queryForm")
554
       this.resetForm("queryForm")
436
       this.handleQuery()
555
       this.handleQuery()
437
     },
556
     },
438
     handleAdd() {
557
     handleAdd() {
439
       this.formKey += 1
558
       this.formKey += 1
559
+      const current = getCurrentYearMonth()
440
       this.form = createEmptyForm()
560
       this.form = createEmptyForm()
561
+      this.form.statYear = current.statYear
562
+      this.form.statMonth = current.statMonth
441
       this.dialogEdit = false
563
       this.dialogEdit = false
442
       this.open = true
564
       this.open = true
443
       this.$nextTick(() => {
565
       this.$nextTick(() => {
@@ -508,7 +630,9 @@ export default {
508
       this.form = createEmptyForm()
630
       this.form = createEmptyForm()
509
     },
631
     },
510
     handleImportOpen() {
632
     handleImportOpen() {
511
-      this.importForm.statYear = this.queryParams.statYear
633
+      const current = getCurrentYearMonth()
634
+      this.importForm.statYear = current.statYear
635
+      this.importForm.statMonth = current.statMonth
512
       this.importOpen = true
636
       this.importOpen = true
513
       this.$nextTick(() => {
637
       this.$nextTick(() => {
514
         this.resetImportFiles()
638
         this.resetImportFiles()
@@ -522,6 +646,7 @@ export default {
522
     resetImport() {
646
     resetImport() {
523
       this.importing = false
647
       this.importing = false
524
       this.importForm.statYear = undefined
648
       this.importForm.statYear = undefined
649
+      this.importForm.statMonth = undefined
525
       this.resetImportFiles()
650
       this.resetImportFiles()
526
     },
651
     },
527
     handleDownloadTemplate() {
652
     handleDownloadTemplate() {
@@ -545,7 +670,9 @@ export default {
545
         })
670
         })
546
     },
671
     },
547
     handleExportOpen() {
672
     handleExportOpen() {
548
-      this.exportForm.statYear = this.queryParams.statYear
673
+      const current = getCurrentYearMonth()
674
+      this.exportForm.statYear = current.statYear
675
+      this.exportForm.statMonth = current.statMonth
549
       this.exportOpen = true
676
       this.exportOpen = true
550
       this.$nextTick(() => {
677
       this.$nextTick(() => {
551
         if (this.$refs.exportFormRef) {
678
         if (this.$refs.exportFormRef) {
@@ -556,6 +683,7 @@ export default {
556
     resetExport() {
683
     resetExport() {
557
       this.exporting = false
684
       this.exporting = false
558
       this.exportForm.statYear = undefined
685
       this.exportForm.statYear = undefined
686
+      this.exportForm.statMonth = undefined
559
     },
687
     },
560
     handleExportSubmit() {
688
     handleExportSubmit() {
561
       this.$refs.exportFormRef.validate((valid) => {
689
       this.$refs.exportFormRef.validate((valid) => {
@@ -563,14 +691,15 @@ export default {
563
           return
691
           return
564
         }
692
         }
565
         const statYear = this.exportForm.statYear
693
         const statYear = this.exportForm.statYear
566
-        if (!this.yearOptions.includes(statYear)) {
694
+        const statMonth = this.exportForm.statMonth
695
+        if (!this.yearOptions.includes(statYear) || !this.monthOptions.includes(statMonth)) {
567
           this.$modal.msgError(this.dmT("exportStatYearInvalid"))
696
           this.$modal.msgError(this.dmT("exportStatYearInvalid"))
568
           return
697
           return
569
         }
698
         }
570
-        const filename = `${statYear}年牦牛存栏数据.xlsx`
699
+        const filename = `${statYear}年${statMonth}月牦牛存栏数据.xlsx`
571
         this.exporting = true
700
         this.exporting = true
572
         this.$modal.loading(this.dmT("exportLoading"))
701
         this.$modal.loading(this.dmT("exportLoading"))
573
-        exportYakHerdInventory(statYear)
702
+        exportYakHerdInventory(statYear, statMonth)
574
           .then(async (data) => {
703
           .then(async (data) => {
575
             if (blobValidate(data)) {
704
             if (blobValidate(data)) {
576
               saveAs(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), filename)
705
               saveAs(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), filename)
@@ -609,7 +738,7 @@ export default {
609
         }
738
         }
610
         this.importing = true
739
         this.importing = true
611
         this.$modal.loading(this.dmT("importLoading"))
740
         this.$modal.loading(this.dmT("importLoading"))
612
-        importYakHerdInventory(raw, this.importForm.statYear)
741
+        importYakHerdInventory(raw, this.importForm.statYear, this.importForm.statMonth)
613
           .then((res) => {
742
           .then((res) => {
614
             this.importOpen = false
743
             this.importOpen = false
615
             this.showImportResult(res.data || {})
744
             this.showImportResult(res.data || {})

+ 134 - 18
ruoyi-ui/src/views/dataModel/yakOutboundReport/index.vue

@@ -8,6 +8,12 @@
8
             <el-option v-for="y in yearOptions" :key="y" :label="String(y)" :value="y" />
8
             <el-option v-for="y in yearOptions" :key="y" :label="String(y)" :value="y" />
9
           </el-select>
9
           </el-select>
10
         </el-form-item>
10
         </el-form-item>
11
+        <el-form-item prop="statMonth">
12
+          <template slot="label">{{ dmT("queryStatMonth") }}</template>
13
+          <el-select v-model="queryParams.statMonth" :placeholder="dmCommon('pleaseSelect')" clearable style="width: 120px">
14
+            <el-option v-for="m in monthOptions" :key="'q-' + m" :label="formatStatMonth(m)" :value="m" />
15
+          </el-select>
16
+        </el-form-item>
11
         <el-form-item prop="townDeptId">
17
         <el-form-item prop="townDeptId">
12
           <template slot="label">{{ dmT("queryTown") }}</template>
18
           <template slot="label">{{ dmT("queryTown") }}</template>
13
           <el-select v-model="queryParams.townDeptId" :placeholder="dmCommon('pleaseSelect')" clearable filterable style="width: 180px">
19
           <el-select v-model="queryParams.townDeptId" :placeholder="dmCommon('pleaseSelect')" clearable filterable style="width: 180px">
@@ -53,6 +59,9 @@
53
           <span>{{ dmT("emptyList") }}</span>
59
           <span>{{ dmT("emptyList") }}</span>
54
         </template>
60
         </template>
55
         <el-table-column :label="dmT('colStatYear')" prop="statYear" align="center" width="100" />
61
         <el-table-column :label="dmT('colStatYear')" prop="statYear" align="center" width="100" />
62
+        <el-table-column :label="dmT('colStatMonth')" align="center" width="90">
63
+          <template slot-scope="scope">{{ formatStatMonth(scope.row.statMonth) }}</template>
64
+        </el-table-column>
56
         <el-table-column :label="dmT('colTownName')" prop="townName" align="center" min-width="120" :show-overflow-tooltip="true" />
65
         <el-table-column :label="dmT('colTownName')" prop="townName" align="center" min-width="120" :show-overflow-tooltip="true" />
57
         <el-table-column :label="dmT('colFarmerHouseholdCount')" align="center" width="100">
66
         <el-table-column :label="dmT('colFarmerHouseholdCount')" align="center" width="100">
58
           <template slot-scope="scope">{{ formatCount(scope.row.farmerHouseholdCount) }}</template>
67
           <template slot-scope="scope">{{ formatCount(scope.row.farmerHouseholdCount) }}</template>
@@ -87,7 +96,7 @@
87
       <el-form :key="formKey" ref="form" :model="form" label-width="168px" size="small">
96
       <el-form :key="formKey" ref="form" :model="form" label-width="168px" size="small">
88
         <div class="yor-section-title">{{ dmT("sectionBasic") }}</div>
97
         <div class="yor-section-title">{{ dmT("sectionBasic") }}</div>
89
         <el-row :gutter="16">
98
         <el-row :gutter="16">
90
-          <el-col :span="12">
99
+          <el-col :span="8">
91
             <el-form-item prop="statYear" :rules="formRules.statYear">
100
             <el-form-item prop="statYear" :rules="formRules.statYear">
92
               <template slot="label">{{ dmT("formStatYear") }}</template>
101
               <template slot="label">{{ dmT("formStatYear") }}</template>
93
               <el-select v-model="form.statYear" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
102
               <el-select v-model="form.statYear" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
@@ -95,7 +104,15 @@
95
               </el-select>
104
               </el-select>
96
             </el-form-item>
105
             </el-form-item>
97
           </el-col>
106
           </el-col>
98
-          <el-col :span="12">
107
+          <el-col :span="8">
108
+            <el-form-item prop="statMonth" :rules="formRules.statMonth">
109
+              <template slot="label">{{ dmT("formStatMonth") }}</template>
110
+              <el-select v-model="form.statMonth" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
111
+                <el-option v-for="m in monthOptions" :key="'fm-' + m" :label="formatStatMonth(m)" :value="m" />
112
+              </el-select>
113
+            </el-form-item>
114
+          </el-col>
115
+          <el-col :span="8">
99
             <el-form-item prop="townDeptId" :rules="formRules.townDeptId">
116
             <el-form-item prop="townDeptId" :rules="formRules.townDeptId">
100
               <template slot="label">{{ dmT("formTown") }}</template>
117
               <template slot="label">{{ dmT("formTown") }}</template>
101
               <el-select v-model="form.townDeptId" :placeholder="dmCommon('pleaseSelect')" filterable style="width: 100%">
118
               <el-select v-model="form.townDeptId" :placeholder="dmCommon('pleaseSelect')" filterable style="width: 100%">
@@ -180,6 +197,11 @@
180
               <span>{{ viewRow.statYear || dash }}</span>
197
               <span>{{ viewRow.statYear || dash }}</span>
181
             </el-form-item>
198
             </el-form-item>
182
           </el-col>
199
           </el-col>
200
+          <el-col :span="12">
201
+            <el-form-item :label="dmT('formStatMonth')">
202
+              <span>{{ formatStatMonth(viewRow.statMonth) }}</span>
203
+            </el-form-item>
204
+          </el-col>
183
           <el-col :span="12">
205
           <el-col :span="12">
184
             <el-form-item :label="dmT('formTown')">
206
             <el-form-item :label="dmT('formTown')">
185
               <span>{{ viewRow.townName || dash }}</span>
207
               <span>{{ viewRow.townName || dash }}</span>
@@ -210,6 +232,12 @@
210
             <el-option v-for="y in yearOptions" :key="'i-' + y" :label="String(y)" :value="y" />
232
             <el-option v-for="y in yearOptions" :key="'i-' + y" :label="String(y)" :value="y" />
211
           </el-select>
233
           </el-select>
212
         </el-form-item>
234
         </el-form-item>
235
+        <el-form-item prop="statMonth" :rules="importRules.statMonth">
236
+          <template slot="label">{{ dmT("importStatMonth") }}</template>
237
+          <el-select v-model="importForm.statMonth" :placeholder="dmT('importStatMonthPh')" style="width: 100%">
238
+            <el-option v-for="m in monthOptions" :key="'im-' + m" :label="formatStatMonth(m)" :value="m" />
239
+          </el-select>
240
+        </el-form-item>
213
         <p class="yor-import-tip">{{ dmT("importTip") }}</p>
241
         <p class="yor-import-tip">{{ dmT("importTip") }}</p>
214
         <el-upload
242
         <el-upload
215
           ref="importUpload"
243
           ref="importUpload"
@@ -240,6 +268,12 @@
240
             <el-option v-for="y in yearOptions" :key="'e-' + y" :label="String(y)" :value="y" />
268
             <el-option v-for="y in yearOptions" :key="'e-' + y" :label="String(y)" :value="y" />
241
           </el-select>
269
           </el-select>
242
         </el-form-item>
270
         </el-form-item>
271
+        <el-form-item prop="statMonth" :rules="exportRules.statMonth">
272
+          <template slot="label">{{ dmT("exportStatMonth") }}</template>
273
+          <el-select v-model="exportForm.statMonth" :placeholder="dmT('exportStatMonthPh')" style="width: 100%">
274
+            <el-option v-for="m in monthOptions" :key="'em-' + m" :label="formatStatMonth(m)" :value="m" />
275
+          </el-select>
276
+        </el-form-item>
243
       </el-form>
277
       </el-form>
244
       <div slot="footer" class="dialog-footer">
278
       <div slot="footer" class="dialog-footer">
245
         <el-button type="primary" :loading="exporting" @click="handleExportSubmit">{{ dmCommon("ok") }}</el-button>
279
         <el-button type="primary" :loading="exporting" @click="handleExportSubmit">{{ dmCommon("ok") }}</el-button>
@@ -258,6 +292,7 @@ import {
258
   updateYakOutboundReport,
292
   updateYakOutboundReport,
259
   delYakOutboundReport,
293
   delYakOutboundReport,
260
   listYakOutboundReportYearOptions,
294
   listYakOutboundReportYearOptions,
295
+  listYakOutboundReportMonthOptions,
261
   listYakOutboundReportTownOptions,
296
   listYakOutboundReportTownOptions,
262
   importYakOutboundReport,
297
   importYakOutboundReport,
263
   downloadYakOutboundReportTemplate,
298
   downloadYakOutboundReportTemplate,
@@ -287,10 +322,19 @@ const VIEW_FIELDS = [
287
   { prop: "selfConsumptionCattleCount", labelKey: "formSelfConsumptionCattleCount" }
322
   { prop: "selfConsumptionCattleCount", labelKey: "formSelfConsumptionCattleCount" }
288
 ]
323
 ]
289
 
324
 
325
+function getCurrentYearMonth() {
326
+  const now = new Date()
327
+  return {
328
+    statYear: now.getFullYear(),
329
+    statMonth: now.getMonth() + 1
330
+  }
331
+}
332
+
290
 function createEmptyForm() {
333
 function createEmptyForm() {
291
   const form = {
334
   const form = {
292
     id: undefined,
335
     id: undefined,
293
     statYear: undefined,
336
     statYear: undefined,
337
+    statMonth: undefined,
294
     townDeptId: undefined,
338
     townDeptId: undefined,
295
     remark: undefined
339
     remark: undefined
296
   }
340
   }
@@ -327,18 +371,22 @@ export default {
327
       total: 0,
371
       total: 0,
328
       tableList: [],
372
       tableList: [],
329
       yearOptions: [],
373
       yearOptions: [],
374
+      monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
330
       townOptions: [],
375
       townOptions: [],
331
       viewRow: {},
376
       viewRow: {},
332
       importForm: {
377
       importForm: {
333
-        statYear: undefined
378
+        statYear: undefined,
379
+        statMonth: undefined
334
       },
380
       },
335
       exportForm: {
381
       exportForm: {
336
-        statYear: undefined
382
+        statYear: undefined,
383
+        statMonth: undefined
337
       },
384
       },
338
       queryParams: {
385
       queryParams: {
339
         pageNum: 1,
386
         pageNum: 1,
340
         pageSize: 20,
387
         pageSize: 20,
341
         statYear: undefined,
388
         statYear: undefined,
389
+        statMonth: undefined,
342
         townDeptId: undefined
390
         townDeptId: undefined
343
       },
391
       },
344
       form: createEmptyForm()
392
       form: createEmptyForm()
@@ -366,22 +414,31 @@ export default {
366
     formRules() {
414
     formRules() {
367
       return {
415
       return {
368
         statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }],
416
         statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }],
369
-        townDeptId: [{ required: true, message: this.dmT("ruleTownRequired"), trigger: "change" }],
370
-        count: [
371
-          { required: true, message: this.dmT("ruleCountRequired"), trigger: "change" },
372
-          { validator: this.validateRequiredCount, trigger: "change" }
417
+        statMonth: [
418
+          { required: true, message: this.dmT("ruleStatMonthRequired"), trigger: "change" },
419
+          { validator: this.validateFormStatMonth, trigger: "change" }
373
         ],
420
         ],
421
+        townDeptId: [{ required: true, message: this.dmT("ruleTownRequired"), trigger: "change" }],
422
+        count: [{ validator: this.validateOptionalCount, trigger: "change" }],
374
         remark: [{ max: 500, message: this.dmCommon("ruleLen500"), trigger: "blur" }]
423
         remark: [{ max: 500, message: this.dmCommon("ruleLen500"), trigger: "blur" }]
375
       }
424
       }
376
     },
425
     },
377
     importRules() {
426
     importRules() {
378
       return {
427
       return {
379
-        statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }]
428
+        statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }],
429
+        statMonth: [
430
+          { required: true, message: this.dmT("ruleStatMonthRequired"), trigger: "change" },
431
+          { validator: this.validateImportStatMonth, trigger: "change" }
432
+        ]
380
       }
433
       }
381
     },
434
     },
382
     exportRules() {
435
     exportRules() {
383
       return {
436
       return {
384
-        statYear: [{ required: true, message: this.dmT("exportStatYearRequired"), trigger: "change" }]
437
+        statYear: [{ required: true, message: this.dmT("exportStatYearRequired"), trigger: "change" }],
438
+        statMonth: [
439
+          { required: true, message: this.dmT("exportStatMonthRequired"), trigger: "change" },
440
+          { validator: this.validateExportStatMonth, trigger: "change" }
441
+        ]
385
       }
442
       }
386
     }
443
     }
387
   },
444
   },
@@ -394,19 +451,63 @@ export default {
394
       listYakOutboundReportYearOptions().then((res) => {
451
       listYakOutboundReportYearOptions().then((res) => {
395
         this.yearOptions = Array.isArray(res.data) ? res.data : []
452
         this.yearOptions = Array.isArray(res.data) ? res.data : []
396
       })
453
       })
454
+      listYakOutboundReportMonthOptions().then((res) => {
455
+        if (Array.isArray(res.data) && res.data.length) {
456
+          this.monthOptions = res.data
457
+        }
458
+      })
397
       listYakOutboundReportTownOptions().then((res) => {
459
       listYakOutboundReportTownOptions().then((res) => {
398
         this.townOptions = Array.isArray(res.data) ? res.data : []
460
         this.townOptions = Array.isArray(res.data) ? res.data : []
399
       })
461
       })
400
     },
462
     },
463
+    formatStatMonth(month) {
464
+      if (month == null || month === "") {
465
+        return this.dash
466
+      }
467
+      return this.dmT("statMonthLabel", { month })
468
+    },
469
+    isFutureStatPeriod(statYear, statMonth) {
470
+      if (statYear == null || statMonth == null) {
471
+        return false
472
+      }
473
+      const now = new Date()
474
+      const currentYear = now.getFullYear()
475
+      const currentMonth = now.getMonth() + 1
476
+      if (statYear > currentYear) {
477
+        return true
478
+      }
479
+      return statYear === currentYear && statMonth > currentMonth
480
+    },
481
+    validateFormStatMonth(rule, value, callback) {
482
+      if (this.isFutureStatPeriod(this.form.statYear, value)) {
483
+        callback(new Error(this.dmT("ruleStatMonthFuture")))
484
+        return
485
+      }
486
+      callback()
487
+    },
488
+    validateImportStatMonth(rule, value, callback) {
489
+      if (this.isFutureStatPeriod(this.importForm.statYear, value)) {
490
+        callback(new Error(this.dmT("ruleStatMonthFuture")))
491
+        return
492
+      }
493
+      callback()
494
+    },
495
+    validateExportStatMonth(rule, value, callback) {
496
+      if (this.isFutureStatPeriod(this.exportForm.statYear, value)) {
497
+        callback(new Error(this.dmT("ruleStatMonthFuture")))
498
+        return
499
+      }
500
+      callback()
501
+    },
401
     formatCount(val) {
502
     formatCount(val) {
402
       if (val == null || val === "") {
503
       if (val == null || val === "") {
403
         return this.dash
504
         return this.dash
404
       }
505
       }
405
       return val
506
       return val
406
     },
507
     },
407
-    validateRequiredCount(rule, value, callback) {
508
+    validateOptionalCount(rule, value, callback) {
408
       if (value == null || value === "") {
509
       if (value == null || value === "") {
409
-        callback(new Error(this.dmT("ruleCountRequired")))
510
+        callback()
410
         return
511
         return
411
       }
512
       }
412
       if (!Number.isInteger(value) || value < 0) {
513
       if (!Number.isInteger(value) || value < 0) {
@@ -423,6 +524,9 @@ export default {
423
       if (this.queryParams.statYear != null) {
524
       if (this.queryParams.statYear != null) {
424
         q.statYear = this.queryParams.statYear
525
         q.statYear = this.queryParams.statYear
425
       }
526
       }
527
+      if (this.queryParams.statMonth != null) {
528
+        q.statMonth = this.queryParams.statMonth
529
+      }
426
       if (this.queryParams.townDeptId != null) {
530
       if (this.queryParams.townDeptId != null) {
427
         q.townDeptId = this.queryParams.townDeptId
531
         q.townDeptId = this.queryParams.townDeptId
428
       }
532
       }
@@ -432,6 +536,7 @@ export default {
432
       const remarkText = this.form.remark != null ? String(this.form.remark).trim() : ""
536
       const remarkText = this.form.remark != null ? String(this.form.remark).trim() : ""
433
       const payload = {
537
       const payload = {
434
         statYear: this.form.statYear,
538
         statYear: this.form.statYear,
539
+        statMonth: this.form.statMonth,
435
         townDeptId: this.form.townDeptId,
540
         townDeptId: this.form.townDeptId,
436
         remark: remarkText !== "" ? remarkText : undefined
541
         remark: remarkText !== "" ? remarkText : undefined
437
       }
542
       }
@@ -457,13 +562,17 @@ export default {
457
     },
562
     },
458
     resetQuery() {
563
     resetQuery() {
459
       this.queryParams.statYear = undefined
564
       this.queryParams.statYear = undefined
565
+      this.queryParams.statMonth = undefined
460
       this.queryParams.townDeptId = undefined
566
       this.queryParams.townDeptId = undefined
461
       this.resetForm("queryForm")
567
       this.resetForm("queryForm")
462
       this.handleQuery()
568
       this.handleQuery()
463
     },
569
     },
464
     handleAdd() {
570
     handleAdd() {
465
       this.formKey += 1
571
       this.formKey += 1
572
+      const current = getCurrentYearMonth()
466
       this.form = createEmptyForm()
573
       this.form = createEmptyForm()
574
+      this.form.statYear = current.statYear
575
+      this.form.statMonth = current.statMonth
467
       this.dialogEdit = false
576
       this.dialogEdit = false
468
       this.open = true
577
       this.open = true
469
       this.$nextTick(() => {
578
       this.$nextTick(() => {
@@ -534,7 +643,9 @@ export default {
534
       this.form = createEmptyForm()
643
       this.form = createEmptyForm()
535
     },
644
     },
536
     handleImportOpen() {
645
     handleImportOpen() {
537
-      this.importForm.statYear = this.queryParams.statYear
646
+      const current = getCurrentYearMonth()
647
+      this.importForm.statYear = current.statYear
648
+      this.importForm.statMonth = current.statMonth
538
       this.importOpen = true
649
       this.importOpen = true
539
       this.$nextTick(() => {
650
       this.$nextTick(() => {
540
         this.resetImportFiles()
651
         this.resetImportFiles()
@@ -548,6 +659,7 @@ export default {
548
     resetImport() {
659
     resetImport() {
549
       this.importing = false
660
       this.importing = false
550
       this.importForm.statYear = undefined
661
       this.importForm.statYear = undefined
662
+      this.importForm.statMonth = undefined
551
       this.resetImportFiles()
663
       this.resetImportFiles()
552
     },
664
     },
553
     handleDownloadTemplate() {
665
     handleDownloadTemplate() {
@@ -571,7 +683,9 @@ export default {
571
         })
683
         })
572
     },
684
     },
573
     handleExportOpen() {
685
     handleExportOpen() {
574
-      this.exportForm.statYear = this.queryParams.statYear
686
+      const current = getCurrentYearMonth()
687
+      this.exportForm.statYear = current.statYear
688
+      this.exportForm.statMonth = current.statMonth
575
       this.exportOpen = true
689
       this.exportOpen = true
576
       this.$nextTick(() => {
690
       this.$nextTick(() => {
577
         if (this.$refs.exportFormRef) {
691
         if (this.$refs.exportFormRef) {
@@ -582,6 +696,7 @@ export default {
582
     resetExport() {
696
     resetExport() {
583
       this.exporting = false
697
       this.exporting = false
584
       this.exportForm.statYear = undefined
698
       this.exportForm.statYear = undefined
699
+      this.exportForm.statMonth = undefined
585
     },
700
     },
586
     handleExportSubmit() {
701
     handleExportSubmit() {
587
       this.$refs.exportFormRef.validate((valid) => {
702
       this.$refs.exportFormRef.validate((valid) => {
@@ -589,14 +704,15 @@ export default {
589
           return
704
           return
590
         }
705
         }
591
         const statYear = this.exportForm.statYear
706
         const statYear = this.exportForm.statYear
592
-        if (!this.yearOptions.includes(statYear)) {
707
+        const statMonth = this.exportForm.statMonth
708
+        if (!this.yearOptions.includes(statYear) || !this.monthOptions.includes(statMonth)) {
593
           this.$modal.msgError(this.dmT("exportStatYearInvalid"))
709
           this.$modal.msgError(this.dmT("exportStatYearInvalid"))
594
           return
710
           return
595
         }
711
         }
596
-        const filename = `${statYear}年牦牛出栏数据.xlsx`
712
+        const filename = `${statYear}年${statMonth}月牦牛出栏数据.xlsx`
597
         this.exporting = true
713
         this.exporting = true
598
         this.$modal.loading(this.dmT("exportLoading"))
714
         this.$modal.loading(this.dmT("exportLoading"))
599
-        exportYakOutboundReport(statYear)
715
+        exportYakOutboundReport(statYear, statMonth)
600
           .then(async (data) => {
716
           .then(async (data) => {
601
             if (blobValidate(data)) {
717
             if (blobValidate(data)) {
602
               saveAs(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), filename)
718
               saveAs(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), filename)
@@ -635,7 +751,7 @@ export default {
635
         }
751
         }
636
         this.importing = true
752
         this.importing = true
637
         this.$modal.loading(this.dmT("importLoading"))
753
         this.$modal.loading(this.dmT("importLoading"))
638
-        importYakOutboundReport(raw, this.importForm.statYear)
754
+        importYakOutboundReport(raw, this.importForm.statYear, this.importForm.statMonth)
639
           .then((res) => {
755
           .then((res) => {
640
             this.importOpen = false
756
             this.importOpen = false
641
             this.showImportResult(res.data || {})
757
             this.showImportResult(res.data || {})

+ 93 - 42
ruoyi-ui/src/views/livestockIndustry/breedingIndustry/index.vue

@@ -42,25 +42,18 @@
42
     <el-card shadow="never" class="bi-section-card">
42
     <el-card shadow="never" class="bi-section-card">
43
       <div slot="header" class="bi-section-header">{{ biT("sectionInventoryChange") }}</div>
43
       <div slot="header" class="bi-section-header">{{ biT("sectionInventoryChange") }}</div>
44
       <el-row :gutter="16" class="bi-inventory-stats">
44
       <el-row :gutter="16" class="bi-inventory-stats">
45
-        <el-col :xs="24" :sm="8">
45
+        <el-col :xs="24" :sm="12">
46
           <div class="bi-stat-block">
46
           <div class="bi-stat-block">
47
             <div class="bi-stat-block__value">{{ formatHead(inventoryChange.bullCount) }}</div>
47
             <div class="bi-stat-block__value">{{ formatHead(inventoryChange.bullCount) }}</div>
48
             <div class="bi-stat-block__label">{{ biT("bullCount") }}</div>
48
             <div class="bi-stat-block__label">{{ biT("bullCount") }}</div>
49
           </div>
49
           </div>
50
         </el-col>
50
         </el-col>
51
-        <el-col :xs="24" :sm="8">
51
+        <el-col :xs="24" :sm="12">
52
           <div class="bi-stat-block">
52
           <div class="bi-stat-block">
53
             <div class="bi-stat-block__value">{{ formatHead(inventoryChange.cowCount) }}</div>
53
             <div class="bi-stat-block__value">{{ formatHead(inventoryChange.cowCount) }}</div>
54
             <div class="bi-stat-block__label">{{ biT("cowCount") }}</div>
54
             <div class="bi-stat-block__label">{{ biT("cowCount") }}</div>
55
           </div>
55
           </div>
56
         </el-col>
56
         </el-col>
57
-        <el-col v-if="inventoryChange.unknownGenderCount > 0" :xs="24" :sm="8">
58
-          <div class="bi-stat-block bi-stat-block--hint">
59
-            <div class="bi-stat-block__value bi-stat-block__value--hint">
60
-              {{ biT("unknownGenderHint", { count: inventoryChange.unknownGenderCount }) }}
61
-            </div>
62
-          </div>
63
-        </el-col>
64
       </el-row>
57
       </el-row>
65
       <div class="bi-chart-title">{{ biT("chartInventoryTrend") }}</div>
58
       <div class="bi-chart-title">{{ biT("chartInventoryTrend") }}</div>
66
       <div ref="chartInventory" class="bi-chart-box" />
59
       <div ref="chartInventory" class="bi-chart-box" />
@@ -126,9 +119,7 @@ import { getBreedingDashboard } from "@/api/livestockIndustry/breedingIndustry"
126
 
119
 
127
 const CHART_REFS = ["chartInventory", "chartAge", "chartDegradation"]
120
 const CHART_REFS = ["chartInventory", "chartAge", "chartDegradation"]
128
 
121
 
129
-/** 产业总览:牦牛存栏/出栏固定展示值(不走接口) */
130
-const YAK_INVENTORY_TOTAL = 238067
131
-const YAK_ANNUAL_OUTBOUND = 37449
122
+const AGE_BAND_ORDER = ["COW_U1", "COW_1_3", "COW_3_12", "COW_O12"]
132
 
123
 
133
 export default {
124
 export default {
134
   name: "BreedingIndustry",
125
   name: "BreedingIndustry",
@@ -160,9 +151,8 @@ export default {
160
     },
151
     },
161
     inventoryChange() {
152
     inventoryChange() {
162
       return (this.dashboardData && this.dashboardData.inventoryChange) || {
153
       return (this.dashboardData && this.dashboardData.inventoryChange) || {
163
-        bullCount: 37466,
164
-        cowCount: 37449,
165
-        unknownGenderCount: 0,
154
+        bullCount: 0,
155
+        cowCount: 0,
166
         monthlyTrend: []
156
         monthlyTrend: []
167
       }
157
       }
168
     },
158
     },
@@ -179,11 +169,14 @@ export default {
179
     degradationTable() {
169
     degradationTable() {
180
       return this.grassland.degradationStats || []
170
       return this.grassland.degradationStats || []
181
     },
171
     },
182
-    monthLabels() {
183
-      return Array.from({ length: 12 }, (_, i) => this.biT("monthLabel", { m: i + 1 }))
184
-    },
185
-    currentMonth() {
186
-      return new Date().getMonth() + 1
172
+    sortedAgeStructure() {
173
+      const list = this.ageStructure || []
174
+      if (!list.length) {
175
+        return []
176
+      }
177
+      const map = new Map(list.map((item) => [item.bandCode, item]))
178
+      const ordered = AGE_BAND_ORDER.map((code) => map.get(code)).filter(Boolean)
179
+      return ordered.length ? ordered : list
187
     }
180
     }
188
   },
181
   },
189
   mounted() {
182
   mounted() {
@@ -315,19 +308,35 @@ export default {
315
       this.renderAgeChart()
308
       this.renderAgeChart()
316
       this.renderDegradationChart()
309
       this.renderDegradationChart()
317
     },
310
     },
318
-    buildTrendSeries() {
319
-      const trend = this.inventoryChange.monthlyTrend || []
320
-      const map = {}
321
-      trend.forEach((p) => {
322
-        map[p.month] = p.inventoryCount
311
+    buildRollingTrendAxis(monthlyTrend) {
312
+      const rows = [...(monthlyTrend || [])].sort((a, b) => {
313
+        const ak = (Number(a.statYear) || 0) * 100 + (Number(a.month) || 0)
314
+        const bk = (Number(b.statYear) || 0) * 100 + (Number(b.month) || 0)
315
+        return ak - bk
323
       })
316
       })
324
-      return Array.from({ length: 12 }, (_, i) => {
325
-        const m = i + 1
326
-        if (m > this.currentMonth) {
327
-          return null
317
+      return {
318
+        rows,
319
+        categories: rows.map((row) => this.biT("monthLabel", { m: Number(row.month) })),
320
+        titleAt: (dataIndex) => {
321
+          const row = rows[dataIndex]
322
+          if (!row) {
323
+            return ""
324
+          }
325
+          return `${row.statYear}年${Number(row.month)}月`
328
         }
326
         }
329
-        return map[m] != null ? map[m] : null
330
-      })
327
+      }
328
+    },
329
+    pickTrendCount(row, field) {
330
+      const val = row?.[field]
331
+      if (val == null || val === "") {
332
+        return 0
333
+      }
334
+      const n = Number(val)
335
+      return Number.isNaN(n) ? 0 : n
336
+    },
337
+    buildRollingTrendSeries(monthlyTrend, field) {
338
+      const axis = this.buildRollingTrendAxis(monthlyTrend)
339
+      return axis.rows.map((row) => this.pickTrendCount(row, field))
331
     },
340
     },
332
     renderInventoryChart() {
341
     renderInventoryChart() {
333
       const dom = this.$refs.chartInventory
342
       const dom = this.$refs.chartInventory
@@ -335,25 +344,58 @@ export default {
335
       if (!chart) {
344
       if (!chart) {
336
         return
345
         return
337
       }
346
       }
338
-      const seriesData = this.buildTrendSeries()
339
-      const hasPoints = seriesData.some((v) => v != null)
347
+      const trend = this.inventoryChange.monthlyTrend || []
348
+      if (!trend.length) {
349
+        chart.setOption(
350
+          {
351
+            graphic: [
352
+              {
353
+                type: "text",
354
+                left: "center",
355
+                top: "middle",
356
+                style: {
357
+                  text: this.biT("emptyChart"),
358
+                  fill: "#909399",
359
+                  fontSize: 14
360
+                }
361
+              }
362
+            ]
363
+          },
364
+          true
365
+        )
366
+        this.charts.inventory = chart
367
+        return
368
+      }
369
+      const axis = this.buildRollingTrendAxis(trend)
370
+      const bullData = this.buildRollingTrendSeries(trend, "bullCount")
371
+      const cowData = this.buildRollingTrendSeries(trend, "cowCount")
372
+      const hasPoints = bullData.some((v) => v > 0) || cowData.some((v) => v > 0)
340
       chart.setOption(
373
       chart.setOption(
341
         {
374
         {
342
           tooltip: {
375
           tooltip: {
343
             trigger: "axis",
376
             trigger: "axis",
344
             formatter: (params) => {
377
             formatter: (params) => {
345
-              const p = params.find((x) => x.value != null)
346
-              if (!p) {
378
+              if (!Array.isArray(params) || !params.length) {
347
                 return ""
379
                 return ""
348
               }
380
               }
349
-              return `${p.name}<br/>${p.seriesName}: ${p.value} ${this.biCommon("unitHead")}`
381
+              const title = axis.titleAt(params[0].dataIndex)
382
+              const lines = params.map(
383
+                (p) => `${p.marker}${p.seriesName}: ${p.value ?? 0} ${this.biCommon("unitHead")}`
384
+              )
385
+              return `${title}<br/>${lines.join("<br/>")}`
350
             }
386
             }
351
           },
387
           },
352
-          grid: { left: "3%", right: "4%", bottom: "3%", top: 32, containLabel: true },
388
+          legend: {
389
+            top: 0,
390
+            right: 8,
391
+            data: [this.biT("seriesBullCount"), this.biT("seriesCowCount")]
392
+          },
393
+          grid: { left: "3%", right: "4%", bottom: "3%", top: 40, containLabel: true },
353
           xAxis: {
394
           xAxis: {
354
             type: "category",
395
             type: "category",
355
             boundaryGap: false,
396
             boundaryGap: false,
356
-            data: this.monthLabels
397
+            data: axis.categories,
398
+            axisLabel: { interval: 0, fontSize: 10 }
357
           },
399
           },
358
           yAxis: {
400
           yAxis: {
359
             type: "value",
401
             type: "value",
@@ -362,11 +404,20 @@ export default {
362
           },
404
           },
363
           series: [
405
           series: [
364
             {
406
             {
365
-              name: this.biT("seriesInventoryCount"),
407
+              name: this.biT("seriesBullCount"),
408
+              type: "line",
409
+              smooth: true,
410
+              symbol: "circle",
411
+              symbolSize: 4,
412
+              data: bullData
413
+            },
414
+            {
415
+              name: this.biT("seriesCowCount"),
366
               type: "line",
416
               type: "line",
367
               smooth: true,
417
               smooth: true,
368
-              connectNulls: false,
369
-              data: seriesData
418
+              symbol: "circle",
419
+              symbolSize: 4,
420
+              data: cowData
370
             }
421
             }
371
           ],
422
           ],
372
           graphic: hasPoints
423
           graphic: hasPoints
@@ -394,7 +445,7 @@ export default {
394
       if (!chart) {
445
       if (!chart) {
395
         return
446
         return
396
       }
447
       }
397
-      const list = this.ageStructure
448
+      const list = this.sortedAgeStructure
398
       const pieData = list.map((item) => ({
449
       const pieData = list.map((item) => ({
399
         name: item.bandLabel,
450
         name: item.bandLabel,
400
         value: item.count
451
         value: item.count