xsh_1997 1 mēnesi atpakaļ
vecāks
revīzija
4cde14fbc9

+ 127 - 0
doc/产业数据模型及服务/牦牛出栏数据填报/牦牛出栏数据填报前端技术方案.md

@@ -0,0 +1,127 @@
1
+# 牦牛出栏数据填报 — 前端技术方案
2
+
3
+> 依据:同目录 `牦牛出栏数据填报功能需求.md`、`牦牛出栏数据填报技术方案.md`;布局对齐 `dataModel/yakHerdInventory/index.vue`(双 `el-card` + 分页表格);字段 **小驼峰**。
4
+
5
+---
6
+
7
+## 1. 页面与路由
8
+
9
+| 项 | 说明 |
10
+| --- | --- |
11
+| **Vue 路径** | `ruoyi-ui/src/views/dataModel/yakOutboundReport/index.vue` |
12
+| **组件名** | `YakOutboundReport` |
13
+| **菜单配置** | 组件路径:`dataModel/yakOutboundReport/index`(「产业数据模型及服务」目录下) |
14
+| **权限前缀** | `dataModel:yakOutboundReport:list\|query\|add\|edit\|remove\|import\|export` |
15
+
16
+---
17
+
18
+## 2. API 模块
19
+
20
+| 项 | 说明 |
21
+| --- | --- |
22
+| **文件** | `ruoyi-ui/src/api/dataModel/yakOutboundReport.js` |
23
+| **Base** | `/dataModel/yakOutboundReport` |
24
+
25
+| 方法 | HTTP | 说明 |
26
+| --- | --- | --- |
27
+| `listYakOutboundReport` | GET | `/list` 分页列表 |
28
+| `getYakOutboundReport` | GET | `/{id}` 详情 |
29
+| `addYakOutboundReport` | POST | `/` 新增 |
30
+| `updateYakOutboundReport` | PUT | `/` 修改 |
31
+| `delYakOutboundReport` | DELETE | `/{ids}` 删除 |
32
+| `listYakOutboundReportYearOptions` | GET | `/yearOptions` 可选年份 |
33
+| `listYakOutboundReportTownOptions` | GET | `/townOptions` 乡镇下拉 |
34
+| `importYakOutboundReport` | POST | `/importData`(`multipart/form-data`:`file` + `statYear`) |
35
+| `downloadYakOutboundReportTemplate` | GET | `/importTemplate` 空白模板(`responseType: 'blob'`) |
36
+| `exportYakOutboundReport` | GET | `/export?statYear=` 按年份导出(`responseType: 'blob'`) |
37
+
38
+模板下载、按年导出均使用 **GET** + `responseType: 'blob'`,勿用若依全局 `this.download()`(固定 **POST**)。
39
+
40
+---
41
+
42
+## 3. 页面结构
43
+
44
+1. **筛选区**:所属年份(`yearOptions`)、所属乡镇(`townOptions`);**搜索**、**重置**、**新增**、**导入**、**导出**。  
45
+2. **列表区**:所属年份、所属乡镇、农牧户户数、农牧户人数、牦牛出栏数、自食情况(牛);空值展示 `—`。  
46
+3. **操作列**:查看、修改、删除。  
47
+4. **新增/编辑弹窗**:年份/乡镇必填;出栏及自食指标选填,`el-input-number`(`:min="0"` `:precision="0"`);备注选填。表单分四块:基本信息、农牧户情况、出栏情况、自食情况。  
48
+5. **查看弹窗**:只读展示需求 **§5.5** 六字段(年份、乡镇、农牧户户数、农牧户人数、牦牛出栏数、自食牛头数);可跳转编辑。  
49
+6. **导入弹窗**:先选「填报年份」→ 上传 `.xlsx` → 展示 insert/update/fail 及 fail/warn 明细。  
50
+7. **导出弹窗**:选择「所属年份」→ `GET /export` 下载 `{statYear}年牦牛出栏数据.xlsx`。
51
+
52
+---
53
+
54
+## 4. 业务规则(前端)
55
+
56
+| 规则 | 实现 |
57
+| --- | --- |
58
+| 年份范围 | 由 `/yearOptions` 动态加载(当前年起向前 5 年,共 6 年) |
59
+| 乡镇下拉 | `/townOptions` 返回 `deptId` / `deptName` |
60
+| 列表筛选 | `statYear`、`townDeptId` 精确匹配 |
61
+| 数量字段 | 选填;填写时须为 ≥ 0 的整数 |
62
+| 单位后缀 | 户、人、头、只、匹(见 i18n `unit*`) |
63
+| 唯一性 | 同年同乡镇重复新增由后端拦截,前端展示 `msg` |
64
+| 导入 | `FormData` 提交;全局 loading;成功后 `$alert` 展示摘要与明细 |
65
+| 导出 | 弹框选年;年份须在 `yearOptions` 内 |
66
+| 删除 | `$modal.confirm` 后 `delYakOutboundReport(id)`,物理删除 |
67
+
68
+---
69
+
70
+## 5. 字段映射(表单 / 接口 Body)
71
+
72
+| 前端 prop | 说明 | 单位 |
73
+| --- | --- | --- |
74
+| `statYear` | 所属年份 | — |
75
+| `townDeptId` | 所属乡镇 | — |
76
+| `farmerHouseholdCount` | 农牧户户数 | 户 |
77
+| `farmerPopulationCount` | 农牧户人数 | 人 |
78
+| `yakOutboundCount` | 牦牛出栏数 | 头 |
79
+| `cattleOutboundCount` | 黄牛/奶牛/犏牛出栏数 | 头 |
80
+| `sheepOutboundCount` | 绵羊出栏数 | 只 |
81
+| `goatOutboundCount` | 山羊出栏数 | 只 |
82
+| `horseMuleOutboundCount` | 马骡出栏数 | 匹 |
83
+| `donkeyOutboundCount` | 驴出栏数 | 头 |
84
+| `selfConsumptionCattleCount` | 自食牛头数 | 头 |
85
+| `selfConsumptionSheepCount` | 自食绵羊只数 | 只 |
86
+| `selfConsumptionGoatCount` | 自食山羊只数 | 只 |
87
+| `remark` | 备注 | — |
88
+
89
+---
90
+
91
+## 6. 国际化
92
+
93
+| 项 | 说明 |
94
+| --- | --- |
95
+| **语言包** | `dataModel.yakOutboundReport`、`dataModel.common` |
96
+| **文件** | `lang/zh/dataModel.js`、`lang/bo/dataModel.js` |
97
+| **Mixin** | `dataModelLocaleMixin`,`dmNs = 'yakOutboundReport'`,`dmT` / `dmCommon` |
98
+| **注册** | 已随 `dataModel` 模块在 `lang/zh-CN.js`、`lang/bo.js` 引入 |
99
+
100
+---
101
+
102
+## 7. 联调说明
103
+
104
+1. 部署后端 `BizYakOutboundReportController` 及 `biz_yak_outbound_report` 表。  
105
+2. 菜单组件 `dataModel/yakOutboundReport/index`,权限与后端 §5 一致。  
106
+3. 执行 `sql/biz_yak_outbound_report.sql`(及乡镇 `sys_dept` 数据)。  
107
+4. 验证:年份/乡镇筛选、CRUD、导入模板下载、导入结果反馈、按年导出、双语切换。  
108
+5. 导入联调:选填报年份 → 上传 `牲畜数据.xlsx` 第 6 Sheet 格式文件;对照 `failMessages` / `warnMessages`。
109
+
110
+---
111
+
112
+## 8. 文件清单
113
+
114
+| 类型 | 路径 |
115
+| --- | --- |
116
+| 页面 | `ruoyi-ui/src/views/dataModel/yakOutboundReport/index.vue` |
117
+| API | `ruoyi-ui/src/api/dataModel/yakOutboundReport.js` |
118
+| Mixin | `ruoyi-ui/src/mixins/dataModelLocaleMixin.js` |
119
+| i18n | `ruoyi-ui/src/lang/zh/dataModel.js`、`lang/bo/dataModel.js` |
120
+
121
+---
122
+
123
+## 9. 修订记录
124
+
125
+| 版本 | 说明 |
126
+| --- | --- |
127
+| 1.0 | 初版:列表/CRUD/详情/导入/模板下载/按年导出;汉藏 i18n;对齐功能需求 v1.1 |

+ 0 - 126
doc/大屏/集中化GIS地图对接技术方案.md

