Selaa lähdekoodia

科技服务管理

xsh_1997 1 viikko sitten
vanhempi
commit
4ee16c77eb

+ 94 - 0
doc/牧业养殖科技服务/科技服务人员管理/科技服务人员管理前端技术方案.md

@@ -0,0 +1,94 @@
1
+# 科技服务人员管理 — 前端技术方案
2
+
3
+> 依据:同目录功能需求、技术方案;模式对齐「风险因子设置」:全局单套 GET + PUT;字段 **小驼峰**。
4
+
5
+---
6
+
7
+## 1. 页面与路由
8
+
9
+| 项 | 说明 |
10
+| --- | --- |
11
+| **Vue 路径** | `ruoyi-ui/src/views/techService/techServicePersonnel/index.vue` |
12
+| **组件名** | `TechServicePersonnel` |
13
+| **菜单配置** | 组件路径:`techService/techServicePersonnel/index`(「牧业养殖科技服务」目录下) |
14
+| **权限** | `techService:techServicePersonnel:query\|edit` |
15
+
16
+本期 **无** 列表分页、删除、导入、导出。
17
+
18
+---
19
+
20
+## 2. API 模块
21
+
22
+| 项 | 说明 |
23
+| --- | --- |
24
+| **文件** | `ruoyi-ui/src/api/techService/techServicePersonnel.js` |
25
+| **Base** | `/techService/techServicePersonnel` |
26
+
27
+| 方法 | 路径 | 说明 |
28
+| --- | --- | --- |
29
+| `getTechServicePersonnel` | GET `/` | 获取当前单例填报;无记录时 `data` 为 `null` |
30
+| `updateTechServicePersonnel` | PUT `/` | 保存可填人数;后端 upsert 并重算总人数 |
31
+
32
+---
33
+
34
+## 3. 页面结构
35
+
36
+1. **单页卡片**:进入即加载配置。  
37
+2. **科技特派员**:男性、女性、党员(选填);总人数只读预览(= 男 + 女)。  
38
+3. **农业事业单位**:初级、员级、中级、副高、管理级(选填);总人数只读预览(五级合计)。  
39
+4. **保存**:`edit` 权限;成功后重新拉取。  
40
+5. **审计**:有 `updateTime` 时展示更新人、更新时间。
41
+
42
+### 字段映射
43
+
44
+| 界面 | 字段 |
45
+| --- | --- |
46
+| 特派员男/女/党员 | `envoyMaleCount` / `envoyFemaleCount` / `envoyPartyMemberCount` |
47
+| 特派员总人数(派生) | `envoyTotalCount`(仅展示,不提交) |
48
+| 初级~管理级 | `agriJuniorCount` … `agriManagementCount` |
49
+| 事业总人数(派生) | `agriTotalCount`(仅展示,不提交) |
50
+
51
+---
52
+
53
+## 4. 业务规则
54
+
55
+| 规则 | 实现 |
56
+| --- | --- |
57
+| 单例 | 无列表;进入页 GET 一次 |
58
+| 人数 | 均可空;非负整数;单位「人」 |
59
+| 特派员总人数预览 | 男、女皆空 →「—」;否则二者之和(党员不计入) |
60
+| 事业总人数预览 | 五级皆空 →「—」;否则有值项之和 |
61
+| 提交 | 仅可填字段;总人数由后端重算落库 |
62
+
63
+---
64
+
65
+## 5. 国际化
66
+
67
+`techService.techServicePersonnel`(`lang/zh|bo/techService.js`);页面 `tsNs = 'techServicePersonnel'`,复用 `techServiceLocaleMixin`(`tsT` / `tsCommon`)。
68
+
69
+---
70
+
71
+## 6. 联调说明
72
+
73
+1. 执行 `sql/biz_tech_service_personnel.sql`。  
74
+2. 菜单组件 `techService/techServicePersonnel/index`,权限见 §1。  
75
+3. 验证:空库进入表单为空、保存后总人数正确、再次进入回填、党员不计入特派员总人数。
76
+
77
+---
78
+
79
+## 7. 文件清单
80
+
81
+| 类型 | 路径 |
82
+| --- | --- |
83
+| 页面 | `ruoyi-ui/src/views/techService/techServicePersonnel/index.vue` |
84
+| API | `ruoyi-ui/src/api/techService/techServicePersonnel.js` |
85
+| i18n | `lang/zh/techService.js`、`lang/bo/techService.js` → `techServicePersonnel` |
86
+| 前端技术方案 | 本文档 |
87
+
88
+---
89
+
90
+## 8. 修订记录
91
+
92
+| 版本 | 说明 |
93
+| --- | --- |
94
+| 1.0 | 初版:单例 GET+PUT;两分区数值填报与总人数只读预览 |

