xsh_1997 1 mēnesi atpakaļ
vecāks
revīzija
77b7cff580

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

@@ -0,0 +1,104 @@
1
+# 牦牛存栏数据填报 — 前端技术方案
2
+
3
+> 依据:同目录 `牦牛存栏数据填报功能需求.md`、`牦牛存栏数据填报技术方案.md`;布局对齐 `dataModel/grassland/index.vue`(双 `el-card` + 分页表格);字段 **小驼峰**。
4
+
5
+---
6
+
7
+## 1. 页面与路由
8
+
9
+| 项 | 说明 |
10
+| --- | --- |
11
+| **Vue 路径** | `ruoyi-ui/src/views/dataModel/yakHerdInventory/index.vue` |
12
+| **组件名** | `YakHerdInventory` |
13
+| **菜单配置** | 组件路径:`dataModel/yakHerdInventory/index`(「产业数据模型及服务」目录下) |
14
+| **权限前缀** | `dataModel:yakHerdInventory:list\|query\|add\|edit\|remove\|import` |
15
+
16
+---
17
+
18
+## 2. API 模块
19
+
20
+| 项 | 说明 |
21
+| --- | --- |
22
+| **文件** | `ruoyi-ui/src/api/dataModel/yakHerdInventory.js` |
23
+| **Base** | `/dataModel/yakHerdInventory` |
24
+
25
+| 方法 | HTTP | 说明 |
26
+| --- | --- | --- |
27
+| `listYakHerdInventory` | GET | `/list` 分页列表 |
28
+| `getYakHerdInventory` | GET | `/{id}` 详情 |
29
+| `addYakHerdInventory` | POST | `/` 新增 |
30
+| `updateYakHerdInventory` | PUT | `/` 修改 |
31
+| `delYakHerdInventory` | DELETE | `/{ids}` 删除 |
32
+| `listYakHerdInventoryYearOptions` | GET | `/yearOptions` 可选年份 |
33
+| `listYakHerdInventoryTownOptions` | GET | `/townOptions` 乡镇下拉 |
34
+| `importYakHerdInventory` | POST | `/importData`(`multipart/form-data`:`file` + `statYear`) |
35
+
36
+模板下载使用 `downloadYakHerdInventoryTemplate()`(**GET** + `responseType: 'blob'`),勿用若依全局 `this.download()`(固定 **POST**,仅适用于导出类接口)。
37
+
38
+---
39
+
40
+## 3. 页面结构
41
+
42
+1. **筛选区**:所属年份(`yearOptions`)、所属乡镇(`townOptions`);**搜索**、**重置**、**新增**、**导入**。  
43
+2. **列表区**:所属年份、乡镇、村居数、牧户数、存栏数、公牛存栏数、母牛存栏数;空值展示 `—`。  
44
+3. **操作列**:查看、修改、删除。  
45
+4. **新增/编辑弹窗**:年份/乡镇必填;存栏指标必填,`el-input-number`(`:min="0"` `:precision="0"`,非负整数);备注选填。  
46
+5. **查看弹窗**:全字段只读(含预留村居、审计信息);可跳转编辑。  
47
+6. **导入弹窗**:先选「填报年份」→ 上传 `.xlsx` → 展示 insert/update/fail 及 fail/warn 明细。
48
+
49
+**本期前端不实现**:Excel 导出(后端已有 `/export`,需求 §3 标注后续立项)。
50
+
51
+---
52
+
53
+## 4. 业务规则(前端)
54
+
55
+| 规则 | 实现 |
56
+| --- | --- |
57
+| 年份范围 | 由 `/yearOptions` 动态加载(当前年起向前 5 年) |
58
+| 乡镇下拉 | `/townOptions` 返回 `deptId` / `deptName` |
59
+| 列表筛选 | `statYear`、`townDeptId` 精确匹配 |
60
+| 存栏数字 | 必填;须为大于等于 0 的整数 |
61
+| 唯一性 | 同年同乡镇重复由后端拦截,前端展示 `msg` |
62
+| 导入 | `FormData` 提交;成功后 `$alert` 展示摘要与明细 |
63
+| 删除 | `$modal.confirm` 后 `delYakHerdInventory(id)` |
64
+| 预留村居 | 详情展示 `villageDeptId` / `villageName`;表单本期不编辑 |
65
+
66
+---
67
+
68
+## 5. 国际化
69
+
70
+| 项 | 说明 |
71
+| --- | --- |
72
+| **语言包** | `dataModel.yakHerdInventory`、`dataModel.common` |
73
+| **文件** | `lang/zh/dataModel.js`、`lang/bo/dataModel.js` |
74
+| **Mixin** | `dataModelLocaleMixin`,`dmNs = 'yakHerdInventory'`,`dmT` / `dmCommon` |
75
+| **注册** | 已随 `dataModel` 模块在 `lang/zh-CN.js`、`lang/bo.js` 引入 |
76
+
77
+---
78
+
79
+## 6. 联调说明
80
+
81
+1. 部署后端 `BizYakHerdInventoryController` 及 `biz_yak_herd_inventory` 表。  
82
+2. 菜单组件 `dataModel/yakHerdInventory/index`,权限与后端 §5 一致。  
83
+3. 执行 `sql/biz_yak_herd_inventory.sql`(及乡镇 `sys_dept` 数据)。  
84
+4. 验证:年份/乡镇筛选、CRUD、导入模板下载、导入结果反馈、双语切换。  
85
+5. 导入联调:选填报年份 → 上传 `2026年.xlsx` 格式文件;开发环境对照后端日志与 `failMessages`。
86
+
87
+---
88
+
89
+## 7. 文件清单
90
+
91
+| 类型 | 路径 |
92
+| --- | --- |
93
+| 页面 | `ruoyi-ui/src/views/dataModel/yakHerdInventory/index.vue` |
94
+| API | `ruoyi-ui/src/api/dataModel/yakHerdInventory.js` |
95
+| Mixin | `ruoyi-ui/src/mixins/dataModelLocaleMixin.js` |
96
+| i18n | `ruoyi-ui/src/lang/zh/dataModel.js`、`lang/bo/dataModel.js` |
97
+
98
+---
99
+
100
+## 8. 修订记录
101
+
102
+| 版本 | 说明 |
103
+| --- | --- |
104
+| 1.0 | 初版:列表/CRUD/详情/导入/模板下载;汉藏 i18n;对齐功能需求 v1.3 |

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