@@ -1,126 +0,0 @@
1
-# 大屏 — 集中化 GIS 地图对接技术方案
2
-
3
-> 依据《中国移动集中化 GIS 平台 JSAPI 接口协议 v1.0》(`集中化GIS系统JSAPI接口文档.docx`)。  
4
-> 前端引擎为 **cmmap GL JS**(API 形态与 Mapbox GL JS 接近),全局对象 **`cmmapgl`**。
5
-
6
----
7
-
8
-## 1. 对接前提(向 GIS 平台申请)
9
-
10
-| 项 | 说明 |
11
-| --- | --- |
12
-| **GIS 服务根地址** | 文档中的「GIS 服务地址」,如 `https://gis.example.com` |
13
-| **地图密钥 accessToken** | 文档 §2.2;初始化前赋值 `cmmapgl.accessToken` |
14
-| **底图样式 style** | Mapbox Style 规范 JSON 或样式 URL(`MapOptions.style`) |
15
-| **跨域** | 生产由 GIS/Nginx 放行;开发可用 Vite 代理 `/gis-dev-proxy` |
16
-
17
----
18
-
19
-## 2. 静态资源引入
20
-
21
-文档 §2.1 要求在页面引入(将 `GIS服务地址` 替换为实际根地址):
22
-
23
-```html
24
-<script src="{GIS_BASE}/agis/resources/v1/tile-server/dist/cmmap-gl/1.0/cmmap-gl.js"></script>
25
-<link href="{GIS_BASE}/agis/resources/v1/tile-server/dist/cmmap-gl/1.0/cmmap-gl.css" rel="stylesheet" />
26
-```
27
-
28
-`ruoyi-screen` 通过 `src/utils/gisLoader.js` **按需动态加载**,避免未配置 GIS 时阻塞整站。
29
-
30
----
31
-
32
-## 3. 初始化流程
33
-
34
-```text
35
-loadGisSdk() → cmmapgl.accessToken = token → new cmmapgl.Map({ container, center, zoom, style })
36
-```
37
-
38
-| 参数 | 说明 |
39
-| --- | --- |
40
-| `container` | 地图容器 DOM 或 id |
41
-| `center` | **`[经度, 纬度]`**(与 GeoJSON 一致,勿写反) |
42
-| `zoom` | 0–24,默认约 2–9 |
43
-| `style` | 底图样式 URL 或 JSON |
44
-| `accessToken` | 也可在 `MapOptions` 中传 `accessToken`,会覆盖全局 `cmmapgl.accessToken` |
45
-
46
-巴青县默认中心(可在页面覆盖):`[94.05, 31.92]`,缩放 `9`。
47
-
48
----
49
-
50
-## 4. 常用能力(按大屏场景)
51
-
52
-| 文档章节 | 能力 | 大屏典型用途 |
53
-| --- | --- | --- |
54
-| §3.1 | `cmmapgl.Map` | 底图展示 |
55
-| §3.3 | `NavigationControl` / `ScaleControl` | 导航、比例尺(大屏可关闭交互) |
56
-| §4 | `addSource` + `addLayer` | 矢量/栅格/GeoJSON 业务图层 |
57
-| §5.1 | `cmmapgl.Marker` | 牧场、检疫站、交易市场点位 |
58
-| §5.2 | `Popup` | 点位详情气泡 |
59
-| §7 | 热力图、散点、轨迹等 | 疫病风险、资源分布 |
60
-| §8 | `agis.turf` | 空间计算(点线面关系) |
61
-| §10–11 | POI / 路线 | 后续扩展 |
62
-
63
----
64
-
65
-## 5. ruoyi-screen 工程约定
66
-
67
-### 5.1 环境变量(`.env.development` / `.env.production`)
68
-
69
-```bash
70
-# GIS 服务根地址(勿带末尾 /)
71
-VITE_GIS_BASE_URL=/gis-dev-proxy
72
-# 地图密钥(§2.2,由 GIS 平台发放)
73
-VITE_GIS_ACCESS_TOKEN=
74
-# 底图样式 URL 或 JSON 路径
75
-VITE_GIS_MAP_STYLE=
76
-# 默认中心:经度,纬度
77
-VITE_GIS_DEFAULT_CENTER=94.05,31.92
78
-VITE_GIS_DEFAULT_ZOOM=9
79
-```
80
-
81
-开发环境在 `vite.config.js` 将 `/gis-dev-proxy` 代理到真实 GIS 服务(`VITE_GIS_PROXY_TARGET`)。
82
-
83
-### 5.2 代码入口
84
-
85
-| 文件 | 职责 |
86
-| --- | --- |
87
-| `src/utils/gisLoader.js` | 动态加载 cmmap-gl.js/css、读取 env |
88
-| `src/components/GisMap.vue` | 通用地图容器,`@ready` 抛出 `map` 实例 |
89
-
90
-### 5.3 页面使用示例
91
-
92
-```vue
93
-<GisMap
94
-  class="my-map"
95
-  :interactive="false"
96
-  @ready="onMapReady"
97
-  @error="onMapError"
98
-/>
99
-```
100
-
101
-```javascript
102
-function onMapReady(map) {
103
-  // map 为 cmmapgl.Map 实例
104
-  // map.addSource / addLayer / Marker 等见文档 §4–§5
105
-}
106
-```
107
-
108
----
109
-
110
-## 6. 注意事项
111
-
112
-1. **坐标顺序**:一律 `[lng, lat]`。
113
-2. **容器尺寸**:父级须有明确宽高(大屏组件内 `width/height: 100%`)。
114
-3. **销毁**:路由离开须 `map.remove()`(`GisMap.vue` 已在 `onUnmounted` 处理)。
115
-4. **resize**:大屏 `screenFit` 缩放后若地图空白,在布局稳定后调用 `map.resize()`。
116
-5. **token 缺失**:未配置 `VITE_GIS_ACCESS_TOKEN` 时组件展示占位提示,不抛白屏。
117
-
118
----
119
-
120
-## 7. 交付清单
121
-
122
-- [x] `gisLoader.js` 动态加载 SDK
123
-- [x] `GisMap.vue` 基础地图组件
124
-- [x] Vite 开发代理占位
125
-- [x] 业务页嵌入地图(`ScreenLayout` 中间栏共用底图,各子页左右面板叠在两侧)
126
-- [ ] 业务点位/GeoJSON 与后端接口联调

+ 0 - 0
doc/大屏/集中化GIS系统JSAPI-目录.txt


Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 0 - 1363
doc/大屏/集中化GIS系统JSAPI接口文档-提取.txt


+ 8 - 0
ruoyi-screen/src/api/home.js

@@ -17,3 +17,11 @@ export function getHomeStandards(params) {
17 17
     params
18 18
   })
19 19
 }
20
+
21
+/** 地图乡镇标注 GET /bigScreen/home/townMapMarkers */
22
+export function getTownMapMarkers() {
23
+  return request({
24
+    url: '/bigScreen/home/townMapMarkers',
25
+    method: 'get'
26
+  })
27
+}

+ 17 - 5
ruoyi-screen/src/components/GisMap.vue

@@ -1,5 +1,6 @@
1 1
 <script setup>
2 2
 import { onMounted, onUnmounted, ref, watch } from 'vue'