+ 11 - 1
doc/牧业养殖科技服务/科技服务人员管理/科技服务人员管理技术方案.md

@@ -122,8 +122,18 @@ DDL:`sql/biz_tech_service_personnel.sql`
122 122
 
123 123
 ---
124 124
 
125
-## 6. 修订记录
125
+## 6. 前端
126
+
127
+详见同目录 **[科技服务人员管理前端技术方案.md](./科技服务人员管理前端技术方案.md)**。
128
+
129
+- 页面:`ruoyi-ui/src/views/techService/techServicePersonnel/index.vue`
130
+- API:`ruoyi-ui/src/api/techService/techServicePersonnel.js`
131
+
132
+---
133
+
134
+## 7. 修订记录
126 135
 
127 136
 | 版本 | 说明 |
128 137
 | --- | --- |
129 138
 | 1.0 | 初版 |
139
+| 1.1 | 补充前端技术方案链接 |

+ 7 - 2
doc/牧业疫病诊疗服务/畜牧医疗资源管理/畜牧医疗资源管理前端技术方案.md

@@ -32,6 +32,7 @@
32 32
 | `publishMedicalResource` | POST | `/publish/{id}` |
33 33
 | `offlineMedicalResource` | POST | `/offline/{id}` |
34 34
 | `assignAccountMedicalResource` | POST | `/assignAccount/{id}` |
35
+| `listMedicalResourceTownOptions` | GET | `/townOptions`,兽医「所属乡镇」下拉 |
35 36
 
36 37
 ---
37 38
 
@@ -53,8 +54,11 @@
53 54
 | 列表筛选 `publishStatus` | `0` 未发布 / `1` 已发布 / `2` 已下架(与后端 Query 一致) |
54 55
 | 多选提交 | `consultModesList`、`serviceWeekdaysList` 数组提交,后端编码入库 |
55 56
 | 服务时段 | 两个 `el-time-select`,校验结束 ≥ 开始 |
56
-| 手机号 | `PHONE_REG = /^1[3-9]\d{9}$/` |
57
-| 照片 | jpg/jpeg/png,≤10MB,必填 |
57
+| 手机号 | `PHONE_REG = /^1[3-9]\d{9}$/`;兽医/团队/机构均为**选填**,有值才校验 |
58
+| 照片 | jpg/jpeg/png,≤10MB,**选填** |
59
+| 兽医所属乡镇 | 选填;`townDeptId` 下拉来自 `/townOptions`;详情展示 `townName` |
60
+| 接诊方式 | 兽医**必填**至少一项 |
61
+| 单日最大接诊人数 | 仅机构;**选填**(新增默认空,不预填 30) |
58 62
 | 分配账号 | 仅兽医/机构 Tab 列表展示「账号分配」列;按钮 `canAssignAccount` |
59 63
 | 资源类型 | 新增默认当前 Tab 类型;编辑不可改 |
60 64
 
@@ -110,3 +114,4 @@
110 114
 | 版本 | 说明 |
111 115
 | --- | --- |
112 116
 | 1.0 | 初版:四 Tab 分库列表、动态表单、发布/下架/分配账号、汉藏 i18n |
117
+| 1.1 | 对齐需求:兽医「所属乡镇」;照片/联系方式选填;接诊方式必填;机构单日接诊人数选填不预填 |

+ 8 - 0
ruoyi-ui/src/api/diseaseTreatment/medicalResource.js

@@ -67,6 +67,14 @@ export function assignAccountMedicalResource(id) {
67 67
   })
68 68
 }
69 69
 
70
+/** 填报乡镇下拉(兽医所属乡镇) */
71
+export function listMedicalResourceTownOptions() {
72
+  return request({
73
+    url: "/diseaseTreatment/medicalResource/townOptions",
74
+    method: "get"
75
+  })
76
+}
77
+
70 78
 /** 批量发布 */