@@ -0,0 +1,94 @@
1
+import request from "@/utils/request"
2
+
3
+/** 分页列表 */
4
+export function listYakHerdInventory(query) {
5
+  return request({
6
+    url: "/dataModel/yakHerdInventory/list",
7
+    method: "get",
8
+    params: query
9
+  })
10
+}
11
+
12
+/** 详情 */
13
+export function getYakHerdInventory(id) {
14
+  return request({
15
+    url: "/dataModel/yakHerdInventory/" + id,
16
+    method: "get"
17
+  })
18
+}
19
+
20
+/** 新增 */
21
+export function addYakHerdInventory(data) {
22
+  return request({
23
+    url: "/dataModel/yakHerdInventory",
24
+    method: "post",
25
+    data
26
+  })
27
+}
28
+
29
+/** 修改 */
30
+export function updateYakHerdInventory(data) {
31
+  return request({
32
+    url: "/dataModel/yakHerdInventory",
33
+    method: "put",
34
+    data
35
+  })
36
+}
37
+
38
+/** 删除 */
39
+export function delYakHerdInventory(ids) {
40
+  return request({
41
+    url: "/dataModel/yakHerdInventory/" + ids,
42
+    method: "delete"
43
+  })
44
+}
45
+
46
+/** 可选年份(当前年起向前 5 年) */
47
+export function listYakHerdInventoryYearOptions() {
48
+  return request({
49
+    url: "/dataModel/yakHerdInventory/yearOptions",
50
+    method: "get"
51
+  })
52
+}
53
+
54
+/** 乡镇下拉 */
55
+export function listYakHerdInventoryTownOptions() {
56
+  return request({
57
+    url: "/dataModel/yakHerdInventory/townOptions",
58
+    method: "get"
59
+  })
60
+}
61
+
62
+/** Excel 导入(multipart/form-data,file 为 el-upload 的 raw File) */
63
+export function importYakHerdInventory(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/yakHerdInventory/importData",
69
+    method: "post",
70
+    headers: {
71
+      repeatSubmit: false
72
+    },
73
+    data: formData
74
+  })
75
+}
76
+
77
+/** 下载导入模板(GET,后端 @GetMapping("/importTemplate")) */
78
+export function downloadYakHerdInventoryTemplate() {
79
+  return request({
80
+    url: "/dataModel/yakHerdInventory/importTemplate",
81
+    method: "get",
82
+    responseType: "blob"
83
+  })
84
+}
85
+
86
+/** 按年份导出(GET,后端 @GetMapping("/export")) */
87
+export function exportYakHerdInventory(statYear) {
88
+  return request({
89
+    url: "/dataModel/yakHerdInventory/export",
90
+    method: "get",
91
+    params: { statYear },
92
+    responseType: "blob"
93
+  })
94
+}

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

@@ -19,6 +19,7 @@ export default {
19 19
     msgModOk: "བཟོ་བཅོས་ཟིན།",
20 20
     msgAddOk: "གསར་སྣོན་ཟིན།",
21 21
     ruleLen100: "ཡིག་ཆ་100ལས་མི་ཆོག",
22
+    ruleLen500: "ཡིག་ཆ་500ལས་མི་ཆོག",
22 23
     syncLoading: "གཞི་གྲངས་མཉམ་སྒྲིག་བཞིན... སྒུག་རོགས།"
23 24
   },
