xsh_1997 4 주 전
부모
커밋
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 6
 const AXIS_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.35)' } }
7 7
 const SPLIT_LINE = { lineStyle: { color: 'rgba(61, 217, 176, 0.12)' } }
8 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 10
 function emptyOption(text = '暂无数据') {
12 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 21
 function pickNumber(row, fields) {
27 22
   for (const key of fields) {
28 23
     const raw = row?.[key]
@@ -36,22 +31,59 @@ function pickNumber(row, fields) {
36 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 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 69
 export function buildInventoryTrendOption(monthlyTrend) {
49 70
   if (!monthlyTrend?.length) {
50 71
     return emptyOption()
51 72
   }
73
+  const axis = buildRollingAxis(monthlyTrend)
52 74
   return {
53 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 87
     legend: {
56 88
       top: 0,
57 89
       right: 8,
@@ -62,7 +94,7 @@ export function buildInventoryTrendOption(monthlyTrend) {
62 94
     grid: GRID,
63 95
     xAxis: {
64 96
       type: 'category',
65
-      data: MONTHS,
97
+      data: axis.categories,
66 98
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
67 99
       axisLine: AXIS_LINE,
68 100
       axisTick: { show: false }
@@ -80,7 +112,7 @@ export function buildInventoryTrendOption(monthlyTrend) {
80 112
         smooth: true,
81 113
         symbol: 'circle',
82 114
         symbolSize: 4,
83
-        data: monthSeries(monthlyTrend, 'bullCount')
115
+        data: rollingSeries(monthlyTrend, 'bullCount')
84 116
       },
85 117
       {
86 118
         name: '母牛',
@@ -88,20 +120,21 @@ export function buildInventoryTrendOption(monthlyTrend) {
88 120
         smooth: true,
89 121
         symbol: 'circle',
90 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 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 139
 function sortAgeBands(ageStructure) {
107 140
   const map = new Map((ageStructure || []).map((item) => [item.bandCode, item]))
@@ -116,7 +149,7 @@ function formatAgeRatio(ratio, percent) {
116 149
   return `${Math.round(percent)}%`
117 150
 }
118 151
 
119
-/** 牦牛年龄结构(环形图 + 右侧图例,固定档) */
152
+/** 牦牛年龄结构(环形图 + 右侧图例,固定档) */
120 153
 export function buildAgeStructureOption(ageStructure) {
121 154
   if (!ageStructure?.length) {
122 155
     return emptyOption()
@@ -167,7 +200,7 @@ export function buildAgeStructureOption(ageStructure) {
167 200
       icon: 'circle',
168 201
       itemWidth: 8,
169 202
       itemHeight: 8,
170
-      itemGap: 12,
203
+      itemGap: 10,
171 204
       textStyle: { color: '#e8eef5', fontSize: 10 }
172 205
     },
173 206
     graphic: [
@@ -227,7 +260,7 @@ export function buildAgeStructureOption(ageStructure) {
227 260
   }
228 261
 }
229 262
 
230
-/** 出栏柱图金色渐变(对齐 doc/首页/耗牛出栏统计.png) */
263
+/** 出栏柱图金色渐变 */
231 264
 const OUTBOUND_BAR_GRADIENT = {
232 265
   type: 'linear',
233 266
   x: 0,
@@ -246,33 +279,30 @@ const OUTBOUND_SPLIT_LINE = {
246 279
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
247 280
 }
248 281
 
249
-/** 牦牛出栏按月(柱 + 折线,设计稿样式) */
282
+/** 牦牛出栏按月(纯柱状图,滚动 12 月) */
250 283
 export function buildOutboundOption(outboundMonthly) {
251 284
   if (!outboundMonthly?.length) {
252 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 289
   return {
256 290
     tooltip: {
257 291
       trigger: 'axis',
258 292
       axisPointer: { type: 'shadow' },
259 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 302
     grid: OUTBOUND_GRID,
273 303
     xAxis: {
274 304
       type: 'category',
275
-      data: MONTHS,
305
+      data: axis.categories,
276 306
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
277 307
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
278 308
       axisTick: { show: false }
@@ -290,27 +320,17 @@ export function buildOutboundOption(outboundMonthly) {
290 320
         name: '出栏',
291 321
         type: 'bar',
292 322
         barWidth: '46%',
293
-        z: 1,
294 323
         itemStyle: {
295 324
           color: OUTBOUND_BAR_GRADIENT,
296 325
           borderRadius: [10, 10, 10, 10]
297 326
         },
298 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 334
 const GRASSLAND_DEGRADATION_COLOR = {
315 335
   1: '#3b66d1',
316 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 423
 const APPOINTMENT_ORG_GRADIENT = {
420 424
   type: 'linear',
421 425
   x: 0,
@@ -445,18 +449,23 @@ const APPOINTMENT_SPLIT_LINE = {
445 449
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
446 450
 }
447 451
 
448
-/** 线下服务预约堆叠柱 */
452
+/** 线下服务预约堆叠柱(滚动 12 月) */
449 453
 export function buildAppointmentStackedOption(monthlyStacked) {
450 454
   if (!monthlyStacked?.length) {
451 455
     return emptyOption()
452 456
   }
457
+  const axis = buildRollingAxis(monthlyStacked)
453 458
   return {
454 459
     tooltip: {
455 460
       trigger: 'axis',
456 461
       axisPointer: { type: 'shadow' },
457 462
       formatter: (params) => {
463
+        if (!Array.isArray(params) || !params.length) {
464
+          return ''
465
+        }
466
+        const title = axis.titleAt(params[0].dataIndex)
458 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 471
     legend: {
@@ -470,7 +479,7 @@ export function buildAppointmentStackedOption(monthlyStacked) {
470 479
     grid: APPOINTMENT_GRID,
471 480
     xAxis: {
472 481
       type: 'category',
473
-      data: APPOINTMENT_MONTHS,
482
+      data: axis.categories,
474 483
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0 },
475 484
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
476 485
       axisTick: { show: false }
@@ -490,7 +499,7 @@ export function buildAppointmentStackedOption(monthlyStacked) {
490 499
         stack: 'apt',
491 500
         barMaxWidth: 10,
492 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 505
         name: '兽医',
@@ -498,7 +507,7 @@ export function buildAppointmentStackedOption(monthlyStacked) {
498 507
         stack: 'apt',
499 508
         barMaxWidth: 10,
500 509
         itemStyle: { color: '#52b89f' },
501
-        data: monthSeries(monthlyStacked, 'vetCount', ['vet_count'])
510
+        data: rollingSeries(monthlyStacked, 'vetCount', ['vet_count'])
502 511
       },
503 512
       {
504 513
         name: '专家',
@@ -506,13 +515,13 @@ export function buildAppointmentStackedOption(monthlyStacked) {
506 515
         stack: 'apt',
507 516
         barMaxWidth: 10,
508 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 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 581
 const TRAINING_ACTIVITY_LINE = '#ff9f43'
574 582
 const TRAINING_ACTIVITY_AREA = {
575 583
   type: 'linear',
@@ -601,12 +609,16 @@ const TRAINING_SPLIT_LINE = {
601 609
   lineStyle: { color: 'rgba(120, 140, 160, 0.28)', type: 'dashed' }
602 610
 }
603 611
 
604
-/** 实战培训报名趋势(双面积折线:活动数 + 报名人数) */
612
+/** 实战培训报名趋势(双面积折线,滚动 12 月) */
605 613
 export function buildTrainingOption(trainingTrend) {
606 614
   if (!trainingTrend?.hasTrainingData) {
607 615
     return emptyOption('暂无培训数据')
608 616
   }
609 617
   const monthly = trainingTrend.monthly || []
618
+  if (!monthly.length) {
619
+    return emptyOption('暂无培训数据')
620
+  }
621
+  const axis = buildRollingAxis(monthly)
610 622
   return {
611 623
     tooltip: {
612 624
       trigger: 'axis',
@@ -614,11 +626,12 @@ export function buildTrainingOption(trainingTrend) {
614 626
         if (!Array.isArray(params) || !params.length) {
615 627
           return ''
616 628
         }
629
+        const title = axis.titleAt(params[0].dataIndex)
617 630
         const lines = params.map((p) => {
618 631
           const unit = p.seriesName === '活动数' ? '个' : '人'
619 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 637
     legend: {
@@ -631,7 +644,7 @@ export function buildTrainingOption(trainingTrend) {
631 644
     grid: TRAINING_GRID,
632 645
     xAxis: {
633 646
       type: 'category',
634
-      data: MONTHS,
647
+      data: axis.categories,
635 648
       axisLabel: { ...MONTH_AXIS_LABEL, interval: 0, color: '#e8eef5' },
636 649
       axisLine: { lineStyle: { color: 'rgba(150, 170, 190, 0.45)' } },
637 650
       axisTick: { show: false }
@@ -672,7 +685,7 @@ export function buildTrainingOption(trainingTrend) {
672 685
           shadowBlur: 10
673 686
         },
674 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 691
         name: '报名人数',
@@ -687,7 +700,7 @@ export function buildTrainingOption(trainingTrend) {
687 700
           shadowBlur: 10
688 701
         },
689 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 1
 <template>
2 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 5
     <div class="home-year-bar">
6 6
       <label class="home-year-bar__label">统计年份</label>
@@ -12,11 +12,12 @@
12 12
       >
13 13
         <option v-for="y in availableYears" :key="y" :value="String(y)">{{ y }}年</option>
14 14
       </select>
15
-    </div> -->
15
+      <span v-if="statDate" class="home-year-bar__date">统计日 {{ statDate }}</span>
16
+    </div>
16 17
 
17 18
     <div class="screen-page--home-column">
18 19
       <div class="screen-page--home-column-top">
19
-        <div class="top_title">产总览 ལས་རྩོམ་ཐོག་མའི་དོ་དམ།</div>
20
+        <div class="top_title">产总览 ལས་རྩོམ་ཐོག་མའི་དོ་དམ།</div>
20 21
         <div class="top_content">
21 22
           <div class="content_1">
22 23
             <div class="content_num">
@@ -74,7 +75,12 @@
74 75
 
75 76
       <div class="screen-page--home-column-flex">
76 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 84
           <div class="flex_content">
79 85
             <ScreenChart :option="inventoryChartOption" />
80 86
           </div>
@@ -103,7 +109,7 @@
103 109
                 <div class="grassland-stat__value">{{ display(dashboardGrassland?.availableCount) }}</div>
104 110
               </div>
105 111
               <div class="grassland-stat grassland-stat--inuse">
106
-                <div class="grassland-stat__label">已用草场数</div>
112
+                <div class="grassland-stat__label">使用中草场数</div>
107 113
                 <div class="grassland-stat__value">{{ display(dashboardGrassland?.inUseCount) }}</div>
108 114
               </div>
109 115
             </div>
@@ -123,22 +129,19 @@
123 129
             <div class="yu">
124 130
               <div class="yu_title">兽医人员</div>
125 131
               <div class="yu_num">
126
-                <!-- <strong>{{ display(appointment?.annualVetCount) }}</strong> 人 -->
127
-                <strong>150</strong> 人
132
+                <strong>{{ display(appointment?.annualVetCount) }}</strong> 人
128 133
               </div>
129 134
             </div>
130 135
             <div class="yu">
131 136
               <div class="yu_title">专家人员</div>
132 137
               <div class="yu_num">
133
-                <!-- <strong>{{ display(appointment?.annualExpertCount) }}</strong> 人 -->
134
-                <strong>1</strong> 人
138
+                <strong>{{ display(appointment?.annualExpertCount) }}</strong> 人
135 139
               </div>
136 140
             </div>
137 141
             <div class="yu">
138 142
               <div class="yu_title">诊疗机构</div>
139 143
               <div class="yu_num">
140
-                <!-- <strong>{{ display(appointment?.annualOrgCount) }}</strong> 个 -->
141
-                <strong>12</strong> 个
144
+                <strong>{{ display(appointment?.annualOrgCount) }}</strong> 个
142 145
               </div>
143 146
             </div>
144 147
           </div>
@@ -237,12 +240,10 @@ const STANDARD_TABS = [
237 240
 
238 241
 const STANDARD_FETCH_PAGE_SIZE = 50
239 242
 
240
-/** 产值总览:农牧户固定展示值(不走接口) */
241
-const FARMER_COUNT = 8988
242
-
243 243
 const loading = ref(false)
244 244
 const loadError = ref('')
245 245
 const statYear = ref(String(new Date().getFullYear()))
246
+const statDate = ref('')
246 247
 const availableYears = ref([])
247 248
 const overview = ref(null)
248 249
 const inventoryChange = ref(null)
@@ -307,6 +308,7 @@ function applyDashboard(data) {
307 308
   if (data.statYear) {
308 309
     statYear.value = String(data.statYear)
309 310
   }
311
+  statDate.value = data.statDate || ''
310 312
   availableYears.value = data.availableYears?.length
311 313
     ? [...data.availableYears]
312 314
     : [Number(statYear.value)]
@@ -436,6 +438,11 @@ onMounted(() => {
436 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 446
 .screen-page--home-column {
440 447
   width: 617px;
441 448
   height: 100%;
@@ -555,6 +562,22 @@ onMounted(() => {
555 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 581
 .flex_content {
559 582
   width: 100%;
560 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 63
 export function listYakHerdInventoryTownOptions() {
56 64
   return request({
@@ -60,10 +68,11 @@ export function listYakHerdInventoryTownOptions() {
60 68
 }
61 69
 
62 70
 /** Excel 导入(multipart/form-data,file 为 el-upload 的 raw File) */
63
-export function importYakHerdInventory(file, statYear) {
71
+export function importYakHerdInventory(file, statYear, statMonth) {
64 72
   const formData = new FormData()
65 73
   formData.append("file", file, file.name)
66 74
   formData.append("statYear", String(statYear))
75
+  formData.append("statMonth", String(statMonth))
67 76
   return request({
68 77
     url: "/dataModel/yakHerdInventory/importData",
69 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 97
   return request({
89 98
     url: "/dataModel/yakHerdInventory/export",
90 99
     method: "get",
91
-    params: { statYear },
100
+    params: { statYear, statMonth },
92 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 63
 export function listYakOutboundReportTownOptions() {
56 64
   return request({
@@ -60,10 +68,11 @@ export function listYakOutboundReportTownOptions() {
60 68
 }
61 69
 
62 70
 /** Excel 导入(multipart/form-data,file 为 el-upload 的 raw File) */
63
-export function importYakOutboundReport(file, statYear) {
71
+export function importYakOutboundReport(file, statYear, statMonth) {
64 72
   const formData = new FormData()
65 73
   formData.append("file", file, file.name)
66 74
   formData.append("statYear", String(statYear))
75
+  formData.append("statMonth", String(statMonth))
67 76
   return request({
68 77
     url: "/dataModel/yakOutboundReport/importData",
69 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 97
   return request({
89 98
     url: "/dataModel/yakOutboundReport/export",
90 99
     method: "get",
91
-    params: { statYear },
100
+    params: { statYear, statMonth },
92 101
     responseType: "blob"
93 102
   })
94 103
 }

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

@@ -19,14 +19,15 @@ export default {
19 19
     containerSeq += 1
20 20
     return {
21 21
       containerId: `ezviz-hls-player-${containerSeq}`,
22
-      player: null,
23 22
       resizeObserver: null
24 23
     }
25 24
   },
26 25
   computed: {
27 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 33
   watch: {
@@ -34,6 +35,10 @@ export default {
34 35
       this.rebuildPlayer(val)
35 36
     }
36 37
   },
38
+  created() {
39
+    this.hlsPlayer = null
40
+    this.onPlayerParsed = null
41
+  },
37 42
   mounted() {
38 43
     this.bindResizeObserver()
39 44
     if (this.url) {
@@ -78,22 +83,42 @@ export default {
78 83
       }
79 84
     },
80 85
     syncPlayerSize() {
81
-      if (!this.player || typeof this.player.resize !== "function") {
86
+      if (!this.hlsPlayer || typeof this.hlsPlayer.resize !== "function") {
82 87
         return
83 88
       }
84 89
       const { width, height } = this.getContainerSize()
85 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 113
     destroyPlayer() {
90
-      if (this.player) {
114
+      this.unbindPlayerEvents()
115
+      if (this.hlsPlayer) {
91 116
         try {
92
-          this.player.destroy()
117
+          this.hlsPlayer.destroy()
93 118
         } catch (e) {
94 119
           // ignore teardown errors
95 120
         }
96
-        this.player = null
121
+        this.hlsPlayer = null
97 122
       }
98 123
       const el = this.$refs.containerRef
99 124
       if (el) {
@@ -127,15 +152,17 @@ export default {
127 152
         }
128 153
         return
129 154
       }
130
-      this.player = new HlsPlayer({
155
+      this.hlsPlayer = new HlsPlayer({
131 156
         id: this.containerId,
132 157
         url: text,
133
-        staticPath: '/',
158
+        staticPath: this.decoderStaticPath,
134 159
         isLive: true,
160
+        autoPlay: true,
135 161
         width,
136 162
         height
137 163
       })
138
-      this.player.play().catch(() => {
164
+      this.bindPlayerEvents()
165
+      this.hlsPlayer.play().catch(() => {
139 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 277
   yakHerdInventory: {
278 278
     queryStatYear: "ལོ་ཚད།",
279
+    queryStatMonth: "ཟླ་ཚད།",
279 280
     queryTown: "ཤང་གྲོང་།",
280 281
     colStatYear: "ལོ་ཚད།",
282
+    colStatMonth: "ཟླ་ཚད།",
281 283
     colTownName: "ཤང་གྲོང་།",
284
+    statMonthLabel: "{month}ཟླ།",
282 285
     colVillageCount: "གྲོང་ཚོ་གྲངས།",
283 286
     colHerdsmanHouseholdCount: "འབྲོག་པ་ཁྱིམ་གྲངས།",
284 287
     colYakTotal: "གནས་ཚད།",
285 288
     colBullTotal: "ཕོ་བ་གནས་ཚད།",
286 289
     colCowTotal: "མོ་བ་གནས་ཚད།",
287 290
     formStatYear: "ལོ་ཚད།",
291
+    formStatMonth: "ཟླ་ཚད།",
288 292
     formTown: "ཤང་གྲོང་།",
289 293
     formVillageCount: "གྲོང་ཚོ་གྲངས།",
290 294
     formHerdsmanHouseholdCount: "འབྲོག་པ་ཁྱིམ་གྲངས།",
@@ -309,14 +313,19 @@ export default {
309 313
     exportTitle: "Excel 导出",
310 314
     exportStatYear: "所属年份",
311 315
     exportStatYearPh: "请选择所属年份",
316
+    exportStatMonth: "所属月份",
317
+    exportStatMonthPh: "请选择所属月份",
312 318
     exportStatYearRequired: "请选择所属年份",
313
-    exportStatYearInvalid: "年份不在可选范围",
319
+    exportStatMonthRequired: "请选择所属月份",
320
+    exportStatYearInvalid: "年月不在可选范围",
314 321
     exportLoading: "正在导出...",
315 322
     exportFail: "导出失败",
316 323
     importTitle: "Excel 导入",
317 324
     importStatYear: "填报年份",
318 325
     importStatYearPh: "请选择年份",
319
-    importTip: "先选年份再上传 .xlsx",
326
+    importStatMonth: "填报月份",
327
+    importStatMonthPh: "请选择月份",
328
+    importTip: "先选年月再上传 .xlsx",
320 329
     importLoading: "正在导入,请稍候...",
321 330
     importSummary: "导入:新增 {insert} 更新 {update} 失败 {fail}",
322 331
     importWarnTitle: "警告",
@@ -329,6 +338,8 @@ export default {
329 338
     unitHousehold: "户",
330 339
     unitHead: "头",
331 340
     ruleStatYearRequired: "请选择年份",
341
+    ruleStatMonthRequired: "请选择月份",
342
+    ruleStatMonthFuture: "未来月份不可填",
332 343
     ruleTownRequired: "请选择乡镇",
333 344
     ruleCountRequired: "请填写",
334 345
     ruleCountNonNegative: "≥0 整数",
@@ -338,14 +349,18 @@ export default {
338 349
   },
339 350
   yakOutboundReport: {
340 351
     queryStatYear: "所属年份",
352
+    queryStatMonth: "所属月份",
341 353
     queryTown: "所属乡镇",
342 354
     colStatYear: "所属年份",
355
+    colStatMonth: "所属月份",
356
+    statMonthLabel: "{month}月",
343 357
     colTownName: "所属乡镇",
344 358
     colFarmerHouseholdCount: "农牧户户数",
345 359
     colFarmerPopulationCount: "农牧户人数",
346 360
     colYakOutboundCount: "牦牛出栏数",
347 361
     colSelfConsumptionCattle: "自食情况(牛)",
348 362
     formStatYear: "所属年份",
363
+    formStatMonth: "所属月份",
349 364
     formTown: "所属乡镇",
350 365
     formFarmerHouseholdCount: "农牧户户数",
351 366
     formFarmerPopulationCount: "农牧户人数",
@@ -369,14 +384,19 @@ export default {
369 384
     exportTitle: "Excel 导出",
370 385
     exportStatYear: "所属年份",
371 386
     exportStatYearPh: "请选择所属年份",
387
+    exportStatMonth: "所属月份",
388
+    exportStatMonthPh: "请选择所属月份",
372 389
     exportStatYearRequired: "请选择所属年份",
373
-    exportStatYearInvalid: "年份不在可选范围",
390
+    exportStatMonthRequired: "请选择所属月份",
391
+    exportStatYearInvalid: "导出年月不在可选范围",
374 392
     exportLoading: "正在导出...",
375 393
     exportFail: "导出失败",
376 394
     importTitle: "Excel 导入",
377 395
     importStatYear: "填报年份",
378 396
     importStatYearPh: "请选择年份",
379
-    importTip: "请先选择填报年份,再上传 .xlsx 文件",
397
+    importStatMonth: "填报月份",
398
+    importStatMonthPh: "请选择月份",
399
+    importTip: "请先选择填报年月,再上传 .xlsx 文件",
380 400
     importLoading: "正在导入...",
381 401
     importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
382 402
     importWarnTitle: "导入警告",
@@ -391,8 +411,9 @@ export default {
391 411
     unitSheep: "只",
392 412
     unitHorse: "匹",
393 413
     ruleStatYearRequired: "请选择年份",
414
+    ruleStatMonthRequired: "请选择月份",
415
+    ruleStatMonthFuture: "不可填报未来月份",
394 416
     ruleTownRequired: "请选择乡镇",
395
-    ruleCountRequired: "请填写",
396 417
     ruleCountNonNegative: "≥0 整数"
397 418
   }
398 419
 }

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

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

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

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

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

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

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

@@ -8,6 +8,12 @@
8 8
             <el-option v-for="y in yearOptions" :key="y" :label="String(y)" :value="y" />
9 9
           </el-select>
10 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 17
         <el-form-item prop="townDeptId">
12 18
           <template slot="label">{{ dmT("queryTown") }}</template>
13 19
           <el-select v-model="queryParams.townDeptId" :placeholder="dmCommon('pleaseSelect')" clearable filterable style="width: 180px">
@@ -53,6 +59,9 @@
53 59
           <span>{{ dmT("emptyList") }}</span>
54 60
         </template>
55 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 65
         <el-table-column :label="dmT('colTownName')" prop="townName" align="center" min-width="120" :show-overflow-tooltip="true" />
57 66
         <el-table-column :label="dmT('colVillageCount')" align="center" width="90">
58 67
           <template slot-scope="scope">{{ formatCount(scope.row.villageCount) }}</template>
@@ -90,7 +99,7 @@
90 99
       <el-form :key="formKey" ref="form" :model="form" label-width="150px" size="small">
91 100
         <div class="yhi-section-title">{{ dmT("sectionBasic") }}</div>
92 101
         <el-row :gutter="16">
93
-          <el-col :span="12">
102
+          <el-col :span="8">
94 103
             <el-form-item prop="statYear" :rules="formRules.statYear">
95 104
               <template slot="label">{{ dmT("formStatYear") }}</template>
96 105
               <el-select v-model="form.statYear" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
@@ -98,7 +107,15 @@
98 107
               </el-select>
99 108
             </el-form-item>
100 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 119
             <el-form-item prop="townDeptId" :rules="formRules.townDeptId">
103 120
               <template slot="label">{{ dmT("formTown") }}</template>
104 121
               <el-select v-model="form.townDeptId" :placeholder="dmCommon('pleaseSelect')" filterable style="width: 100%">
@@ -145,11 +162,26 @@
145 162
               <span>{{ viewRow.statYear || dash }}</span>
146 163
             </el-form-item>
147 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 170
           <el-col :span="12">
149 171
             <el-form-item :label="dmT('formTown')">
150 172
               <span>{{ viewRow.townName || dash }}</span>
151 173
             </el-form-item>
152 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 185
           <el-col v-for="field in countFields" :key="'v-' + field.prop" :span="12">
154 186
             <el-form-item :label="dmT(field.labelKey)">
155 187
               <span>{{ formatCount(viewRow[field.prop]) }}</span>
@@ -200,6 +232,12 @@
200 232
             <el-option v-for="y in yearOptions" :key="'i-' + y" :label="String(y)" :value="y" />
201 233
           </el-select>
202 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 241
         <p class="yhi-import-tip">{{ dmT("importTip") }}</p>
204 242
         <el-upload
205 243
           ref="importUpload"
@@ -230,6 +268,12 @@
230 268
             <el-option v-for="y in yearOptions" :key="'e-' + y" :label="String(y)" :value="y" />
231 269
           </el-select>
232 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 277
       </el-form>
234 278
       <div slot="footer" class="dialog-footer">
235 279
         <el-button type="primary" :loading="exporting" @click="handleExportSubmit">{{ dmCommon("ok") }}</el-button>
@@ -248,6 +292,7 @@ import {
248 292
   updateYakHerdInventory,
249 293
   delYakHerdInventory,
250 294
   listYakHerdInventoryYearOptions,
295
+  listYakHerdInventoryMonthOptions,
251 296
   listYakHerdInventoryTownOptions,
252 297
   importYakHerdInventory,
253 298
   downloadYakHerdInventoryTemplate,
@@ -270,10 +315,19 @@ const COUNT_FIELDS = [
270 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 326
 function createEmptyForm() {
274 327
   const form = {
275 328
     id: undefined,
276 329
     statYear: undefined,
330
+    statMonth: undefined,
277 331
     townDeptId: undefined,
278 332
     remark: undefined
279 333
   }
@@ -310,18 +364,22 @@ export default {
310 364
       total: 0,
311 365
       tableList: [],
312 366
       yearOptions: [],
367
+      monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
313 368
       townOptions: [],
314 369
       viewRow: {},
315 370
       importForm: {
316
-        statYear: undefined
371
+        statYear: undefined,
372
+        statMonth: undefined
317 373
       },
318 374
       exportForm: {
319
-        statYear: undefined
375
+        statYear: undefined,
376
+        statMonth: undefined
320 377
       },
321 378
       queryParams: {
322 379
         pageNum: 1,
323 380
         pageSize: 20,
324 381
         statYear: undefined,
382
+        statMonth: undefined,
325 383
         townDeptId: undefined
326 384
       },
327 385
       form: createEmptyForm()
@@ -340,6 +398,10 @@ export default {
340 398
     formRules() {
341 399
       return {
342 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 405
         townDeptId: [{ required: true, message: this.dmT("ruleTownRequired"), trigger: "change" }],
344 406
         count: [
345 407
           { required: true, message: this.dmT("ruleCountRequired"), trigger: "change" },
@@ -350,12 +412,20 @@ export default {
350 412
     },
351 413
     importRules() {
352 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 422
     exportRules() {
357 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 438
       listYakHerdInventoryYearOptions().then((res) => {
369 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 446
       listYakHerdInventoryTownOptions().then((res) => {
372 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 489
     formatCount(val) {
376 490
       if (val == null || val === "") {
377 491
         return this.dash
@@ -397,6 +511,9 @@ export default {
397 511
       if (this.queryParams.statYear != null) {
398 512
         q.statYear = this.queryParams.statYear
399 513
       }
514
+      if (this.queryParams.statMonth != null) {
515
+        q.statMonth = this.queryParams.statMonth
516
+      }
400 517
       if (this.queryParams.townDeptId != null) {
401 518
         q.townDeptId = this.queryParams.townDeptId
402 519
       }
@@ -406,6 +523,7 @@ export default {
406 523
       const remarkText = this.form.remark != null ? String(this.form.remark).trim() : ""
407 524
       const payload = {
408 525
         statYear: this.form.statYear,
526
+        statMonth: this.form.statMonth,
409 527
         townDeptId: this.form.townDeptId,
410 528
         remark: remarkText !== "" ? remarkText : undefined
411 529
       }
@@ -431,13 +549,17 @@ export default {
431 549
     },
432 550
     resetQuery() {
433 551
       this.queryParams.statYear = undefined
552
+      this.queryParams.statMonth = undefined
434 553
       this.queryParams.townDeptId = undefined
435 554
       this.resetForm("queryForm")
436 555
       this.handleQuery()
437 556
     },
438 557
     handleAdd() {
439 558
       this.formKey += 1
559
+      const current = getCurrentYearMonth()
440 560
       this.form = createEmptyForm()
561
+      this.form.statYear = current.statYear
562
+      this.form.statMonth = current.statMonth
441 563
       this.dialogEdit = false
442 564
       this.open = true
443 565
       this.$nextTick(() => {
@@ -508,7 +630,9 @@ export default {
508 630
       this.form = createEmptyForm()
509 631
     },
510 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 636
       this.importOpen = true
513 637
       this.$nextTick(() => {
514 638
         this.resetImportFiles()
@@ -522,6 +646,7 @@ export default {
522 646
     resetImport() {
523 647
       this.importing = false
524 648
       this.importForm.statYear = undefined
649
+      this.importForm.statMonth = undefined
525 650
       this.resetImportFiles()
526 651
     },
527 652
     handleDownloadTemplate() {
@@ -545,7 +670,9 @@ export default {
545 670
         })
546 671
     },
547 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 676
       this.exportOpen = true
550 677
       this.$nextTick(() => {
551 678
         if (this.$refs.exportFormRef) {
@@ -556,6 +683,7 @@ export default {
556 683
     resetExport() {
557 684
       this.exporting = false
558 685
       this.exportForm.statYear = undefined
686
+      this.exportForm.statMonth = undefined
559 687
     },
560 688
     handleExportSubmit() {
561 689
       this.$refs.exportFormRef.validate((valid) => {
@@ -563,14 +691,15 @@ export default {
563 691
           return
564 692
         }
565 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 696
           this.$modal.msgError(this.dmT("exportStatYearInvalid"))
568 697
           return
569 698
         }
570
-        const filename = `${statYear}年牦牛存栏数据.xlsx`
699
+        const filename = `${statYear}年${statMonth}月牦牛存栏数据.xlsx`
571 700
         this.exporting = true
572 701
         this.$modal.loading(this.dmT("exportLoading"))
573
-        exportYakHerdInventory(statYear)
702
+        exportYakHerdInventory(statYear, statMonth)
574 703
           .then(async (data) => {
575 704
             if (blobValidate(data)) {
576 705
               saveAs(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), filename)
@@ -609,7 +738,7 @@ export default {
609 738
         }
610 739
         this.importing = true
611 740
         this.$modal.loading(this.dmT("importLoading"))
612
-        importYakHerdInventory(raw, this.importForm.statYear)
741
+        importYakHerdInventory(raw, this.importForm.statYear, this.importForm.statMonth)
613 742
           .then((res) => {
614 743
             this.importOpen = false
615 744
             this.showImportResult(res.data || {})

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

@@ -8,6 +8,12 @@
8 8
             <el-option v-for="y in yearOptions" :key="y" :label="String(y)" :value="y" />
9 9
           </el-select>
10 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 17
         <el-form-item prop="townDeptId">
12 18
           <template slot="label">{{ dmT("queryTown") }}</template>
13 19
           <el-select v-model="queryParams.townDeptId" :placeholder="dmCommon('pleaseSelect')" clearable filterable style="width: 180px">
@@ -53,6 +59,9 @@
53 59
           <span>{{ dmT("emptyList") }}</span>
54 60
         </template>
55 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 65
         <el-table-column :label="dmT('colTownName')" prop="townName" align="center" min-width="120" :show-overflow-tooltip="true" />
57 66
         <el-table-column :label="dmT('colFarmerHouseholdCount')" align="center" width="100">
58 67
           <template slot-scope="scope">{{ formatCount(scope.row.farmerHouseholdCount) }}</template>
@@ -87,7 +96,7 @@
87 96
       <el-form :key="formKey" ref="form" :model="form" label-width="168px" size="small">
88 97
         <div class="yor-section-title">{{ dmT("sectionBasic") }}</div>
89 98
         <el-row :gutter="16">
90
-          <el-col :span="12">
99
+          <el-col :span="8">
91 100
             <el-form-item prop="statYear" :rules="formRules.statYear">
92 101
               <template slot="label">{{ dmT("formStatYear") }}</template>
93 102
               <el-select v-model="form.statYear" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
@@ -95,7 +104,15 @@
95 104
               </el-select>
96 105
             </el-form-item>
97 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 116
             <el-form-item prop="townDeptId" :rules="formRules.townDeptId">
100 117
               <template slot="label">{{ dmT("formTown") }}</template>
101 118
               <el-select v-model="form.townDeptId" :placeholder="dmCommon('pleaseSelect')" filterable style="width: 100%">
@@ -180,6 +197,11 @@
180 197
               <span>{{ viewRow.statYear || dash }}</span>
181 198
             </el-form-item>
182 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 205
           <el-col :span="12">
184 206
             <el-form-item :label="dmT('formTown')">
185 207
               <span>{{ viewRow.townName || dash }}</span>
@@ -210,6 +232,12 @@
210 232
             <el-option v-for="y in yearOptions" :key="'i-' + y" :label="String(y)" :value="y" />
211 233
           </el-select>
212 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 241
         <p class="yor-import-tip">{{ dmT("importTip") }}</p>
214 242
         <el-upload
215 243
           ref="importUpload"
@@ -240,6 +268,12 @@
240 268
             <el-option v-for="y in yearOptions" :key="'e-' + y" :label="String(y)" :value="y" />
241 269
           </el-select>
242 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 277
       </el-form>
244 278
       <div slot="footer" class="dialog-footer">
245 279
         <el-button type="primary" :loading="exporting" @click="handleExportSubmit">{{ dmCommon("ok") }}</el-button>
@@ -258,6 +292,7 @@ import {
258 292
   updateYakOutboundReport,
259 293
   delYakOutboundReport,
260 294
   listYakOutboundReportYearOptions,
295
+  listYakOutboundReportMonthOptions,
261 296
   listYakOutboundReportTownOptions,
262 297
   importYakOutboundReport,
263 298
   downloadYakOutboundReportTemplate,
@@ -287,10 +322,19 @@ const VIEW_FIELDS = [
287 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 333
 function createEmptyForm() {
291 334
   const form = {
292 335
     id: undefined,
293 336
     statYear: undefined,
337
+    statMonth: undefined,
294 338
     townDeptId: undefined,
295 339
     remark: undefined
296 340
   }
@@ -327,18 +371,22 @@ export default {
327 371
       total: 0,
328 372
       tableList: [],
329 373
       yearOptions: [],
374
+      monthOptions: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12],
330 375
       townOptions: [],
331 376
       viewRow: {},
332 377
       importForm: {
333
-        statYear: undefined
378
+        statYear: undefined,
379
+        statMonth: undefined
334 380
       },
335 381
       exportForm: {
336
-        statYear: undefined
382
+        statYear: undefined,
383
+        statMonth: undefined
337 384
       },
338 385
       queryParams: {
339 386
         pageNum: 1,
340 387
         pageSize: 20,
341 388
         statYear: undefined,
389
+        statMonth: undefined,
342 390
         townDeptId: undefined
343 391
       },
344 392
       form: createEmptyForm()
@@ -366,22 +414,31 @@ export default {
366 414
     formRules() {
367 415
       return {
368 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 423
         remark: [{ max: 500, message: this.dmCommon("ruleLen500"), trigger: "blur" }]
375 424
       }
376 425
     },
377 426
     importRules() {
378 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 435
     exportRules() {
383 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 451
       listYakOutboundReportYearOptions().then((res) => {
395 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 459
       listYakOutboundReportTownOptions().then((res) => {
398 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 502
     formatCount(val) {
402 503
       if (val == null || val === "") {
403 504
         return this.dash
404 505
       }
405 506
       return val
406 507
     },
407
-    validateRequiredCount(rule, value, callback) {
508
+    validateOptionalCount(rule, value, callback) {
408 509
       if (value == null || value === "") {
409
-        callback(new Error(this.dmT("ruleCountRequired")))
510
+        callback()
410 511
         return
411 512
       }
412 513
       if (!Number.isInteger(value) || value < 0) {
@@ -423,6 +524,9 @@ export default {
423 524
       if (this.queryParams.statYear != null) {
424 525
         q.statYear = this.queryParams.statYear
425 526
       }
527
+      if (this.queryParams.statMonth != null) {
528
+        q.statMonth = this.queryParams.statMonth
529
+      }
426 530
       if (this.queryParams.townDeptId != null) {
427 531
         q.townDeptId = this.queryParams.townDeptId
428 532
       }
@@ -432,6 +536,7 @@ export default {
432 536
       const remarkText = this.form.remark != null ? String(this.form.remark).trim() : ""
433 537
       const payload = {
434 538
         statYear: this.form.statYear,
539
+        statMonth: this.form.statMonth,
435 540
         townDeptId: this.form.townDeptId,
436 541
         remark: remarkText !== "" ? remarkText : undefined
437 542
       }
@@ -457,13 +562,17 @@ export default {
457 562
     },
458 563
     resetQuery() {
459 564
       this.queryParams.statYear = undefined
565
+      this.queryParams.statMonth = undefined
460 566
       this.queryParams.townDeptId = undefined
461 567
       this.resetForm("queryForm")
462 568
       this.handleQuery()
463 569
     },
464 570
     handleAdd() {
465 571
       this.formKey += 1
572
+      const current = getCurrentYearMonth()
466 573
       this.form = createEmptyForm()
574
+      this.form.statYear = current.statYear
575
+      this.form.statMonth = current.statMonth
467 576
       this.dialogEdit = false
468 577
       this.open = true
469 578
       this.$nextTick(() => {
@@ -534,7 +643,9 @@ export default {
534 643
       this.form = createEmptyForm()
535 644
     },
536 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 649
       this.importOpen = true
539 650
       this.$nextTick(() => {
540 651
         this.resetImportFiles()
@@ -548,6 +659,7 @@ export default {
548 659
     resetImport() {
549 660
       this.importing = false
550 661
       this.importForm.statYear = undefined
662
+      this.importForm.statMonth = undefined
551 663
       this.resetImportFiles()
552 664
     },
553 665
     handleDownloadTemplate() {
@@ -571,7 +683,9 @@ export default {
571 683
         })
572 684
     },
573 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 689
       this.exportOpen = true
576 690
       this.$nextTick(() => {
577 691
         if (this.$refs.exportFormRef) {
@@ -582,6 +696,7 @@ export default {
582 696
     resetExport() {
583 697
       this.exporting = false
584 698
       this.exportForm.statYear = undefined
699
+      this.exportForm.statMonth = undefined
585 700
     },
586 701
     handleExportSubmit() {
587 702
       this.$refs.exportFormRef.validate((valid) => {
@@ -589,14 +704,15 @@ export default {
589 704
           return
590 705
         }
591 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 709
           this.$modal.msgError(this.dmT("exportStatYearInvalid"))
594 710
           return
595 711
         }
596
-        const filename = `${statYear}年牦牛出栏数据.xlsx`
712
+        const filename = `${statYear}年${statMonth}月牦牛出栏数据.xlsx`
597 713
         this.exporting = true
598 714
         this.$modal.loading(this.dmT("exportLoading"))
599
-        exportYakOutboundReport(statYear)
715
+        exportYakOutboundReport(statYear, statMonth)
600 716
           .then(async (data) => {
601 717
             if (blobValidate(data)) {
602 718
               saveAs(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), filename)
@@ -635,7 +751,7 @@ export default {
635 751
         }
636 752
         this.importing = true
637 753
         this.$modal.loading(this.dmT("importLoading"))
638
-        importYakOutboundReport(raw, this.importForm.statYear)
754
+        importYakOutboundReport(raw, this.importForm.statYear, this.importForm.statMonth)
639 755
           .then((res) => {
640 756
             this.importOpen = false
641 757
             this.showImportResult(res.data || {})

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

@@ -42,25 +42,18 @@
42 42
     <el-card shadow="never" class="bi-section-card">
43 43
       <div slot="header" class="bi-section-header">{{ biT("sectionInventoryChange") }}</div>
44 44
       <el-row :gutter="16" class="bi-inventory-stats">
45
-        <el-col :xs="24" :sm="8">
45
+        <el-col :xs="24" :sm="12">
46 46
           <div class="bi-stat-block">
47 47
             <div class="bi-stat-block__value">{{ formatHead(inventoryChange.bullCount) }}</div>
48 48
             <div class="bi-stat-block__label">{{ biT("bullCount") }}</div>
49 49
           </div>
50 50
         </el-col>
51
-        <el-col :xs="24" :sm="8">
51
+        <el-col :xs="24" :sm="12">
52 52
           <div class="bi-stat-block">
53 53
             <div class="bi-stat-block__value">{{ formatHead(inventoryChange.cowCount) }}</div>
54 54
             <div class="bi-stat-block__label">{{ biT("cowCount") }}</div>
55 55
           </div>
56 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 57
       </el-row>
65 58
       <div class="bi-chart-title">{{ biT("chartInventoryTrend") }}</div>
66 59
       <div ref="chartInventory" class="bi-chart-box" />
@@ -126,9 +119,7 @@ import { getBreedingDashboard } from "@/api/livestockIndustry/breedingIndustry"
126 119
 
127 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 124
 export default {
134 125
   name: "BreedingIndustry",
@@ -160,9 +151,8 @@ export default {
160 151
     },
161 152
     inventoryChange() {
162 153
       return (this.dashboardData && this.dashboardData.inventoryChange) || {
163
-        bullCount: 37466,
164
-        cowCount: 37449,
165
-        unknownGenderCount: 0,
154
+        bullCount: 0,
155
+        cowCount: 0,
166 156
         monthlyTrend: []
167 157
       }
168 158
     },
@@ -179,11 +169,14 @@ export default {
179 169
     degradationTable() {
180 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 182
   mounted() {
@@ -315,19 +308,35 @@ export default {
315 308
       this.renderAgeChart()
316 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 341
     renderInventoryChart() {
333 342
       const dom = this.$refs.chartInventory
@@ -335,25 +344,58 @@ export default {
335 344
       if (!chart) {
336 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 373
       chart.setOption(
341 374
         {
342 375
           tooltip: {
343 376
             trigger: "axis",
344 377
             formatter: (params) => {
345
-              const p = params.find((x) => x.value != null)
346
-              if (!p) {
378
+              if (!Array.isArray(params) || !params.length) {
347 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 394
           xAxis: {
354 395
             type: "category",
355 396
             boundaryGap: false,
356
-            data: this.monthLabels
397
+            data: axis.categories,
398
+            axisLabel: { interval: 0, fontSize: 10 }
357 399
           },
358 400
           yAxis: {
359 401
             type: "value",
@@ -362,11 +404,20 @@ export default {
362 404
           },
363 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 416
               type: "line",
367 417
               smooth: true,
368
-              connectNulls: false,
369
-              data: seriesData
418
+              symbol: "circle",
419
+              symbolSize: 4,
420
+              data: cowData
370 421
             }
371 422
           ],
372 423
           graphic: hasPoints
@@ -394,7 +445,7 @@ export default {
394 445
       if (!chart) {
395 446
         return
396 447
       }
397
-      const list = this.ageStructure
448
+      const list = this.sortedAgeStructure
398 449
       const pieData = list.map((item) => ({
399 450
         name: item.bandLabel,
400 451
         value: item.count