71 79
 export function batchPublishMedicalResource(ids) {
72 80
   return request({

+ 18 - 0
ruoyi-ui/src/api/techService/techServicePersonnel.js

@@ -0,0 +1,18 @@
1
+import request from "@/utils/request"
2
+
3
+/** 获取当前科技服务人员填报(单例) */
4
+export function getTechServicePersonnel() {
5
+  return request({
6
+    url: "/techService/techServicePersonnel",
7
+    method: "get"
8
+  })
9
+}
10
+
11
+/** 保存填报(upsert) */
12
+export function updateTechServicePersonnel(data) {
13
+  return request({
14
+    url: "/techService/techServicePersonnel",
15
+    method: "put",
16
+    data
17
+  })
18
+}

+ 3 - 1
ruoyi-ui/src/lang/bo/diseaseTreatment.js

@@ -90,6 +90,7 @@ export default {
90 90
     formIntroduction: "ངོ་སྤྲོད།",
91 91
     formContactPhone: "འབྲེལ་གཏུག",
92 92
     formAffiliatedUnit: "ཁོངས་འབྲེལ།",
93
+    formTown: "ཤང་གྲོང།",
93 94
     formDetailAddress: "ཁ་བྱང་།",
94 95
     formConsultModes: "གཅོད་སྨན་རྣམ་པ།",
95 96
     formServiceArea: "ཞབས་ཞུ་ཁུལ།",
@@ -107,8 +108,9 @@ export default {
107 108
     formEquipmentModel: "སྒྲིག་ཆས་དཔེ་གཞི།",
108 109
     phResourceName: "མིང་འབྲི་རོགས།",
109 110
     phIntroduction: "ངོ་སྤྲོད་འབྲི་རོགས།",
110
-    phContactPhone: "ཁ་པར་འབྲི་རོགས།",
111
+    phContactPhone: "འདེམས་འབྲི,ཁ་པར་འབྲི་རོགས།",
111 112
     phAffiliatedUnit: "ཁོངས་འབྲེལ་འབྲི་རོགས།",
113
+    phTown: "ཤང་གྲོང་འདེམས་རོགས(འདེམས་འབྲི)",
112 114
     phDetailAddress: "ཁ་བྱང་འབྲི་རོགས།",
113 115
     phServiceArea: "ཞབས་ཞུ་ཁུལ་འབྲི་རོགས།",
114 116
     phTeamMembers: "འདེམ་ཆོག,500་ཡིག",

+ 20 - 0
ruoyi-ui/src/lang/bo/techService.js

@@ -365,5 +365,25 @@ export default {
365 365
     msgRejectOk: "ངོས་ལེན་མ་བྱས་ཟིན།",
366 366
     msgCompleteOk: "མཇུག་སྒྲིལ་ཟིན།",
367 367
     dateRangeInvalid: "མཇུག་ཚེས་སྔོན་དུས་མི་ཆོག"
368
+  },
369
+  techServicePersonnel: {
370
+    pageTitle: "ཚན་རིག་ཞབས་ཞུ་མི་སྣ་དོ་དམ།",
371
+    sectionEnvoy: "ཚན་རིག་ཆེད་མངགས་པ།",
372
+    sectionAgri: "ཞིང་ལས་སྡེ་ཚན།",
373
+    formEnvoyMale: "ཕོ་གྲངས།",
374
+    formEnvoyFemale: "མོ་གྲངས།",
375
+    formEnvoyParty: "ཏང་ཡོན་གྲངས།",
376
+    formEnvoyTotal: "སྤྱི་གྲངས།",
377
+    formAgriJunior: "ཐོག་མའི་གྲངས།",
378
+    formAgriClerk: "ཡིག་ཆའི་གྲངས།",
379
+    formAgriIntermediate: "བར་རིམ་གྲངས།",
380
+    formAgriAssociateSenior: "ཞིབ་འཇུག་གྲངས།",
381
+    formAgriManagement: "དོ་དམ་རིམ་གྲངས།",
382
+    formAgriTotal: "སྤྱི་གྲངས།",
383
+    unitPerson: "མི།",
384
+    btnSave: "ཉར་ཚགས།",
385
+    viewUpdateBy: "བཟོ་མཁན།",
386
+    viewUpdateTime: "བཟོ་དུས།",
387
+    ruleCountNonNegative: "0་ཡན་གྱི་ཧྲིལ་གྲངས་དགོས།"
368 388
   }
369 389
 }

+ 3 - 1
ruoyi-ui/src/lang/zh/diseaseTreatment.js

@@ -90,6 +90,7 @@ export default {
90 90
     formIntroduction: "介绍",
91 91
     formContactPhone: "联系方式",
92 92
     formAffiliatedUnit: "隶属单位",
93
+    formTown: "所属乡镇",
93 94
     formDetailAddress: "详细地址",
94 95
     formConsultModes: "接诊方式",
95 96
     formServiceArea: "服务区域",
@@ -107,8 +108,9 @@ export default {
107 108
     formEquipmentModel: "设备型号",
108 109
     phResourceName: "请输入展示名称",
109 110
     phIntroduction: "请输入介绍",
110
-    phContactPhone: "请输入手机号",
111
+    phContactPhone: "选填,请输入手机号",
111 112
     phAffiliatedUnit: "请输入隶属单位",
113
+    phTown: "请选择所属乡镇(选填)",
112 114
     phDetailAddress: "请输入详细地址",
113 115
     phServiceArea: "请输入服务区域",
114 116
     phTeamMembers: "选填,最多 500 字",

+ 20 - 0
ruoyi-ui/src/lang/zh/techService.js

@@ -365,5 +365,25 @@ export default {
365 365
     msgRejectOk: "已拒绝接诊",
366 366
     msgCompleteOk: "已完成接诊",
367 367
     dateRangeInvalid: "结束日期不能早于开始日期"
368
+  },
369
+  techServicePersonnel: {
370
+    pageTitle: "科技服务人员管理",
371
+    sectionEnvoy: "科技特派员",
372
+    sectionAgri: "农业事业单位",
373
+    formEnvoyMale: "男性人数",
374
+    formEnvoyFemale: "女性人数",
375
+    formEnvoyParty: "党员人数",
376
+    formEnvoyTotal: "总人数",
377
+    formAgriJunior: "初级人数",
378
+    formAgriClerk: "员级人数",
379
+    formAgriIntermediate: "中级人数",
380
+    formAgriAssociateSenior: "副高人数",
381
+    formAgriManagement: "管理级人数",
382
+    formAgriTotal: "总人数",
383
+    unitPerson: "人",
384
+    btnSave: "保 存",
385
+    viewUpdateBy: "更新人",
386
+    viewUpdateTime: "更新时间",
387
+    ruleCountNonNegative: "须为大于等于 0 的整数"
368 388
   }
369 389
 }

+ 1 - 1
ruoyi-ui/src/views/dataModel/farmerHousehold/index.vue

@@ -860,7 +860,7 @@ export default {
860 860
         const payload = this.buildSubmitPayload()
861 861
         if (this.dialogEdit) {
862 862
           updateFarmerHousehold(payload).then(() => {
863
-            this.$modal.msgSuccess(this.dmCommon("msgEditOk"))
863
+            this.$modal.msgSuccess(this.dmCommon("msgModOk"))
864 864
             this.open = false
865 865
             this.getList()
866 866
           })

+ 1 - 1
ruoyi-ui/src/views/dataModel/livestockOwnershipHousehold/index.vue

@@ -674,7 +674,7 @@ export default {
674 674
         const payload = this.buildSubmitPayload()
675 675
         if (this.dialogEdit) {
676 676
           updateLivestockOwnershipHousehold(payload).then(() => {
677
-            this.$modal.msgSuccess(this.dmCommon("msgEditOk"))
677
+            this.$modal.msgSuccess(this.dmCommon("msgModOk"))
678 678
             this.open = false
679 679
             this.getList()
680 680
           })

+ 1 - 1
ruoyi-ui/src/views/dataModel/ruralThreeAssets/index.vue

@@ -425,7 +425,7 @@ export default {
425 425
         const payload = this.buildSubmitPayload()
426 426
         if (this.dialogEdit) {
427 427
           updateRuralThreeAssets(payload).then(() => {
428
-            this.$modal.msgSuccess(this.dmCommon("msgEditOk"))
428
+            this.$modal.msgSuccess(this.dmCommon("msgModOk"))
429 429
             this.open = false
430 430
             this.getList()
431 431
           })

+ 1 - 1
ruoyi-ui/src/views/dataModel/vaccinationData/index.vue

@@ -665,7 +665,7 @@ export default {
665 665
         const payload = this.buildSubmitPayload()
666 666
         if (this.dialogEdit) {
667 667
           updateVaccinationData(payload).then(() => {
668
-            this.$modal.msgSuccess(this.dmCommon("msgEditOk"))
668
+            this.$modal.msgSuccess(this.dmCommon("msgModOk"))
669 669
             this.open = false
670 670
             this.getList()
671 671
           })

+ 41 - 8
ruoyi-ui/src/views/diseaseTreatment/medicalResource/index.vue

@@ -207,6 +207,23 @@
207 207
             <template slot="label">{{ dtT("formAffiliatedUnit") }}</template>
208 208
             <el-input v-model="form.affiliatedUnit" :placeholder="dtT('phAffiliatedUnit')" maxlength="20" show-word-limit />
209 209
           </el-form-item>
210
+          <el-form-item prop="townDeptId">
211
+            <template slot="label">{{ dtT("formTown") }}</template>
212
+            <el-select
213
+              v-model="form.townDeptId"
214
+              :placeholder="dtT('phTown')"
215
+              clearable
216
+              filterable
217
+              style="width: 100%"
218
+            >
219
+              <el-option
220
+                v-for="item in townOptions"
221
+                :key="item.deptId"
222
+                :label="item.deptName"
223
+                :value="item.deptId"
224
+              />
225
+            </el-select>
226
+          </el-form-item>
210 227
           <el-form-item prop="detailAddress" :rules="formRules.detailAddress">
211 228
             <template slot="label">{{ dtT("formDetailAddress") }}</template>
212 229
             <el-input v-model="form.detailAddress" :placeholder="dtT('phDetailAddress')" maxlength="100" show-word-limit />
@@ -374,6 +391,9 @@
374 391
         <el-form-item v-if="viewRow.affiliatedUnit" :label="dtT('formAffiliatedUnit')">
375 392
           <span>{{ viewRow.affiliatedUnit }}</span>
376 393
         </el-form-item>
394
+        <el-form-item v-if="isVetType(viewRow.resourceType)" :label="dtT('formTown')">
395
+          <span>{{ viewRow.townName || $t('diseaseTreatment.status.dash') }}</span>
396
+        </el-form-item>
377 397
         <el-form-item v-if="viewRow.detailAddress" :label="dtT('formDetailAddress')">
378 398
           <span>{{ viewRow.detailAddress }}</span>
379 399
         </el-form-item>
@@ -449,6 +469,7 @@ import {
449 469
   publishMedicalResource,
450 470
   offlineMedicalResource,
451 471
   assignAccountMedicalResource,
472
+  listMedicalResourceTownOptions,
452 473
   batchPublishMedicalResource,
453 474
   batchOfflineMedicalResource,
454 475
   batchAssignAccountMedicalResource
@@ -501,6 +522,7 @@ export default {
501 522
       tableList: [],
502 523
       selectedRows: [],
503 524
       resourceTypeOptions: [],
525
+      townOptions: [],
504 526
       queryParams: {
505 527
         pageNum: 1,
506 528
         pageSize: 20,
@@ -581,7 +603,7 @@ export default {
581 603
         rules.contactPhone = phoneRuleOptional
582 604
         rules.affiliatedUnit = [{ max: 20, message: this.dtCommon("ruleLen120"), trigger: "blur" }]
583 605
         rules.detailAddress = [{ max: 100, message: this.dtCommon("ruleLen1100"), trigger: "blur" }]
584
-        rules.consultModesList = []
606
+        rules.consultModesList = [{ validator: this.validateConsultModes, trigger: "change" }]
585 607
         rules.serviceArea = [{ max: 100, message: this.dtCommon("ruleLen1100"), trigger: "blur" }]
586 608
         rules.feeStandard = []
587 609
         rules.serviceStartTime = [{ validator: this.validateServiceTimeOptional, trigger: "change" }]
@@ -590,11 +612,8 @@ export default {
590 612
         rules.photoFilePath = []
591 613
         rules.introduction = [{ max: 500, message: this.dtCommon("ruleLen1500"), trigger: "blur" }]
592 614
       } else {
593
-        rules.photoFilePath = [{ required: true, message: this.dtT("rulePhoto"), trigger: "change" }]
594
-        rules.introduction = [
595
-          { required: true, message: this.dtT("ruleIntroduction"), trigger: "blur" },
596
-          { min: 1, max: 500, message: this.dtCommon("ruleLen1500"), trigger: "blur" }
597
-        ]
615
+        rules.photoFilePath = []
616
+        rules.introduction = [{ max: 500, message: this.dtCommon("ruleLen1500"), trigger: "blur" }]
598 617
       }
599 618
       if (this.isTeamType(t)) {
600 619
         rules.contactPhone = phoneRuleOptional
@@ -621,8 +640,18 @@ export default {
621 640
     this.loadResourceTypeOptions().finally(() => {
622 641
       this.getList()
623 642
     })
643
+    this.loadTownOptions()
624 644
   },
625 645
   methods: {
646
+    loadTownOptions() {
647
+      listMedicalResourceTownOptions()
648
+        .then((res) => {
649
+          this.townOptions = Array.isArray(res.data) ? res.data : []
650
+        })
651
+        .catch(() => {
652
+          this.townOptions = []
653
+        })
654
+    },
626 655
     mapCategoryItem(item) {
627 656
       if (!item) {
628 657
         return { value: "", label: "" }
@@ -1008,6 +1037,7 @@ export default {
1008 1037
         introduction: undefined,
1009 1038
         contactPhone: undefined,
1010 1039
         affiliatedUnit: undefined,
1040
+        townDeptId: undefined,
1011 1041
         detailAddress: undefined,
1012 1042
         consultModesList: this.isVetType(resourceType) ? this.allConsultModeValues() : [],
1013 1043
         serviceArea: undefined,
@@ -1015,7 +1045,7 @@ export default {
1015 1045
         serviceStartTime: hasServiceSchedule ? this.defaultServiceStartTime() : undefined,
1016 1046
         serviceEndTime: hasServiceSchedule ? this.defaultServiceEndTime() : undefined,
1017 1047
         serviceWeekdaysList: hasServiceSchedule ? this.defaultServiceWeekdaysList() : [],
1018
-        maxDailyAppointments: this.isOrgType(resourceType) ? 30 : undefined,
1048
+        maxDailyAppointments: undefined,
1019 1049
         establishDate: undefined,
1020 1050
         teamSize: undefined,
1021 1051
         teamMembers: undefined,
@@ -1049,6 +1079,7 @@ export default {
1049 1079
         introduction: row.introduction,
1050 1080
         contactPhone: row.contactPhone,
1051 1081
         affiliatedUnit: row.affiliatedUnit,
1082
+        townDeptId: this.isVetType(row.resourceType) ? row.townDeptId : undefined,
1052 1083
         detailAddress: row.detailAddress,
1053 1084
         consultModesList: [...(row.consultModesList || [])],
1054 1085
         serviceArea: row.serviceArea,
@@ -1141,6 +1172,7 @@ export default {
1141 1172
       if (this.isVetType(t)) {
1142 1173
         this.form.consultModesList = this.allConsultModeValues()
1143 1174
       } else {
1175
+        this.form.townDeptId = undefined
1144 1176
         this.form.detailAddress = undefined
1145 1177
         this.form.consultModesList = []
1146 1178
         this.form.serviceArea = undefined
@@ -1155,7 +1187,7 @@ export default {
1155 1187
         this.form.orgLevel = undefined
1156 1188
         this.form.maxDailyAppointments = undefined
1157 1189
       } else {
1158
-        this.form.maxDailyAppointments = 30
1190
+        this.form.maxDailyAppointments = undefined
1159 1191
       }
1160 1192
       if (!this.isVetType(t) && !this.isOrgType(t)) {
1161 1193
         this.form.serviceStartTime = undefined
@@ -1236,6 +1268,7 @@ export default {
1236 1268
         Object.assign(payload, {
1237 1269
           contactPhone: this.form.contactPhone,
1238 1270
           affiliatedUnit: (this.form.affiliatedUnit || "").trim(),
1271
+          townDeptId: this.form.townDeptId,
1239 1272
           detailAddress: (this.form.detailAddress || "").trim(),
1240 1273
           consultModesList: this.form.consultModesList,
1241 1274
           serviceArea: (this.form.serviceArea || "").trim(),

+ 328 - 0
ruoyi-ui/src/views/techService/techServicePersonnel/index.vue

@@ -0,0 +1,328 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-card shadow="never" v-loading="loading">
4
+      <div slot="header" class="clearfix">
5
+        <span>{{ tsT("pageTitle") }}</span>
6
+      </div>
7
+      <el-form ref="form" :model="form" label-width="140px" class="tsp-form" size="small">
8
+        <div class="tsp-section-title">{{ tsT("sectionEnvoy") }}</div>
9
+        <el-form-item prop="envoyMaleCount" :rules="countRule">
10
+          <template slot="label">{{ tsT("formEnvoyMale") }}</template>
11
+          <div class="dm-input-with-unit">
12
+            <el-input-number
13
+              v-model="form.envoyMaleCount"
14
+              :min="0"
15
+              :precision="0"
16
+              controls-position="right"
17
+              class="dm-input-with-unit__input"
18
+            />
19
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
20
+          </div>
21
+        </el-form-item>
22
+        <el-form-item prop="envoyFemaleCount" :rules="countRule">
23
+          <template slot="label">{{ tsT("formEnvoyFemale") }}</template>
24
+          <div class="dm-input-with-unit">
25
+            <el-input-number
26
+              v-model="form.envoyFemaleCount"
27
+              :min="0"
28
+              :precision="0"
29
+              controls-position="right"
30
+              class="dm-input-with-unit__input"
31
+            />
32
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
33
+          </div>
34
+        </el-form-item>
35
+        <el-form-item prop="envoyPartyMemberCount" :rules="countRule">
36
+          <template slot="label">{{ tsT("formEnvoyParty") }}</template>
37
+          <div class="dm-input-with-unit">
38
+            <el-input-number
39
+              v-model="form.envoyPartyMemberCount"
40
+              :min="0"
41
+              :precision="0"
42
+              controls-position="right"
43
+              class="dm-input-with-unit__input"
44
+            />
45
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
46
+          </div>
47
+        </el-form-item>
48
+        <el-form-item>
49
+          <template slot="label">{{ tsT("formEnvoyTotal") }}</template>
50
+          <div class="dm-input-with-unit">
51
+            <el-input :value="envoyTotalDisplay" disabled class="dm-input-with-unit__input" />
52
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
53
+          </div>
54
+        </el-form-item>
55
+
56
+        <div class="tsp-section-title">{{ tsT("sectionAgri") }}</div>
57
+        <el-form-item prop="agriJuniorCount" :rules="countRule">
58
+          <template slot="label">{{ tsT("formAgriJunior") }}</template>
59
+          <div class="dm-input-with-unit">
60
+            <el-input-number
61
+              v-model="form.agriJuniorCount"
62
+              :min="0"
63
+              :precision="0"
64
+              controls-position="right"
65
+              class="dm-input-with-unit__input"
66
+            />
67
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
68
+          </div>
69
+        </el-form-item>
70
+        <el-form-item prop="agriClerkCount" :rules="countRule">
71
+          <template slot="label">{{ tsT("formAgriClerk") }}</template>
72
+          <div class="dm-input-with-unit">
73
+            <el-input-number
74
+              v-model="form.agriClerkCount"
75
+              :min="0"
76
+              :precision="0"
77
+              controls-position="right"
78
+              class="dm-input-with-unit__input"
79
+            />
80
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
81
+          </div>
82
+        </el-form-item>
83
+        <el-form-item prop="agriIntermediateCount" :rules="countRule">
84
+          <template slot="label">{{ tsT("formAgriIntermediate") }}</template>
85
+          <div class="dm-input-with-unit">
86
+            <el-input-number
87
+              v-model="form.agriIntermediateCount"
88
+              :min="0"
89
+              :precision="0"
90
+              controls-position="right"
91
+              class="dm-input-with-unit__input"
92
+            />
93
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
94
+          </div>
95
+        </el-form-item>
96
+        <el-form-item prop="agriAssociateSeniorCount" :rules="countRule">
97
+          <template slot="label">{{ tsT("formAgriAssociateSenior") }}</template>
98
+          <div class="dm-input-with-unit">
99
+            <el-input-number
100
+              v-model="form.agriAssociateSeniorCount"
101
+              :min="0"
102
+              :precision="0"
103
+              controls-position="right"
104
+              class="dm-input-with-unit__input"
105
+            />
106
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
107
+          </div>
108
+        </el-form-item>
109
+        <el-form-item prop="agriManagementCount" :rules="countRule">
110
+          <template slot="label">{{ tsT("formAgriManagement") }}</template>
111
+          <div class="dm-input-with-unit">
112
+            <el-input-number
113
+              v-model="form.agriManagementCount"
114
+              :min="0"
115
+              :precision="0"
116
+              controls-position="right"
117
+              class="dm-input-with-unit__input"
118
+            />
119
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
120
+          </div>
121
+        </el-form-item>
122
+        <el-form-item>
123
+          <template slot="label">{{ tsT("formAgriTotal") }}</template>
124
+          <div class="dm-input-with-unit">
125
+            <el-input :value="agriTotalDisplay" disabled class="dm-input-with-unit__input" />
126
+            <span class="dm-input-with-unit__suffix">{{ tsT("unitPerson") }}</span>
127
+          </div>
128
+        </el-form-item>
129
+
130
+        <el-form-item v-if="meta.updateTime">
131
+          <span class="tsp-meta">{{ tsT("viewUpdateBy") }}:{{ meta.updateBy || dash }}</span>
132
+          <span class="tsp-meta">{{ tsT("viewUpdateTime") }}:{{ meta.updateTime ? parseTime(meta.updateTime) : dash }}</span>
133
+        </el-form-item>
134
+        <el-form-item>
135
+          <el-button
136
+            v-hasPermi="['techService:techServicePersonnel:edit']"
137
+            type="primary"
138
+            @click="submitForm"
139
+          >{{ tsT("btnSave") }}</el-button>
140
+        </el-form-item>
141
+      </el-form>
142
+    </el-card>
143
+  </div>
144
+</template>
145
+
146
+<script>
147
+import techServiceLocaleMixin from "@/mixins/techServiceLocaleMixin"
148
+import { getTechServicePersonnel, updateTechServicePersonnel } from "@/api/techService/techServicePersonnel"
149
+
150
+const ENVOY_SUM_KEYS = ["envoyMaleCount", "envoyFemaleCount"]
151
+const AGRI_SUM_KEYS = [
152
+  "agriJuniorCount",
153
+  "agriClerkCount",
154
+  "agriIntermediateCount",
155
+  "agriAssociateSeniorCount",
156
+  "agriManagementCount"
157
+]
158
+const EDITABLE_KEYS = [
159
+  "envoyMaleCount",
160
+  "envoyFemaleCount",
161
+  "envoyPartyMemberCount",
162
+  "agriJuniorCount",
163
+  "agriClerkCount",
164
+  "agriIntermediateCount",
165
+  "agriAssociateSeniorCount",
166
+  "agriManagementCount"
167
+]
168
+
169
+function sumNullable(values) {
170
+  let any = false
171
+  let sum = 0
172
+  values.forEach((v) => {
173
+    if (v != null && v !== "") {
174
+      const n = Number(v)
175
+      if (Number.isFinite(n)) {
176
+        any = true
177
+        sum += n
178
+      }
179
+    }
180
+  })
181
+  return any ? sum : null
182
+}
183
+
184
+export default {
185
+  name: "TechServicePersonnel",
186
+  mixins: [techServiceLocaleMixin],
187
+  data() {
188
+    return {
189
+      tsNs: "techServicePersonnel",
190
+      loading: false,
191
+      meta: {
192
+        updateBy: undefined,
193
+        updateTime: undefined
194
+      },
195
+      form: this.defaultForm()
196
+    }
197
+  },
198
+  computed: {
199
+    dash() {
200
+      return this.$t("techService.status.dash")
201
+    },
202
+    countRule() {
203
+      return [{ validator: this.validateOptionalCount, trigger: "change" }]
204
+    },
205
+    envoyTotalDisplay() {
206
+      const sum = sumNullable(ENVOY_SUM_KEYS.map((k) => this.form[k]))
207
+      return sum == null ? this.dash : String(sum)
208
+    },
209
+    agriTotalDisplay() {
210
+      const sum = sumNullable(AGRI_SUM_KEYS.map((k) => this.form[k]))
211
+      return sum == null ? this.dash : String(sum)
212
+    }
213
+  },
214
+  created() {
215
+    this.loadConfig()
216
+  },
217
+  methods: {
218
+    defaultForm() {
219
+      const form = {}
220
+      EDITABLE_KEYS.forEach((key) => {
221
+        form[key] = undefined
222
+      })
223
+      return form
224
+    },
225
+    validateOptionalCount(rule, value, callback) {
226
+      if (value == null || value === "") {
227
+        callback()
228
+        return
229
+      }
230
+      if (typeof value !== "number" || Number.isNaN(value) || value < 0 || !Number.isInteger(value)) {
231
+        callback(new Error(this.tsT("ruleCountNonNegative")))
232
+        return
233
+      }
234
+      callback()
235
+    },
236
+    normalizeCount(val) {
237
+      if (val == null || val === "") {
238
+        return undefined
239
+      }
240
+      const n = Number(val)
241
+      return Number.isFinite(n) ? n : undefined
242
+    },
243
+    applyData(data) {
244
+      if (!data) {
245
+        this.form = this.defaultForm()
246
+        this.meta = { updateBy: undefined, updateTime: undefined }
247
+        return
248
+      }
249
+      const form = this.defaultForm()
250
+      EDITABLE_KEYS.forEach((key) => {
251
+        form[key] = this.normalizeCount(data[key])
252
+      })
253
+      this.form = form
254
+      this.meta = {
255
+        updateBy: data.updateBy,
256
+        updateTime: data.updateTime
257
+      }
258
+    },
259
+    loadConfig() {
260
+      this.loading = true
261
+      getTechServicePersonnel()
262
+        .then((res) => {
263
+          this.applyData(res.data)
264
+        })
265
+        .catch(() => {
266
+          this.applyData(null)
267
+        })
268
+        .finally(() => {
269
+          this.loading = false
270
+        })
271
+    },
272
+    buildPayload() {
273
+      const payload = {}
274
+      EDITABLE_KEYS.forEach((key) => {
275
+        const val = this.form[key]
276
+        payload[key] = val == null || val === "" ? null : val
277
+      })
278
+      return payload
279
+    },
280
+    submitForm() {
281
+      this.$refs.form.validate((valid) => {
282
+        if (!valid) {
283
+          return
284
+        }
285
+        updateTechServicePersonnel(this.buildPayload()).then(() => {
286
+          this.$modal.msgSuccess(this.tsCommon("msgModOk"))
287
+          this.loadConfig()
288
+        })
289
+      })
290
+    }
291
+  }
292
+}
293
+</script>
294
+
295
+<style scoped>
296
+.tsp-form {
297
+  max-width: 560px;
298
+}
299
+.tsp-section-title {
300
+  margin: 4px 0 16px;
301
+  font-weight: 600;
302
+  color: #303133;
303
+}
304
+.tsp-meta {
305
+  display: block;
306
+  color: #909399;
307
+  font-size: 13px;
308
+  line-height: 1.8;
309
+}
310
+.dm-input-with-unit {
311
+  display: flex;
312
+  align-items: center;
313
+  width: 100%;
314
+  max-width: 320px;
315
+}
316
+.dm-input-with-unit__input {
317
+  flex: 1;
318
+  width: 100%;
319
+}
320
+.dm-input-with-unit__suffix {
321
+  margin-left: 8px;
322
+  color: #909399;
323
+  white-space: nowrap;
324
+}
325
+.dm-input-with-unit >>> .el-input-number {
326
+  width: 100%;
327
+}
328
+</style>