24 25
   status: {
@@ -272,5 +273,67 @@ export default {
272 273
     dateRangeInvalid: "འགོ་ཚེས་མཇུག་ཚེས་ལས་མི་ཆུག",
273 274
     actionViewAsset: "གཡག་ཡིག་ཆ་ལྟ།",
274 275
     emptyList: "ཉེན་བརྡ་མེད།"
276
+  },
277
+  yakHerdInventory: {
278
+    queryStatYear: "ལོ་ཚད།",
279
+    queryTown: "ཤང་གྲོང་།",
280
+    colStatYear: "ལོ་ཚད།",
281
+    colTownName: "ཤང་གྲོང་།",
282
+    colVillageCount: "གྲོང་ཚོ་གྲངས།",
283
+    colHerdsmanHouseholdCount: "འབྲོག་པ་ཁྱིམ་གྲངས།",
284
+    colYakTotal: "གནས་ཚད།",
285
+    colBullTotal: "ཕོ་བ་གནས་ཚད།",
286
+    colCowTotal: "མོ་བ་གནས་ཚད།",
287
+    formStatYear: "ལོ་ཚད།",
288
+    formTown: "ཤང་གྲོང་།",
289
+    formVillageCount: "གྲོང་ཚོ་གྲངས།",
290
+    formHerdsmanHouseholdCount: "འབྲོག་པ་ཁྱིམ་གྲངས།",
291
+    formYakTotal: "གཡག་གནས་ཚད།",
292
+    formBullTotal: "ཕོ་བ་གནས་ཚད།",
293
+    formCowTotal: "མོ་བ་གནས་ཚད།",
294
+    formBreedingBullCount: "རྒྱུད་ཕོ་བ།",
295
+    formBullCastratedCount: "ཕོ/སྤྱི་བ།",
296
+    formCowUnder1Count: "མོ 1 ལོ་མན།",
297
+    formCow13Count: "མོ 1-3 ལོ།",
298
+    formCow312Count: "མོ 3-12 ལོ།",
299
+    formCowOver12Count: "མོ 12 ལོ་ཡན།",
300
+    formRemark: "ཟིན་བྲིས།",
301
+    phRemark: "འདེམས་ཆོག ཡིག་ཆ་500ལས་མི་ཆོག",
302
+    formVillageDeptId: "གྲོང་ཚོ ID(预留)",
303
+    formVillageName: "གྲོང་ཚོ(预留)",
304
+    sectionBasic: "གཞི་རྩའི་ཆ་འཕྲིན།",
305
+    sectionInventory: "གནས་ཚད་གྲངས་ཀ།",
306
+    sectionReserved: "预留",
307
+    btnImport: "ནང་འདྲེན།",
308
+    btnDownloadTemplate: "模板下载",
309
+    exportTitle: "Excel 导出",
310
+    exportStatYear: "所属年份",
311
+    exportStatYearPh: "请选择所属年份",
312
+    exportStatYearRequired: "请选择所属年份",
313
+    exportStatYearInvalid: "年份不在可选范围",
314
+    exportLoading: "正在导出...",
315
+    exportFail: "导出失败",
316
+    importTitle: "Excel 导入",
317
+    importStatYear: "填报年份",
318
+    importStatYearPh: "请选择年份",
319
+    importTip: "先选年份再上传 .xlsx",
320
+    importLoading: "正在导入,请稍候...",
321
+    importSummary: "导入:新增 {insert} 更新 {update} 失败 {fail}",
322
+    importWarnTitle: "警告",
323
+    importFailTitle: "失败明细",
324
+    dialogAdd: "[新增]牦牛存栏",
325
+    dialogEdit: "[编辑]牦牛存栏",
326
+    confirmDelete: "是否删除?",
327
+    emptyList: "暂无数据",
328
+    unitGe: "个",
329
+    unitHousehold: "户",
330
+    unitHead: "头",
331
+    ruleStatYearRequired: "请选择年份",
332
+    ruleTownRequired: "请选择乡镇",
333
+    ruleCountRequired: "请填写",
334
+    ruleCountNonNegative: "≥0 整数",
335
+    viewCreateBy: "创建人",
336
+    viewUpdateBy: "修改人",
337
+    viewUpdateTime: "修改时间"
275 338
   }
276 339
 }

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

@@ -19,6 +19,7 @@ export default {
19 19
     msgModOk: "修改成功",
20 20
     msgAddOk: "新增成功",
21 21
     ruleLen100: "长度不能超过 100 个字符",
22
+    ruleLen500: "长度不能超过 500 个字符",
22 23
     syncLoading: "正在同步数据,请稍候..."
23 24
   },