3
+import { getTownMapMarkers } from '@/api/home'
3 4
 import {
4 5
   getGisAccessToken,
5 6
   getGisDefaultCenter,
@@ -24,7 +25,7 @@ import {
24 25
   getTownshipLabelPoints,
25 26
   removeTownshipBoundaryLayers
26 27
 } from '@/utils/gisTownshipBoundary'
27
-import { getTownshipLivestockStats } from '@/constants/townshipLivestock'
28
+import { getTownshipLivestockStats, markersToTownshipLabels } from '@/constants/townshipLivestock'
28 29
 
29 30
 const props = defineProps({
30 31
   center: {
@@ -77,6 +78,7 @@ const props = defineProps({
77 78
 const emit = defineEmits(['ready', 'click', 'township-click'])
78 79
 
79 80
 const containerRef = ref(null)
81
+const townshipLabelData = ref({})
80 82
 
81 83
 let map = null
82 84
 let cmmapglRef = null
@@ -254,7 +256,7 @@ function createTownshipMarkerElement(name, stats) {
254 256
   if (stats && stats.inventory != null) {
255 257
     const inventoryEl = document.createElement('div')
256 258
     inventoryEl.className = 'gis-township-marker__stat'
257
-    inventoryEl.textContent = `栏:${stats.inventory}`
259
+    inventoryEl.textContent = `栏:${stats.inventory}`
258 260
     wrap.appendChild(inventoryEl)
259 261
   }
260 262
 
@@ -285,7 +287,10 @@ function renderTownshipBoundary() {
285 287
   }
286 288
 
287 289
   getTownshipLabelPoints().forEach((item) => {
288
-    const stats = getTownshipLivestockStats(item.name, props.townshipLabels)
290
+    const stats = getTownshipLivestockStats(item.name, {
291
+      ...townshipLabelData.value,
292
+      ...props.townshipLabels
293
+    })
289 294
     const marker = new Marker({
290 295
       element: createTownshipMarkerElement(item.name, stats),
291 296
       anchor: 'center'
@@ -681,7 +686,7 @@ watch(
681 686
 )
682 687
 
683 688
 watch(
684
-  () => [props.showTownshipBoundary, props.townshipLabels, props.townshipDrillDown],
689
+  () => [props.showTownshipBoundary, props.townshipLabels, props.townshipDrillDown, townshipLabelData.value],
685 690
   () => {
686 691
     if (!map || !cmmapglRef) return
687 692
     if (props.showTownshipBoundary) {
@@ -693,7 +698,14 @@ watch(
693 698
   { deep: true }
694 699
 )
695 700
 
696
-onMounted(() => {
701
+onMounted(async () => {
702
+  try {
703
+    const res = await getTownMapMarkers()
704
+    townshipLabelData.value = markersToTownshipLabels(res.data?.markers)
705
+  } catch (e) {
706
+    townshipLabelData.value = {}
707
+    console.warn('[GisMap] townMapMarkers load failed', e)
708
+  }
697 709
   initMap()
698 710
 })
699 711
 

+ 26 - 17
ruoyi-screen/src/constants/townshipLivestock.js

@@ -1,25 +1,34 @@
1
-/** 巴青县各乡镇牦牛存栏 / 出栏(大屏地图标注) */
2
-export const TOWNSHIP_LIVESTOCK_STATS = {
3
-  拉西镇: { inventory: 21587, outbound: 5670 },
4
-  雅安镇: { inventory: 12356, outbound: 3432 },
5
-  杂色镇: { inventory: 40489, outbound: 7028 },
6
-  本塔乡: { inventory: 11854, outbound: 2786 },
7
-  巴青乡: { inventory: 14158, outbound: 1523 },
8
-  岗切乡: { inventory: 32889, outbound: 3984 },
9
-  玛如乡: { inventory: 41186, outbound: 5632 },
10
-  贡日乡: { inventory: 7364, outbound: 1389 },
11
-  阿秀乡: { inventory: 14609, outbound: 1849 },
12
-  江绵乡: { inventory: 41575, outbound: 4156 }
1
+/**
2
+ * 将 /bigScreen/home/townMapMarkers 响应转为地图标注覆盖表
3
+ * @param {Array<{ townName: string, yakInventoryCount?: number, yakOutboundCount?: number }>} markers
4
+ * @returns {Record<string, { inventory: number, outbound: number }>}
5
+ */
6
+export function markersToTownshipLabels(markers) {
7
+  const labels = {}
8
+  if (!Array.isArray(markers)) {
9
+    return labels
10
+  }
11
+  markers.forEach((item) => {
12
+    const name = item?.townName != null ? String(item.townName).trim() : ''
13
+    if (!name) {
14
+      return
15
+    }
16
+    labels[name] = {
17
+      inventory: item.yakInventoryCount != null ? Number(item.yakInventoryCount) : 0,
18
+      outbound: item.yakOutboundCount != null ? Number(item.yakOutboundCount) : 0
19
+    }
20
+  })
21
+  return labels
13 22
 }
14 23
 
24
+/** 按乡镇名取存栏/出栏标注(仅使用接口或 props 传入的数据) */
15 25
 export function getTownshipLivestockStats(name, overrides = {}) {
16
-  const base = TOWNSHIP_LIVESTOCK_STATS[name]
17 26
   const custom = overrides?.[name]
18
-  if (!base && !custom) {
27
+  if (!custom || typeof custom !== 'object') {
19 28
     return null
20 29
   }
21
-  if (custom && typeof custom === 'object') {
22
-    return { ...base, ...custom }
30
+  return {
31
+    inventory: custom.inventory != null ? custom.inventory : 0,
32
+    outbound: custom.outbound != null ? custom.outbound : 0
23 33
   }
24
-  return base || null
25 34
 }

+ 5 - 8
ruoyi-screen/src/views/home/index.vue

@@ -20,23 +20,22 @@
20 20
         <div class="top_content">
21 21
           <div class="content_1">
22 22
             <div class="content_num">
23
-              <ScreenScrollNumber :value="YAK_INVENTORY_TOTAL" /> 头
23
+              <ScreenScrollNumber :value="overview?.inventoryTotal" /> 头
24 24
             </div>
25 25
             <div class="content_title"><div>牦牛存栏总量</div></div>
26 26
           </div>
27 27
           <div class="content_2">
28 28
             <div class="content_num">
29
-              <ScreenScrollNumber :value="YAK_ANNUAL_OUTBOUND" /> 头
29
+              <ScreenScrollNumber :value="overview?.annualOutbound" /> 头
30 30
             </div>
31 31
             <div class="content_title"><div>牦牛年出栏量</div></div>
32 32
           </div>
33 33
           <div class="content_3">
34 34
             <div class="content_num">
35
-              <!-- <ScreenScrollNumber :value="overview?.pastureCount" /> 个 -->
36
-              <ScreenScrollNumber :value="FARMER_COUNT" /> 户
35
+              <ScreenScrollNumber :value="overview?.pastureCount" /> 户
37 36
             </div>
38
-            <!-- <div class="content_title"><div>牧场数量</div></div> -->
39 37
             <div class="content_title"><div>农牧户</div></div>
38
+            <!-- 牧场数量 -->
40 39
           </div>
41 40
           <div class="content_4">
42 41
             <div class="content_num">
@@ -238,9 +237,7 @@ const STANDARD_TABS = [
238 237
 
239 238
 const STANDARD_FETCH_PAGE_SIZE = 50
240 239
 
241
-/** 产值总览:牦牛存栏/出栏固定展示值(不走接口) */
242
-const YAK_INVENTORY_TOTAL = 238067
243
-const YAK_ANNUAL_OUTBOUND = 37449
240
+/** 产值总览:农牧户固定展示值(不走接口) */
244 241
 const FARMER_COUNT = 8988
245 242
 
246 243
 const loading = ref(false)

+ 94 - 0
ruoyi-ui/src/api/dataModel/yakOutboundReport.js

@@ -0,0 +1,94 @@
1
+import request from "@/utils/request"
2
+
3
+/** 分页列表 */
4
+export function listYakOutboundReport(query) {
5
+  return request({
6
+    url: "/dataModel/yakOutboundReport/list",
7
+    method: "get",
8
+    params: query
9
+  })
10
+}
11
+
12
+/** 详情 */
13
+export function getYakOutboundReport(id) {
14
+  return request({
15
+    url: "/dataModel/yakOutboundReport/" + id,
16
+    method: "get"
17
+  })
18
+}
19
+
20
+/** 新增 */
21
+export function addYakOutboundReport(data) {
22
+  return request({
23
+    url: "/dataModel/yakOutboundReport",
24
+    method: "post",
25
+    data
26
+  })
27
+}
28
+
29
+/** 修改 */
30
+export function updateYakOutboundReport(data) {
31
+  return request({
32
+    url: "/dataModel/yakOutboundReport",
33
+    method: "put",
34
+    data
35
+  })
36
+}
37
+
38
+/** 删除 */
39
+export function delYakOutboundReport(ids) {
40
+  return request({
41
+    url: "/dataModel/yakOutboundReport/" + ids,
42
+    method: "delete"
43
+  })
44
+}
45
+
46
+/** 可选年份(当前年起向前 5 年) */
47
+export function listYakOutboundReportYearOptions() {
48
+  return request({
49
+    url: "/dataModel/yakOutboundReport/yearOptions",
50
+    method: "get"
51
+  })
52
+}
53
+
54
+/** 乡镇下拉 */
55
+export function listYakOutboundReportTownOptions() {
56
+  return request({
57
+    url: "/dataModel/yakOutboundReport/townOptions",
58
+    method: "get"
59
+  })
60
+}
61
+
62
+/** Excel 导入(multipart/form-data,file 为 el-upload 的 raw File) */
63
+export function importYakOutboundReport(file, statYear) {
64
+  const formData = new FormData()
65
+  formData.append("file", file, file.name)
66
+  formData.append("statYear", String(statYear))
67
+  return request({
68
+    url: "/dataModel/yakOutboundReport/importData",
69
+    method: "post",
70
+    headers: {
71
+      repeatSubmit: false
72
+    },
73
+    data: formData
74
+  })
75
+}
76
+
77
+/** 下载导入模板(GET) */
78
+export function downloadYakOutboundReportTemplate() {
79
+  return request({
80
+    url: "/dataModel/yakOutboundReport/importTemplate",
81
+    method: "get",
82
+    responseType: "blob"
83
+  })
84
+}
85
+
86
+/** 按年份导出(GET) */
87
+export function exportYakOutboundReport(statYear) {
88
+  return request({
89
+    url: "/dataModel/yakOutboundReport/export",
90
+    method: "get",
91
+    params: { statYear },
92
+    responseType: "blob"
93
+  })
94
+}

+ 59 - 0
ruoyi-ui/src/lang/bo/dataModel.js

@@ -335,5 +335,64 @@ export default {
335 335
     viewCreateBy: "创建人",
336 336
     viewUpdateBy: "修改人",
337 337
     viewUpdateTime: "修改时间"
338
+  },
339
+  yakOutboundReport: {
340
+    queryStatYear: "所属年份",
341
+    queryTown: "所属乡镇",
342
+    colStatYear: "所属年份",
343
+    colTownName: "所属乡镇",
344
+    colFarmerHouseholdCount: "农牧户户数",
345
+    colFarmerPopulationCount: "农牧户人数",
346
+    colYakOutboundCount: "牦牛出栏数",
347
+    colSelfConsumptionCattle: "自食情况(牛)",
348
+    formStatYear: "所属年份",
349
+    formTown: "所属乡镇",
350
+    formFarmerHouseholdCount: "农牧户户数",
351
+    formFarmerPopulationCount: "农牧户人数",
352
+    formYakOutboundCount: "牦牛出栏数",
353
+    formCattleOutboundCount: "黄牛/奶牛/犏牛出栏数",
354
+    formSheepOutboundCount: "绵羊出栏数",
355
+    formGoatOutboundCount: "山羊出栏数",
356
+    formHorseMuleOutboundCount: "马骡出栏数",
357
+    formDonkeyOutboundCount: "驴出栏数",
358
+    formSelfConsumptionCattleCount: "自食牛头数",
359
+    formSelfConsumptionSheepCount: "自食绵羊只数",
360
+    formSelfConsumptionGoatCount: "自食山羊只数",
361
+    formRemark: "备注",
362
+    phRemark: "选填,不超过 500 字",
363
+    sectionBasic: "基本信息",
364
+    sectionFarmer: "农牧户情况",
365
+    sectionOutbound: "出栏情况",
366
+    sectionSelfConsumption: "自食情况",
367
+    btnImport: "导入",
368
+    btnDownloadTemplate: "模板下载",
369
+    exportTitle: "Excel 导出",
370
+    exportStatYear: "所属年份",
371
+    exportStatYearPh: "请选择所属年份",
372
+    exportStatYearRequired: "请选择所属年份",
373
+    exportStatYearInvalid: "年份不在可选范围",
374
+    exportLoading: "正在导出...",
375
+    exportFail: "导出失败",
376
+    importTitle: "Excel 导入",
377
+    importStatYear: "填报年份",
378
+    importStatYearPh: "请选择年份",
379
+    importTip: "请先选择填报年份,再上传 .xlsx 文件",
380
+    importLoading: "正在导入...",
381
+    importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
382
+    importWarnTitle: "导入警告",
383
+    importFailTitle: "失败明细",
384
+    dialogAdd: "[新增]牦牛出栏",
385
+    dialogEdit: "[编辑]牦牛出栏",
386
+    confirmDelete: "是否删除?",
387
+    emptyList: "暂无数据",
388
+    unitHousehold: "户",
389
+    unitPerson: "人",
390
+    unitHead: "头",
391
+    unitSheep: "只",
392
+    unitHorse: "匹",
393
+    ruleStatYearRequired: "请选择年份",
394
+    ruleTownRequired: "请选择乡镇",
395
+    ruleCountRequired: "请填写",
396
+    ruleCountNonNegative: "≥0 整数"
338 397
   }
339 398
 }

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

@@ -3,7 +3,8 @@ export default {
3 3
   common: {
4 4
     unitHead: "ཀོང་།",
5 5
     unitGe: "ཚན་པ།",
6
-    unitMu: "ཞིང་།"
6
+    unitMu: "ཞིང་།",
7
+    unitHousehold: "ཁྱིམ་ཚང"
7 8
   },
8 9
   status: {
9 10
     dash: "—"
@@ -19,7 +20,7 @@ export default {
19 20
     kpiInventoryTotal: "གཡག་གནས་ཁུངས་སྤྱི་ཁྱབ།",
20 21
     kpiAnnualOutbound: "གཡག་ལོ་རེའི་ཕྱིར་འདོན།",
21 22
     kpiWarningYakCount: "ཉེན་བརྡ་གཡག་གྲངས།",
22
-    kpiPastureCount: "རanch་གྲངས།",
23
+    kpiPastureCount: "སོ་ཞིང་ཁྱིམ་མི",
23 24
     kpiGrasslandCount: "རྩྭ་ཁང་གྲངས།",
24 25
     kpiGrasslandAreaMu: "རྩྭ་ཁང་ཞིང་ཚད།",
25 26
     bullCount: "ཕོ་གཡག་གྲངས།",

+ 59 - 0
ruoyi-ui/src/lang/zh/dataModel.js

@@ -335,5 +335,64 @@ export default {
335 335
     viewCreateBy: "创建人",
336 336
     viewUpdateBy: "修改人",
337 337
     viewUpdateTime: "修改时间"
338
+  },
339
+  yakOutboundReport: {
340
+    queryStatYear: "所属年份",
341
+    queryTown: "所属乡镇",
342
+    colStatYear: "所属年份",
343
+    colTownName: "所属乡镇",
344
+    colFarmerHouseholdCount: "农牧户户数",
345
+    colFarmerPopulationCount: "农牧户人数",
346
+    colYakOutboundCount: "牦牛出栏数",
347
+    colSelfConsumptionCattle: "自食情况(牛)",
348
+    formStatYear: "所属年份",
349
+    formTown: "所属乡镇",
350
+    formFarmerHouseholdCount: "农牧户户数",
351
+    formFarmerPopulationCount: "农牧户人数",
352
+    formYakOutboundCount: "牦牛出栏数",
353
+    formCattleOutboundCount: "黄牛/奶牛/犏牛出栏数",
354
+    formSheepOutboundCount: "绵羊出栏数",
355
+    formGoatOutboundCount: "山羊出栏数",
356
+    formHorseMuleOutboundCount: "马骡出栏数",
357
+    formDonkeyOutboundCount: "驴出栏数",
358
+    formSelfConsumptionCattleCount: "自食牛头数",
359
+    formSelfConsumptionSheepCount: "自食绵羊只数",
360
+    formSelfConsumptionGoatCount: "自食山羊只数",
361
+    formRemark: "备注",
362
+    phRemark: "选填,不超过 500 字",
363
+    sectionBasic: "基本信息",
364
+    sectionFarmer: "农牧户情况",
365
+    sectionOutbound: "出栏情况",
366
+    sectionSelfConsumption: "自食情况",
367
+    btnImport: "导入",
368
+    btnDownloadTemplate: "下载模板",
369
+    exportTitle: "Excel 导出",
370
+    exportStatYear: "所属年份",
371
+    exportStatYearPh: "请选择所属年份",
372
+    exportStatYearRequired: "请选择所属年份",
373
+    exportStatYearInvalid: "导出年份不在可选范围内",
374
+    exportLoading: "正在导出,请稍候...",
375
+    exportFail: "导出失败",
376
+    importTitle: "Excel 导入",
377
+    importStatYear: "填报年份",
378
+    importStatYearPh: "请选择填报年份",
379
+    importTip: "请先选择填报年份,再上传与模板格式一致的 .xlsx 文件",
380
+    importLoading: "正在导入,请稍候...",
381
+    importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
382
+    importWarnTitle: "导入警告",
383
+    importFailTitle: "导入失败明细",
384
+    dialogAdd: "[新增]牦牛出栏数据",
385
+    dialogEdit: "[编辑]牦牛出栏数据",
386
+    confirmDelete: "是否确认删除这条数据?",
387
+    emptyList: "暂无填报数据",
388
+    unitHousehold: "户",
389
+    unitPerson: "人",
390
+    unitHead: "头",
391
+    unitSheep: "只",
392
+    unitHorse: "匹",
393
+    ruleStatYearRequired: "请选择所属年份",
394
+    ruleTownRequired: "请选择所属乡镇",
395
+    ruleCountRequired: "请填写该项",
396
+    ruleCountNonNegative: "须为大于等于 0 的整数"
338 397
   }
339 398
 }

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

@@ -3,7 +3,8 @@ export default {
3 3
   common: {
4 4
     unitHead: "头",
5 5
     unitGe: "个",
6
-    unitMu: "亩"
6
+    unitMu: "亩",
7
+    unitHousehold: "户"
7 8
   },
8 9
   status: {
9 10
     dash: "—"
@@ -19,7 +20,7 @@ export default {
19 20
     kpiInventoryTotal: "牦牛存栏总量",
20 21
     kpiAnnualOutbound: "牦牛年出栏量",
21 22
     kpiWarningYakCount: "预警牦牛数量",
22
-    kpiPastureCount: "牧场数量",
23
+    kpiPastureCount: "农牧户",
23 24
     kpiGrasslandCount: "草场数量",
24 25
     kpiGrasslandAreaMu: "草场面积",
25 26
     bullCount: "公牛数量",

+ 708 - 0
ruoyi-ui/src/views/dataModel/yakOutboundReport/index.vue

@@ -0,0 +1,708 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-card shadow="never">
4
+      <el-form ref="queryForm" :model="queryParams" size="small" :inline="true">
5
+        <el-form-item prop="statYear">
6
+          <template slot="label">{{ dmT("queryStatYear") }}</template>
7
+          <el-select v-model="queryParams.statYear" :placeholder="dmCommon('pleaseSelect')" clearable style="width: 120px">
8
+            <el-option v-for="y in yearOptions" :key="y" :label="String(y)" :value="y" />
9
+          </el-select>
10
+        </el-form-item>
11
+        <el-form-item prop="townDeptId">
12
+          <template slot="label">{{ dmT("queryTown") }}</template>
13
+          <el-select v-model="queryParams.townDeptId" :placeholder="dmCommon('pleaseSelect')" clearable filterable style="width: 180px">
14
+            <el-option v-for="item in townOptions" :key="item.deptId" :label="item.deptName" :value="item.deptId" />
15
+          </el-select>
16
+        </el-form-item>
17
+        <el-form-item>
18
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">{{ dmCommon("search") }}</el-button>
19
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">{{ dmCommon("reset") }}</el-button>
20
+          <el-button
21
+            v-hasPermi="['dataModel:yakOutboundReport:add']"
22
+            type="primary"
23
+            plain
24
+            icon="el-icon-plus"
25
+            size="mini"
26
+            @click="handleAdd"
27
+          >{{ dmCommon("add") }}</el-button>
28
+          <el-button
29
+            v-hasPermi="['dataModel:yakOutboundReport:import']"
30
+            type="info"
31
+            plain
32
+            icon="el-icon-upload2"
33
+            size="mini"
34
+            @click="handleImportOpen"
35
+          >{{ dmT("btnImport") }}</el-button>
36
+          <el-button
37
+            v-hasPermi="['dataModel:yakOutboundReport:export']"
38
+            type="warning"
39
+            plain
40
+            icon="el-icon-download"
41
+            size="mini"
42
+            @click="handleExportOpen"
43
+          >{{ dmCommon("export") }}</el-button>
44
+        </el-form-item>
45
+      </el-form>
46
+    </el-card>
47
+
48
+    <br />
49
+
50
+    <el-card shadow="never">
51
+      <el-table v-loading="loading" :data="tableList" border>
52
+        <template slot="empty">
53
+          <span>{{ dmT("emptyList") }}</span>
54
+        </template>
55
+        <el-table-column :label="dmT('colStatYear')" prop="statYear" align="center" width="100" />
56
+        <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">
58
+          <template slot-scope="scope">{{ formatCount(scope.row.farmerHouseholdCount) }}</template>
59
+        </el-table-column>
60
+        <el-table-column :label="dmT('colFarmerPopulationCount')" align="center" width="100">
61
+          <template slot-scope="scope">{{ formatCount(scope.row.farmerPopulationCount) }}</template>
62
+        </el-table-column>
63
+        <el-table-column :label="dmT('colYakOutboundCount')" align="center" width="110">
64
+          <template slot-scope="scope">{{ formatCount(scope.row.yakOutboundCount) }}</template>
65
+        </el-table-column>
66
+        <el-table-column :label="dmT('colSelfConsumptionCattle')" align="center" width="120">
67
+          <template slot-scope="scope">{{ formatCount(scope.row.selfConsumptionCattleCount) }}</template>
68
+        </el-table-column>
69
+        <el-table-column :label="dmCommon('colOp')" align="center" class-name="small-padding fixed-width" width="180" fixed="right">
70
+          <template slot-scope="scope">
71
+            <el-button v-hasPermi="['dataModel:yakOutboundReport:query']" type="text" size="mini" @click="handleView(scope.row)">{{ dmCommon("view") }}</el-button>
72
+            <el-button v-hasPermi="['dataModel:yakOutboundReport:edit']" type="text" size="mini" @click="handleEdit(scope.row)">{{ dmCommon("edit") }}</el-button>
73
+            <el-button v-hasPermi="['dataModel:yakOutboundReport:remove']" type="text" size="mini" @click="handleDelete(scope.row)">{{ dmCommon("delete") }}</el-button>
74
+          </template>
75
+        </el-table-column>
76
+      </el-table>
77
+      <pagination
78
+        v-show="total > 0"
79
+        :total="total"
80
+        :page.sync="queryParams.pageNum"
81
+        :limit.sync="queryParams.pageSize"
82
+        @pagination="getList"
83
+      />
84
+    </el-card>
85
+
86
+    <el-dialog :title="dialogTitle" :visible.sync="open" width="920px" append-to-body @close="cancel">
87
+      <el-form :key="formKey" ref="form" :model="form" label-width="168px" size="small">
88
+        <div class="yor-section-title">{{ dmT("sectionBasic") }}</div>
89
+        <el-row :gutter="16">
90
+          <el-col :span="12">
91
+            <el-form-item prop="statYear" :rules="formRules.statYear">
92
+              <template slot="label">{{ dmT("formStatYear") }}</template>
93
+              <el-select v-model="form.statYear" :placeholder="dmCommon('pleaseSelect')" style="width: 100%">
94
+                <el-option v-for="y in yearOptions" :key="'f-' + y" :label="String(y)" :value="y" />
95
+              </el-select>
96
+            </el-form-item>
97
+          </el-col>
98
+          <el-col :span="12">
99
+            <el-form-item prop="townDeptId" :rules="formRules.townDeptId">
100
+              <template slot="label">{{ dmT("formTown") }}</template>
101
+              <el-select v-model="form.townDeptId" :placeholder="dmCommon('pleaseSelect')" filterable style="width: 100%">
102
+                <el-option v-for="item in townOptions" :key="'ft-' + item.deptId" :label="item.deptName" :value="item.deptId" />
103
+              </el-select>
104
+            </el-form-item>
105
+          </el-col>
106
+        </el-row>
107
+        <div class="yor-section-title">{{ dmT("sectionFarmer") }}</div>
108
+        <el-row :gutter="16">
109
+          <el-col v-for="field in farmerCountFields" :key="field.prop" :span="12">
110
+            <el-form-item :prop="field.prop" :rules="formRules.count">
111
+              <template slot="label">{{ dmT(field.labelKey) }}</template>
112
+              <div class="dm-input-with-unit">
113
+                <el-input-number
114
+                  v-model="form[field.prop]"
115
+                  :min="0"
116
+                  :precision="0"
117
+                  :controls="true"
118
+                  controls-position="right"
119
+                  class="dm-input-with-unit__input"
120
+                />
121
+                <span class="dm-input-with-unit__suffix">{{ dmT(field.unitKey) }}</span>
122
+              </div>
123
+            </el-form-item>
124
+          </el-col>
125
+        </el-row>
126
+        <div class="yor-section-title">{{ dmT("sectionOutbound") }}</div>
127
+        <el-row :gutter="16">
128
+          <el-col v-for="field in outboundCountFields" :key="field.prop" :span="12">
129
+            <el-form-item :prop="field.prop" :rules="formRules.count">
130
+              <template slot="label">{{ dmT(field.labelKey) }}</template>
131
+              <div class="dm-input-with-unit">
132
+                <el-input-number
133
+                  v-model="form[field.prop]"
134
+                  :min="0"
135
+                  :precision="0"
136
+                  :controls="true"
137
+                  controls-position="right"
138
+                  class="dm-input-with-unit__input"
139
+                />
140
+                <span class="dm-input-with-unit__suffix">{{ dmT(field.unitKey) }}</span>
141
+              </div>
142
+            </el-form-item>
143
+          </el-col>
144
+        </el-row>
145
+        <div class="yor-section-title">{{ dmT("sectionSelfConsumption") }}</div>
146
+        <el-row :gutter="16">
147
+          <el-col v-for="field in selfConsumptionCountFields" :key="field.prop" :span="12">
148
+            <el-form-item :prop="field.prop" :rules="formRules.count">
149
+              <template slot="label">{{ dmT(field.labelKey) }}</template>
150
+              <div class="dm-input-with-unit">
151
+                <el-input-number
152
+                  v-model="form[field.prop]"
153
+                  :min="0"
154
+                  :precision="0"
155
+                  :controls="true"
156
+                  controls-position="right"
157
+                  class="dm-input-with-unit__input"
158
+                />
159
+                <span class="dm-input-with-unit__suffix">{{ dmT(field.unitKey) }}</span>
160
+              </div>
161
+            </el-form-item>
162
+          </el-col>
163
+        </el-row>
164
+        <el-form-item prop="remark" :rules="formRules.remark">
165
+          <template slot="label">{{ dmT("formRemark") }}</template>
166
+          <el-input v-model="form.remark" type="textarea" :rows="2" :placeholder="dmT('phRemark')" maxlength="500" show-word-limit />
167
+        </el-form-item>
168
+      </el-form>
169
+      <div slot="footer" class="dialog-footer">
170
+        <el-button type="primary" @click="submitForm">{{ dmCommon("ok") }}</el-button>
171
+        <el-button @click="cancel">{{ dmCommon("cancel") }}</el-button>
172
+      </div>
173
+    </el-dialog>
174
+
175
+    <el-dialog :title="dmCommon('viewTitle')" :visible.sync="viewOpen" width="720px" append-to-body>
176
+      <el-form label-width="168px" size="small">
177
+        <el-row :gutter="16">
178
+          <el-col :span="12">
179
+            <el-form-item :label="dmT('formStatYear')">
180
+              <span>{{ viewRow.statYear || dash }}</span>
181
+            </el-form-item>
182
+          </el-col>
183
+          <el-col :span="12">
184
+            <el-form-item :label="dmT('formTown')">
185
+              <span>{{ viewRow.townName || dash }}</span>
186
+            </el-form-item>
187
+          </el-col>
188
+          <el-col v-for="field in viewFields" :key="'v-' + field.prop" :span="12">
189
+            <el-form-item :label="dmT(field.labelKey)">
190
+              <span>{{ formatCount(viewRow[field.prop]) }}</span>
191
+            </el-form-item>
192
+          </el-col>
193
+        </el-row>
194
+      </el-form>
195
+      <div slot="footer" class="dialog-footer">
196
+        <el-button
197
+          v-hasPermi="['dataModel:yakOutboundReport:edit']"
198
+          type="primary"
199
+          @click="handleEditFromView"
200
+        >{{ dmCommon("edit") }}</el-button>
201
+        <el-button @click="viewOpen = false">{{ dmCommon("close") }}</el-button>
202
+      </div>
203
+    </el-dialog>
204
+
205
+    <el-dialog :title="dmT('importTitle')" :visible.sync="importOpen" width="480px" append-to-body @close="resetImport">
206
+      <el-form ref="importFormRef" :model="importForm" label-width="100px" size="small">
207
+        <el-form-item prop="statYear" :rules="importRules.statYear">
208
+          <template slot="label">{{ dmT("importStatYear") }}</template>
209
+          <el-select v-model="importForm.statYear" :placeholder="dmT('importStatYearPh')" style="width: 100%">
210
+            <el-option v-for="y in yearOptions" :key="'i-' + y" :label="String(y)" :value="y" />
211
+          </el-select>
212
+        </el-form-item>
213
+        <p class="yor-import-tip">{{ dmT("importTip") }}</p>
214
+        <el-upload
215
+          ref="importUpload"
216
+          drag
217
+          :limit="1"
218
+          accept=".xlsx,.xls"
219
+          :auto-upload="false"
220
+          action="#"
221
+        >
222
+          <i class="el-icon-upload" />
223
+          <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
224
+          <div slot="tip" class="el-upload__tip">
225
+            <el-link type="primary" :underline="false" @click="handleDownloadTemplate">{{ dmT("btnDownloadTemplate") }}</el-link>
226
+          </div>
227
+        </el-upload>
228
+      </el-form>
229
+      <div slot="footer" class="dialog-footer">
230
+        <el-button type="primary" :loading="importing" @click="handleImportSubmit">{{ dmCommon("ok") }}</el-button>
231
+        <el-button @click="importOpen = false">{{ dmCommon("cancel") }}</el-button>
232
+      </div>
233
+    </el-dialog>
234
+
235
+    <el-dialog :title="dmT('exportTitle')" :visible.sync="exportOpen" width="480px" append-to-body @close="resetExport">
236
+      <el-form ref="exportFormRef" :model="exportForm" label-width="100px" size="small">
237
+        <el-form-item prop="statYear" :rules="exportRules.statYear">
238
+          <template slot="label">{{ dmT("exportStatYear") }}</template>
239
+          <el-select v-model="exportForm.statYear" :placeholder="dmT('exportStatYearPh')" style="width: 100%">
240
+            <el-option v-for="y in yearOptions" :key="'e-' + y" :label="String(y)" :value="y" />
241
+          </el-select>
242
+        </el-form-item>
243
+      </el-form>
244
+      <div slot="footer" class="dialog-footer">
245
+        <el-button type="primary" :loading="exporting" @click="handleExportSubmit">{{ dmCommon("ok") }}</el-button>
246
+        <el-button @click="exportOpen = false">{{ dmCommon("cancel") }}</el-button>
247
+      </div>
248
+    </el-dialog>
249
+  </div>
250
+</template>
251
+
252
+<script>
253
+import dataModelLocaleMixin from "@/mixins/dataModelLocaleMixin"
254
+import {
255
+  listYakOutboundReport,
256
+  getYakOutboundReport,
257
+  addYakOutboundReport,
258
+  updateYakOutboundReport,
259
+  delYakOutboundReport,
260
+  listYakOutboundReportYearOptions,
261
+  listYakOutboundReportTownOptions,
262
+  importYakOutboundReport,
263
+  downloadYakOutboundReportTemplate,
264
+  exportYakOutboundReport
265
+} from "@/api/dataModel/yakOutboundReport"
266
+import { blobValidate } from "@/utils/ruoyi"
267
+import { saveAs } from "file-saver"
268
+
269
+const COUNT_FIELDS = [
270
+  { prop: "farmerHouseholdCount", labelKey: "formFarmerHouseholdCount", unitKey: "unitHousehold", section: "farmer" },
271
+  { prop: "farmerPopulationCount", labelKey: "formFarmerPopulationCount", unitKey: "unitPerson", section: "farmer" },
272
+  { prop: "yakOutboundCount", labelKey: "formYakOutboundCount", unitKey: "unitHead", section: "outbound" },
273
+  { prop: "cattleOutboundCount", labelKey: "formCattleOutboundCount", unitKey: "unitHead", section: "outbound" },
274
+  { prop: "sheepOutboundCount", labelKey: "formSheepOutboundCount", unitKey: "unitSheep", section: "outbound" },
275
+  { prop: "goatOutboundCount", labelKey: "formGoatOutboundCount", unitKey: "unitSheep", section: "outbound" },
276
+  { prop: "horseMuleOutboundCount", labelKey: "formHorseMuleOutboundCount", unitKey: "unitHorse", section: "outbound" },
277
+  { prop: "donkeyOutboundCount", labelKey: "formDonkeyOutboundCount", unitKey: "unitHead", section: "outbound" },
278
+  { prop: "selfConsumptionCattleCount", labelKey: "formSelfConsumptionCattleCount", unitKey: "unitHead", section: "selfConsumption" },
279
+  { prop: "selfConsumptionSheepCount", labelKey: "formSelfConsumptionSheepCount", unitKey: "unitSheep", section: "selfConsumption" },
280
+  { prop: "selfConsumptionGoatCount", labelKey: "formSelfConsumptionGoatCount", unitKey: "unitSheep", section: "selfConsumption" }
281
+]
282
+
283
+const VIEW_FIELDS = [
284
+  { prop: "farmerHouseholdCount", labelKey: "formFarmerHouseholdCount" },
285
+  { prop: "farmerPopulationCount", labelKey: "formFarmerPopulationCount" },
286
+  { prop: "yakOutboundCount", labelKey: "formYakOutboundCount" },
287
+  { prop: "selfConsumptionCattleCount", labelKey: "formSelfConsumptionCattleCount" }
288
+]
289
+
290
+function createEmptyForm() {
291
+  const form = {
292
+    id: undefined,
293
+    statYear: undefined,
294
+    townDeptId: undefined,
295
+    remark: undefined
296
+  }
297
+  COUNT_FIELDS.forEach((field) => {
298
+    form[field.prop] = 0
299
+  })
300
+  return form
301
+}
302
+
303
+function normalizeCountFields(data) {
304
+  const row = Object.assign(createEmptyForm(), data || {})
305
+  COUNT_FIELDS.forEach((field) => {
306
+    const val = row[field.prop]
307
+    row[field.prop] = val == null || val === "" ? 0 : val
308
+  })
309
+  return row
310
+}
311
+
312
+export default {
313
+  name: "YakOutboundReport",
314
+  mixins: [dataModelLocaleMixin],
315
+  data() {
316
+    return {
317
+      dmNs: "yakOutboundReport",
318
+      loading: false,
319
+      importing: false,
320
+      exporting: false,
321
+      open: false,
322
+      viewOpen: false,
323
+      importOpen: false,
324
+      exportOpen: false,
325
+      dialogEdit: false,
326
+      formKey: 0,
327
+      total: 0,
328
+      tableList: [],
329
+      yearOptions: [],
330
+      townOptions: [],
331
+      viewRow: {},
332
+      importForm: {
333
+        statYear: undefined
334
+      },
335
+      exportForm: {
336
+        statYear: undefined
337
+      },
338
+      queryParams: {
339
+        pageNum: 1,
340
+        pageSize: 20,
341
+        statYear: undefined,
342
+        townDeptId: undefined
343
+      },
344
+      form: createEmptyForm()
345
+    }
346
+  },
347
+  computed: {
348
+    dash() {
349
+      return this.$t("dataModel.status.dash")
350
+    },
351
+    farmerCountFields() {
352
+      return COUNT_FIELDS.filter((field) => field.section === "farmer")
353
+    },
354
+    outboundCountFields() {
355
+      return COUNT_FIELDS.filter((field) => field.section === "outbound")
356
+    },
357
+    selfConsumptionCountFields() {
358
+      return COUNT_FIELDS.filter((field) => field.section === "selfConsumption")
359
+    },
360
+    viewFields() {
361
+      return VIEW_FIELDS
362
+    },
363
+    dialogTitle() {
364
+      return this.dialogEdit ? this.dmT("dialogEdit") : this.dmT("dialogAdd")
365
+    },
366
+    formRules() {
367
+      return {
368
+        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" }
373
+        ],
374
+        remark: [{ max: 500, message: this.dmCommon("ruleLen500"), trigger: "blur" }]
375
+      }
376
+    },
377
+    importRules() {
378
+      return {
379
+        statYear: [{ required: true, message: this.dmT("ruleStatYearRequired"), trigger: "change" }]
380
+      }
381
+    },
382
+    exportRules() {
383
+      return {
384
+        statYear: [{ required: true, message: this.dmT("exportStatYearRequired"), trigger: "change" }]
385
+      }
386
+    }
387
+  },
388
+  created() {
389
+    this.loadOptions()
390
+    this.getList()
391
+  },
392
+  methods: {
393
+    loadOptions() {
394
+      listYakOutboundReportYearOptions().then((res) => {
395
+        this.yearOptions = Array.isArray(res.data) ? res.data : []
396
+      })
397
+      listYakOutboundReportTownOptions().then((res) => {
398
+        this.townOptions = Array.isArray(res.data) ? res.data : []
399
+      })
400
+    },
401
+    formatCount(val) {
402
+      if (val == null || val === "") {
403
+        return this.dash
404
+      }
405
+      return val
406
+    },
407
+    validateRequiredCount(rule, value, callback) {
408
+      if (value == null || value === "") {
409
+        callback(new Error(this.dmT("ruleCountRequired")))
410
+        return
411
+      }
412
+      if (!Number.isInteger(value) || value < 0) {
413
+        callback(new Error(this.dmT("ruleCountNonNegative")))
414
+        return
415
+      }
416
+      callback()
417
+    },
418
+    buildListQuery() {
419
+      const q = {
420
+        pageNum: this.queryParams.pageNum,
421
+        pageSize: this.queryParams.pageSize
422
+      }
423
+      if (this.queryParams.statYear != null) {
424
+        q.statYear = this.queryParams.statYear
425
+      }
426
+      if (this.queryParams.townDeptId != null) {
427
+        q.townDeptId = this.queryParams.townDeptId
428
+      }
429
+      return q
430
+    },
431
+    buildPayload() {
432
+      const remarkText = this.form.remark != null ? String(this.form.remark).trim() : ""
433
+      const payload = {
434
+        statYear: this.form.statYear,
435
+        townDeptId: this.form.townDeptId,
436
+        remark: remarkText !== "" ? remarkText : undefined
437
+      }
438
+      COUNT_FIELDS.forEach((field) => {
439
+        payload[field.prop] = this.form[field.prop]
440
+      })
441
+      return payload
442
+    },
443
+    getList() {
444
+      this.loading = true
445
+      listYakOutboundReport(this.buildListQuery())
446
+        .then((res) => {
447
+          this.tableList = res.rows || []
448
+          this.total = res.total != null ? res.total : 0
449
+        })
450
+        .finally(() => {
451
+          this.loading = false
452
+        })
453
+    },
454
+    handleQuery() {
455
+      this.queryParams.pageNum = 1
456
+      this.getList()
457
+    },
458
+    resetQuery() {
459
+      this.queryParams.statYear = undefined
460
+      this.queryParams.townDeptId = undefined
461
+      this.resetForm("queryForm")
462
+      this.handleQuery()
463
+    },
464
+    handleAdd() {
465
+      this.formKey += 1
466
+      this.form = createEmptyForm()
467
+      this.dialogEdit = false
468
+      this.open = true
469
+      this.$nextTick(() => {
470
+        if (this.$refs.form) {
471
+          this.$refs.form.clearValidate()
472
+        }
473
+      })
474
+    },
475
+    handleEdit(row) {
476
+      getYakOutboundReport(row.id).then((res) => {
477
+        const data = res.data || {}
478
+        this.formKey += 1
479
+        this.dialogEdit = true
480
+        this.form = normalizeCountFields(data)
481
+        this.open = true
482
+        this.viewOpen = false
483
+        this.$nextTick(() => {
484
+          if (this.$refs.form) {
485
+            this.$refs.form.clearValidate()
486
+          }
487
+        })
488
+      })
489
+    },
490
+    handleEditFromView() {
491
+      this.handleEdit(this.viewRow)
492
+    },
493
+    handleView(row) {
494
+      getYakOutboundReport(row.id).then((res) => {
495
+        this.viewRow = res.data || {}
496
+        this.viewOpen = true
497
+      })
498
+    },
499
+    handleDelete(row) {
500
+      this.$modal
501
+        .confirm(this.dmT("confirmDelete"))
502
+        .then(() => delYakOutboundReport(row.id))
503
+        .then(() => {
504
+          this.$modal.msgSuccess(this.dmCommon("msgDelOk"))
505
+          this.viewOpen = false
506
+          this.getList()
507
+        })
508
+        .catch(() => {})
509
+    },
510
+    submitForm() {
511
+      this.$refs.form.validate((valid) => {
512
+        if (!valid) {
513
+          return
514
+        }
515
+        const payload = this.buildPayload()
516
+        if (this.dialogEdit) {
517
+          payload.id = this.form.id
518
+          updateYakOutboundReport(payload).then(() => {
519
+            this.$modal.msgSuccess(this.dmCommon("msgModOk"))
520
+            this.open = false
521
+            this.getList()
522
+          })
523
+        } else {
524
+          addYakOutboundReport(payload).then(() => {
525
+            this.$modal.msgSuccess(this.dmCommon("msgAddOk"))
526
+            this.open = false
527
+            this.getList()
528
+          })
529
+        }
530
+      })
531
+    },
532
+    cancel() {
533
+      this.open = false
534
+      this.form = createEmptyForm()
535
+    },
536
+    handleImportOpen() {
537
+      this.importForm.statYear = this.queryParams.statYear
538
+      this.importOpen = true
539
+      this.$nextTick(() => {
540
+        this.resetImportFiles()
541
+      })
542
+    },
543
+    resetImportFiles() {
544
+      if (this.$refs.importUpload) {
545
+        this.$refs.importUpload.clearFiles()
546
+      }
547
+    },
548
+    resetImport() {
549
+      this.importing = false
550
+      this.importForm.statYear = undefined
551
+      this.resetImportFiles()
552
+    },
553
+    handleDownloadTemplate() {
554
+      const filename = `yak_outbound_report_template_${new Date().getTime()}.xlsx`
555
+      this.$modal.loading("正在下载模板,请稍候")
556
+      downloadYakOutboundReportTemplate()
557
+        .then(async (data) => {
558
+          if (blobValidate(data)) {
559
+            saveAs(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), filename)
560
+          } else {
561
+            const resText = await data.text()
562
+            const rspObj = JSON.parse(resText)
563
+            this.$modal.msgError(rspObj.msg || "下载模板失败")
564
+          }
565
+        })
566
+        .catch(() => {
567
+          this.$modal.msgError("下载模板失败")
568
+        })
569
+        .finally(() => {
570
+          this.$modal.closeLoading()
571
+        })
572
+    },
573
+    handleExportOpen() {
574
+      this.exportForm.statYear = this.queryParams.statYear
575
+      this.exportOpen = true
576
+      this.$nextTick(() => {
577
+        if (this.$refs.exportFormRef) {
578
+          this.$refs.exportFormRef.clearValidate()
579
+        }
580
+      })
581
+    },
582
+    resetExport() {
583
+      this.exporting = false
584
+      this.exportForm.statYear = undefined
585
+    },
586
+    handleExportSubmit() {
587
+      this.$refs.exportFormRef.validate((valid) => {
588
+        if (!valid) {
589
+          return
590
+        }
591
+        const statYear = this.exportForm.statYear
592
+        if (!this.yearOptions.includes(statYear)) {
593
+          this.$modal.msgError(this.dmT("exportStatYearInvalid"))
594
+          return
595
+        }
596
+        const filename = `${statYear}年牦牛出栏数据.xlsx`
597
+        this.exporting = true
598
+        this.$modal.loading(this.dmT("exportLoading"))
599
+        exportYakOutboundReport(statYear)
600
+          .then(async (data) => {
601
+            if (blobValidate(data)) {
602
+              saveAs(new Blob([data], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }), filename)
603
+              this.exportOpen = false
604
+            } else {
605
+              const resText = await data.text()
606
+              const rspObj = JSON.parse(resText)
607
+              this.$modal.msgError(rspObj.msg || this.dmT("exportFail"))
608
+            }
609
+          })
610
+          .catch(() => {
611
+            this.$modal.msgError(this.dmT("exportFail"))
612
+          })
613
+          .finally(() => {
614
+            this.exporting = false
615
+            this.$modal.closeLoading()
616
+          })
617
+      })
618
+    },
619
+    handleImportSubmit() {
620
+      this.$refs.importFormRef.validate((valid) => {
621
+        if (!valid) {
622
+          return
623
+        }
624
+        const upload = this.$refs.importUpload
625
+        const files = upload && upload.uploadFiles
626
+        if (!files || !files.length) {
627
+          this.$modal.msgError("请选择要上传的文件")
628
+          return
629
+        }
630
+        const raw = files[0].raw
631
+        const name = (raw.name || "").toLowerCase()
632
+        if (!name.endsWith(".xlsx") && !name.endsWith(".xls")) {
633
+          this.$modal.msgError('请选择后缀为 "xls" 或 "xlsx" 的文件')
634
+          return
635
+        }
636
+        this.importing = true
637
+        this.$modal.loading(this.dmT("importLoading"))
638
+        importYakOutboundReport(raw, this.importForm.statYear)
639
+          .then((res) => {
640
+            this.importOpen = false
641
+            this.showImportResult(res.data || {})
642
+            this.getList()
643
+          })
644
+          .finally(() => {
645
+            this.importing = false
646
+            this.$modal.closeLoading()
647
+            this.resetImportFiles()
648
+          })
649
+      })
650
+    },
651
+    showImportResult(data) {
652
+      const insert = data.insertCount != null ? data.insertCount : 0
653
+      const update = data.updateCount != null ? data.updateCount : 0
654
+      const fail = data.failCount != null ? data.failCount : 0
655
+      let html = this.dmT("importSummary", { insert, update, fail })
656
+      if (data.warnMessages && data.warnMessages.length) {
657
+        html += `<p><strong>${this.dmT("importWarnTitle")}</strong></p><p>${data.warnMessages.join("<br/>")}</p>`
658
+      }
659
+      if (data.failMessages && data.failMessages.length) {
660
+        html += `<p><strong>${this.dmT("importFailTitle")}</strong></p><p>${data.failMessages.join("<br/>")}</p>`
661
+      }
662
+      this.$alert(
663
+        `<div style='overflow:auto;overflow-x:hidden;max-height:70vh;padding:10px 20px 0;'>${html}</div>`,
664
+        this.dmT("importTitle"),
665
+        { dangerouslyUseHTMLString: true }
666
+      )
667
+    }
668
+  }
669
+}
670
+</script>
671
+
672
+<style scoped>
673
+.yor-section-title {
674
+  margin: 4px 0 12px;
675
+  padding-left: 8px;
676
+  font-size: 14px;
677
+  font-weight: 600;
678
+  color: #303133;
679
+  border-left: 3px solid #409eff;
680
+}
681
+
682
+.yor-import-tip {
683
+  margin: 0 0 12px;
684
+  font-size: 12px;
685
+  color: #909399;
686
+  line-height: 1.6;
687
+}
688
+
689
+.dm-input-with-unit {
690
+  display: flex;
691
+  align-items: center;
692
+  width: 100%;
693
+}
694
+.dm-input-with-unit__input {
695
+  flex: 1;
696
+  width: 0;
697
+}
698
+.dm-input-with-unit__input ::v-deep.el-input-number {
699
+  width: 100%;
700
+}
701
+.dm-input-with-unit__suffix {
702
+  flex-shrink: 0;
703
+  margin-left: 8px;
704
+  color: #606266;
705
+  font-size: 14px;
706
+  white-space: nowrap;
707
+}
708
+</style>

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

@@ -147,10 +147,10 @@ export default {
147 147
   computed: {
148 148
     overviewItems() {
149 149
       return [
150
-        { key: "inventoryTotal", labelKey: "kpiInventoryTotal", unit: "unitHead", int: true, fixedValue: YAK_INVENTORY_TOTAL },
151
-        { key: "annualOutbound", labelKey: "kpiAnnualOutbound", unit: "unitHead", int: true, fixedValue: YAK_ANNUAL_OUTBOUND },
150
+        { key: "inventoryTotal", labelKey: "kpiInventoryTotal", unit: "unitHead", int: true,  },
151
+        { key: "annualOutbound", labelKey: "kpiAnnualOutbound", unit: "unitHead", int: true,  },
152 152
         { key: "warningYakCount", labelKey: "kpiWarningYakCount", unit: "unitHead", int: true },
153
-        { key: "pastureCount", labelKey: "kpiPastureCount", unit: "unitGe", int: true },
153
+        { key: "pastureCount", labelKey: "kpiPastureCount", unit: "unitHousehold", int: true },
154 154
         { key: "grasslandCount", labelKey: "kpiGrasslandCount", unit: "unitGe", int: true },
155 155
         { key: "grasslandAreaMu", labelKey: "kpiGrasslandAreaMu", unit: "unitMu", int: false }
156 156
       ]