24 25
   status: {
@@ -272,5 +273,67 @@ export default {
272 273
     dateRangeInvalid: "开始日期不能晚于结束日期",
273 274
     actionViewAsset: "查看牦牛档案",
274 275
     emptyList: "暂无预警数据"
276
+  },
277
+  yakHerdInventory: {
278
+    queryStatYear: "所属年份",
279
+    queryTown: "所属乡镇",
280
+    colStatYear: "所属年份",
281
+    colTownName: "乡镇",
282
+    colVillageCount: "村居数",
283
+    colHerdsmanHouseholdCount: "牧户数",
284
+    colYakTotal: "存栏数",
285
+    colBullTotal: "公牛存栏数",
286
+    colCowTotal: "母牛存栏数",
287
+    formStatYear: "所属年份",
288
+    formTown: "所属乡镇",
289
+    formVillageCount: "村居数",
290
+    formHerdsmanHouseholdCount: "牧户数",
291
+    formYakTotal: "牦牛存栏数",
292
+    formBullTotal: "公牛存栏数",
293
+    formCowTotal: "母牛存栏数",
294
+    formBreedingBullCount: "种公牛存栏数",
295
+    formBullCastratedCount: "公牛/阉畜存栏数",
296
+    formCowUnder1Count: "1岁以下母牛存栏数",
297
+    formCow13Count: "1~3岁母牛存栏数",
298
+    formCow312Count: "3~12岁母牛存栏数",
299
+    formCowOver12Count: "12岁以上母牛存栏数",
300
+    formRemark: "备注",
301
+    phRemark: "选填,不超过 500 字",
302
+    formVillageDeptId: "所属村居ID(预留)",
303
+    formVillageName: "所属村居(预留)",
304
+    sectionBasic: "基本信息",
305
+    sectionInventory: "存栏指标",
306
+    sectionReserved: "预留字段",
307
+    btnImport: "导入",
308
+    btnDownloadTemplate: "下载模板",
309
+    exportTitle: "Excel 导出",
310
+    exportStatYear: "所属年份",
311
+    exportStatYearPh: "请选择所属年份",
312
+    exportStatYearRequired: "请选择所属年份",
313
+    exportStatYearInvalid: "导出年份不在可选范围内",
314
+    exportLoading: "正在导出,请稍候...",
315
+    exportFail: "导出失败",
316
+    importTitle: "Excel 导入",
317
+    importStatYear: "填报年份",
318
+    importStatYearPh: "请选择填报年份",
319
+    importTip: "请先选择填报年份,再上传与模板格式一致的 .xlsx 文件",
320
+    importLoading: "正在导入,请稍候...",
321
+    importSummary: "导入完成:新增 {insert} 条,更新 {update} 条,失败 {fail} 条",
322
+    importWarnTitle: "导入警告",
323
+    importFailTitle: "导入失败明细",
324
+    dialogAdd: "[新增]牦牛存栏数据",
325
+    dialogEdit: "[编辑]牦牛存栏数据",
326
+    confirmDelete: "是否确认删除这条数据?",
327
+    emptyList: "暂无填报数据",
328
+    unitGe: "个",
329
+    unitHousehold: "户",
330
+    unitHead: "头",
331
+    ruleStatYearRequired: "请选择所属年份",
332
+    ruleTownRequired: "请选择所属乡镇",
333
+    ruleCountRequired: "请填写该项",
334
+    ruleCountNonNegative: "须为大于等于 0 的整数",
335
+    viewCreateBy: "创建人",
336
+    viewUpdateBy: "修改人",
337
+    viewUpdateTime: "修改时间"
275 338
   }
276 339
 }

+ 7 - 0
ruoyi-ui/src/utils/request.js

@@ -31,6 +31,10 @@ service.interceptors.request.use(config => {
31 31
   if (getToken() && !isToken) {
32 32
     config.headers['Authorization'] = 'Bearer ' + getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
33 33
   }
34
+  // FormData 上传须去掉 JSON Content-Type,由浏览器自动带 multipart boundary
35
+  if (config.data instanceof FormData) {
36
+    delete config.headers['Content-Type']
37
+  }
34 38
   // get请求映射params参数
35 39
   if (config.method === 'get' && config.params) {
36 40
     let url = config.url + '?' + tansParams(config.params)
@@ -39,6 +43,9 @@ service.interceptors.request.use(config => {
39 43
     config.url = url
40 44
   }
41 45
   if (!isRepeatSubmit && (config.method === 'post' || config.method === 'put')) {
46
+    if (config.data instanceof FormData) {
47
+      return config
48
+    }
42 49
     const requestObj = {
43 50
       url: config.url,
44 51
       data: typeof config.data === 'object' ? JSON.stringify(config.data) : config.data,

+ 687 - 0
ruoyi-ui/src/views/dataModel/yakHerdInventory/index.vue

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