Parcourir la source

移动端:新增人事管理模块

余思翰 il y a 1 semaine
Parent
révision
c73abf7dd4

+ 8
- 0
oa-ui-app/api/system/dept.js Voir le fichier

@@ -17,6 +17,14 @@ export function treeselect() {
17 17
   })
18 18
 }
19 19
 
20
+// 查询部门下拉树结构(新版本)
21
+export function deptTreeSelectNew() {
22
+  return request({
23
+    url: '/system/dept/treeselect',
24
+    method: 'get'
25
+  })
26
+}
27
+
20 28
 // 查询部门列表(排除节点)
21 29
 export function listDeptExcludeChild(deptId) {
22 30
   return request({

+ 8
- 0
oa-ui-app/pages.json Voir le fichier

@@ -175,6 +175,14 @@
175 175
 				"enablePullDownRefresh": true
176 176
 			}
177 177
 		},
178
+		{
179
+			"path" : "pages/oa/staff/staffList",
180
+			"style" : 
181
+			{
182
+				"navigationBarTitleText" : "人事管理",
183
+				"enablePullDownRefresh": true
184
+			}
185
+		},
178 186
 		{
179 187
 			"path" : "pages/components/formInfo",
180 188
 			"style" : 

+ 328
- 5
oa-ui-app/pages/oa/declare/declareList.vue Voir le fichier

@@ -2,7 +2,7 @@
2 2
  * @Author: ysh
3 3
  * @Date: 2025-08-04 11:04:25
4 4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-08-04 14:50:13
5
+ * @LastEditTime: 2025-08-08 09:15:18
6 6
 -->
7 7
 <template>
8 8
   <view class="container">
@@ -95,6 +95,79 @@
95 95
         </view>
96 96
       </view>
97 97
     </mescroll-uni>
98
+
99
+    <!-- 修改对话框 -->
100
+    <uni-popup ref="updatePopup" type="center" :mask-click="false">
101
+      <view class="update-dialog">
102
+        <view class="dialog-header">
103
+          <text class="dialog-title">修改工作填报</text>
104
+          <text class="dialog-close" @click="closeUpdateDialog">×</text>
105
+        </view>
106
+
107
+        <view class="dialog-body">
108
+          <!-- 只读信息 -->
109
+          <view class="readonly-section">
110
+            <view class="info-item">
111
+              <text class="info-label">项目编号:</text>
112
+              <text class="info-value">{{ updateForm.projectNumber }}</text>
113
+            </view>
114
+            <view class="info-item">
115
+              <text class="info-label">项目名称:</text>
116
+              <text class="info-value">{{ updateForm.projectName }}</text>
117
+            </view>
118
+            <view class="info-item">
119
+              <text class="info-label">填报人:</text>
120
+              <text class="info-value">{{ updateForm.nickName }}</text>
121
+            </view>
122
+            <view class="info-item">
123
+              <text class="info-label">工作类别:</text>
124
+              <text class="info-value">{{ updateForm.workType }}</text>
125
+            </view>
126
+            <view class="info-item">
127
+              <text class="info-label">工作项目:</text>
128
+              <text class="info-value">{{ updateForm.workItem }}</text>
129
+            </view>
130
+            <view class="info-item">
131
+              <text class="info-label">工作内容:</text>
132
+              <text class="info-value">{{ updateForm.workContent }}</text>
133
+            </view>
134
+            <view class="info-item">
135
+              <text class="info-label">工作时间:</text>
136
+              <text class="info-value">{{ updateForm.beginDate }} ~ {{ updateForm.endDate }}</text>
137
+            </view>
138
+            <view class="info-item">
139
+              <text class="info-label">工天数量:</text>
140
+              <text class="info-value">{{ updateForm.workLoad }}天</text>
141
+            </view>
142
+            <view class="info-item">
143
+              <text class="info-label">工天单价:</text>
144
+              <text class="info-value">¥{{ updateForm.price }}</text>
145
+            </view>
146
+          </view>
147
+
148
+          <!-- 可修改的系数 -->
149
+          <view class="edit-section">
150
+            <view class="form-item">
151
+              <text class="form-label">系数:</text>
152
+              <uni-easyinput class="form-input" type="number" v-model="updateForm.coefficient"
153
+                @input="calculatePredictMoney" @confirm="confirmUpdate" placeholder="请输入系数" :clearable="false"
154
+                :trim="true" />
155
+            </view>
156
+
157
+            <!-- 预估绩效显示 -->
158
+            <view class="predict-section">
159
+              <text class="predict-label">预估绩效:</text>
160
+              <text class="predict-amount">¥{{ predictMoney(updateForm) }}</text>
161
+            </view>
162
+          </view>
163
+        </view>
164
+
165
+        <view class="dialog-footer">
166
+          <button class="dialog-btn cancel-btn" @click="closeUpdateDialog">取消</button>
167
+          <button class="dialog-btn confirm-btn" @click="confirmUpdate">确认修改</button>
168
+        </view>
169
+      </view>
170
+    </uni-popup>
98 171
   </view>
99 172
 </template>
100 173
 
@@ -126,6 +199,18 @@ export default {
126 199
         projectId: null,
127 200
         userId: null,
128 201
         submitTime: null
202
+      },
203
+      // 修改表单数据
204
+      updateForm: {
205
+        formId: null,
206
+        workType: '',
207
+        workItem: '',
208
+        workContent: '',
209
+        beginDate: '',
210
+        endDate: '',
211
+        workLoad: 0,
212
+        price: 0,
213
+        coefficient: 0
129 214
       }
130 215
     }
131 216
   },
@@ -220,8 +305,16 @@ export default {
220 305
 
221 306
     // 计算预估绩效
222 307
     predictMoney(row) {
223
-      let result = parseFloat(row.price * row.workLoad * row.coefficient)
224
-      return result.toFixed(2)
308
+      try {
309
+        const price = parseFloat(row.price) || 0
310
+        const workLoad = parseFloat(row.workLoad) || 0
311
+        const coefficient = parseFloat(row.coefficient) || 0
312
+        let result = price * workLoad * coefficient
313
+        return result.toFixed(2)
314
+      } catch (error) {
315
+        console.log('计算预估绩效失败:', error)
316
+        return '0.00'
317
+      }
225 318
     },
226 319
 
227 320
     // 时间解析函数
@@ -259,7 +352,6 @@ export default {
259 352
 
260 353
     // 打开表单信息
261 354
     openFormInfo(item) {
262
-      console.log(item)
263 355
       uni.navigateTo({
264 356
         url: `/pages/components/formInfo?formId=${item.formId}&formType=declare`
265 357
       });
@@ -267,7 +359,97 @@ export default {
267 359
 
268 360
     // 修改操作
269 361
     handleUpdate(item) {
270
-      
362
+      console.log(item)
363
+      // 填充修改表单数据
364
+      this.updateForm = {
365
+        formId: item.formId,
366
+        projectNumber: item.project.projectNumber,
367
+        projectName: item.project.projectName,
368
+        nickName: item.user.nickName,
369
+        workType: item.workType,
370
+        workItem: item.workItem,
371
+        workContent: item.workContent,
372
+        beginDate: item.beginDate,
373
+        endDate: item.endDate,
374
+        workLoad: item.workLoad,
375
+        price: item.price,
376
+        coefficient: item.coefficient
377
+      }
378
+
379
+      // 打开修改对话框
380
+      this.$refs.updatePopup.open()
381
+
382
+      // 延迟聚焦到系数输入框(移动端兼容)
383
+      this.$nextTick(() => {
384
+        setTimeout(() => {
385
+          try {
386
+            // 查找uni-easyinput内部的input元素
387
+            const input = document.querySelector('.form-input .uni-easyinput__content-input')
388
+            if (input && typeof input.focus === 'function') {
389
+              input.focus()
390
+            }
391
+          } catch (error) {
392
+            console.log('聚焦输入框失败:', error)
393
+          }
394
+        }, 300)
395
+      })
396
+    },
397
+
398
+    // 关闭修改对话框
399
+    closeUpdateDialog() {
400
+      this.$refs.updatePopup.close()
401
+    },
402
+
403
+    // 计算预估绩效(当系数改变时)
404
+    calculatePredictMoney() {
405
+      // 系数输入时自动触发计算,这里不需要额外处理
406
+      // 因为模板中已经绑定了 predictMoney(updateForm)
407
+    },
408
+
409
+    // 确认修改
410
+    async confirmUpdate() {
411
+      try {
412
+        // 验证系数
413
+        if (!this.updateForm.coefficient || this.updateForm.coefficient <= 0) {
414
+          uni.showToast({
415
+            title: '请输入有效的系数',
416
+            icon: 'none'
417
+          })
418
+          return
419
+        }
420
+
421
+        // 显示加载状态
422
+        uni.showLoading({
423
+          title: '修改中...'
424
+        })
425
+
426
+        // 调用更新接口
427
+        await updateDeclare(this.updateForm)
428
+
429
+        // 隐藏加载状态
430
+        uni.hideLoading()
431
+
432
+        uni.showToast({
433
+          title: '修改成功',
434
+          icon: 'success'
435
+        })
436
+
437
+        // 关闭对话框
438
+        this.closeUpdateDialog()
439
+
440
+        // 刷新列表
441
+        this.downCallback()
442
+
443
+      } catch (error) {
444
+        // 隐藏加载状态
445
+        uni.hideLoading()
446
+
447
+        console.error('修改失败:', error)
448
+        uni.showToast({
449
+          title: '修改失败,请重试',
450
+          icon: 'none'
451
+        })
452
+      }
271 453
     }
272 454
   }
273 455
 }
@@ -456,4 +638,145 @@ export default {
456 638
   background: #f4c542;
457 639
   color: #fff;
458 640
 }
641
+
642
+// 修改对话框样式
643
+.update-dialog {
644
+  background: #fff;
645
+  border-radius: 20rpx;
646
+  width: 600rpx;
647
+  max-height: 80vh;
648
+  overflow: hidden;
649
+}
650
+
651
+.dialog-header {
652
+  display: flex;
653
+  justify-content: space-between;
654
+  align-items: center;
655
+  padding: 32rpx 32rpx 24rpx 32rpx;
656
+  border-bottom: 1px solid #f0f0f0;
657
+  background: linear-gradient(90deg, #e0e7ff 0%, #f8fafc 100%);
658
+}
659
+
660
+.dialog-title {
661
+  font-size: 32rpx;
662
+  font-weight: bold;
663
+  color: #22223b;
664
+}
665
+
666
+.dialog-close {
667
+  font-size: 40rpx;
668
+  color: #64748b;
669
+  cursor: pointer;
670
+  padding: 8rpx;
671
+}
672
+
673
+.dialog-body {
674
+  padding: 32rpx;
675
+  max-height: 60vh;
676
+  overflow-y: auto;
677
+}
678
+
679
+.readonly-section {
680
+  margin-bottom: 32rpx;
681
+  padding-bottom: 24rpx;
682
+  border-bottom: 1px solid #f0f0f0;
683
+}
684
+
685
+.info-item {
686
+  display: flex;
687
+  margin-bottom: 16rpx;
688
+}
689
+
690
+.edit-section {
691
+  padding-top: 16rpx;
692
+}
693
+
694
+.form-item {
695
+  display: flex;
696
+  align-items: center;
697
+  margin-bottom: 24rpx;
698
+}
699
+
700
+.form-label {
701
+  color: #64748b;
702
+  font-size: 28rpx;
703
+  min-width: 120rpx;
704
+}
705
+
706
+// uni-easyinput 样式调整
707
+:deep(.uni-easyinput) {
708
+  flex: 1;
709
+
710
+  .uni-easyinput__content {
711
+    border: 1px solid #e2e8f0 !important;
712
+    border-radius: 8rpx !important;
713
+    background: #fff !important;
714
+    padding: 16rpx 20rpx !important;
715
+  }
716
+
717
+  .uni-easyinput__content-input {
718
+    border: none !important;
719
+    background: transparent !important;
720
+    font-size: 28rpx !important;
721
+    color: #22223b !important;
722
+  }
723
+
724
+  .uni-easyinput__content:focus-within {
725
+    border-color: #6366f1 !important;
726
+  }
727
+}
728
+
729
+.predict-section {
730
+  display: flex;
731
+  align-items: center;
732
+  background: #fef3c7;
733
+  padding: 20rpx;
734
+  border-radius: 12rpx;
735
+  margin-top: 16rpx;
736
+}
737
+
738
+.predict-label {
739
+  color: #d97706;
740
+  font-size: 28rpx;
741
+  font-weight: 500;
742
+}
743
+
744
+.predict-amount {
745
+  color: #d97706;
746
+  font-size: 32rpx;
747
+  font-weight: bold;
748
+  margin-left: 16rpx;
749
+}
750
+
751
+.dialog-footer {
752
+  display: flex;
753
+  justify-content: flex-end;
754
+  gap: 20rpx;
755
+  padding: 24rpx 32rpx 32rpx 32rpx;
756
+  border-top: 1px solid #f0f0f0;
757
+  background: #fafbfc;
758
+}
759
+
760
+.dialog-btn {
761
+  font-size: 28rpx;
762
+  border-radius: 8rpx;
763
+  border: none;
764
+  outline: none;
765
+  padding: 16rpx 32rpx;
766
+  cursor: pointer;
767
+}
768
+
769
+.cancel-btn {
770
+  background: #f1f5f9;
771
+  color: #64748b;
772
+}
773
+
774
+.confirm-btn {
775
+  background: #6366f1;
776
+  color: #fff;
777
+}
778
+
779
+.confirm-btn:hover {
780
+  background: #4f46e5;
781
+}
459 782
 </style>

+ 122
- 153
oa-ui-app/pages/oa/device/instrumentsList.vue Voir le fichier

@@ -2,7 +2,7 @@
2 2
  * @Author: ysh
3 3
  * @Date: 2025-07-08 14:00:47
4 4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-08-04 10:15:13
5
+ * @LastEditTime: 2025-08-12 09:32:32
6 6
 -->
7 7
 <template>
8 8
   <view class="device-container">
@@ -65,86 +65,67 @@
65 65
     </view>
66 66
 
67 67
     <!-- 设备列表 -->
68
-    <scroll-view :scroll-top="scrollTop" scroll-y="true" class="device-list" @scrolltoupper="upper"
69
-      @scrolltolower="lower" @scroll="scroll" :refresher-enabled="true" :refresher-triggered="isRefreshing"
70
-      @refresherrefresh="onRefresh" :lower-threshold="50" :upper-threshold="50">
71
-      <view class="content-wrapper">
72
-        <template v-if="deviceList.length > 0">
73
-          <view class="device-card" v-for="(item, index) in deviceList" :key="index" @click="handleViewDetail(item)">
74
-            <view class="device-header">
75
-              <view class="device-info">
76
-                <text class="device-name">{{ item.name }}</text>
77
-                <text class="device-brand">{{ item.brand }}</text>
78
-              </view>
79
-              <view class="device-status">
80
-                <uni-tag :text="statusTypeText(item.status)" :type="statusTypeStyle(item.status)" size="small" />
81
-              </view>
68
+    <mescroll-uni ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption"
69
+      :up="upOption" :fixed="false" :top="10" style="padding: 30rpx;">
70
+      <view>
71
+        <view v-for="(item, index) in deviceList" :key="index" class="device-card" @click="handleViewDetail(item)">
72
+          <view class="device-header">
73
+            <view class="device-info">
74
+              <text class="device-name">{{ item.name }}</text>
75
+              <text class="device-brand">{{ item.brand }}</text>
82 76
             </view>
83
-
84
-            <view class="device-content">
85
-              <view class="info-row">
86
-                <text class="info-label">设备编号:</text>
87
-                <text class="info-value">{{ item.deviceNumber || '暂无' }}</text>
88
-              </view>
89
-              <view class="info-row">
90
-                <text class="info-label">规格型号:</text>
91
-                <text class="info-value">{{ item.series || '暂无' }}</text>
92
-              </view>
93
-              <view class="info-row">
94
-                <text class="info-label">出厂编号:</text>
95
-                <text class="info-value">{{ item.code || '暂无' }}</text>
96
-              </view>
97
-              <view class="info-row">
98
-                <text class="info-label">购置时间:</text>
99
-                <text class="info-value">{{ formatDate(item.acquisitionTime) }}</text>
100
-              </view>
101
-              <view class="info-row">
102
-                <text class="info-label">购买价格:</text>
103
-                <text class="info-value price">¥{{ item.cost || '0' }}</text>
104
-              </view>
105
-              <view class="info-row">
106
-                <text class="info-label">单日成本:</text>
107
-                <text class="info-value cost">¥{{ item.dayCost || '0' }}</text>
108
-              </view>
77
+            <view class="device-status">
78
+              <uni-tag :text="statusTypeText(item.status)" :type="statusTypeStyle(item.status)" size="small" />
109 79
             </view>
80
+          </view>
110 81
 
111
-            <view class="device-actions">
112
-              <button class="action-btn view" @click.stop="handleViewDetail(item)">
113
-                <uni-icons type="eye" size="14" color="#007AFF"></uni-icons>
114
-                <text>查看</text>
115
-              </button>
116
-              <button class="action-btn edit" @click.stop="handleUpdate(item)" v-if="hasPermission('oa:device:edit')">
117
-                <uni-icons type="compose" size="14" color="#FF9500"></uni-icons>
118
-                <text>编辑</text>
119
-              </button>
120
-              <button class="action-btn delete" @click.stop="handleDelete(item)"
121
-                v-if="hasPermission('oa:device:remove')">
122
-                <uni-icons type="trash" size="14" color="#FF3B30"></uni-icons>
123
-                <text>删除</text>
124
-              </button>
82
+          <view class="device-content">
83
+            <view class="info-row">
84
+              <text class="info-label">设备编号:</text>
85
+              <text class="info-value">{{ item.deviceNumber || '暂无' }}</text>
86
+            </view>
87
+            <view class="info-row">
88
+              <text class="info-label">规格型号:</text>
89
+              <text class="info-value">{{ item.series || '暂无' }}</text>
90
+            </view>
91
+            <view class="info-row">
92
+              <text class="info-label">出厂编号:</text>
93
+              <text class="info-value">{{ item.code || '暂无' }}</text>
94
+            </view>
95
+            <view class="info-row">
96
+              <text class="info-label">购置时间:</text>
97
+              <text class="info-value">{{ formatDate(item.acquisitionTime) }}</text>
98
+            </view>
99
+            <view class="info-row">
100
+              <text class="info-label">购买价格:</text>
101
+              <text class="info-value price">¥{{ item.cost || '0' }}</text>
102
+            </view>
103
+            <view class="info-row">
104
+              <text class="info-label">单日成本:</text>
105
+              <text class="info-value cost">¥{{ item.dayCost || '0' }}</text>
125 106
             </view>
126 107
           </view>
127 108
 
128
-          <!-- 加载更多 -->
129
-          <view class="loading-more" v-if="hasMore">
130
-            <text class="loading-text">加载中...</text>
109
+          <view class="device-actions">
110
+            <button class="action-btn view" @click.stop="handleViewDetail(item)">
111
+              <uni-icons type="eye" size="14" color="#007AFF"></uni-icons>
112
+              <text>查看</text>
113
+            </button>
114
+            <button class="action-btn edit" @click.stop="handleUpdate(item)" v-show="checkPermi(['oa:device:edit'])">
115
+              <uni-icons type="compose" size="14" color="#FF9500"></uni-icons>
116
+              <text>编辑</text>
117
+            </button>
118
+            <button class="action-btn delete" @click.stop="handleDelete(item)" v-show="checkPermi(['oa:device:remove'])">
119
+              <uni-icons type="trash" size="14" color="#FF3B30"></uni-icons>
120
+              <text>删除</text>
121
+            </button>
131 122
           </view>
132
-          <view class="no-more" v-else>
133
-            <text class="no-more-text">没有更多数据了</text>
134
-          </view>
135
-        </template>
136
-
137
-        <!-- 空状态 -->
138
-        <view v-else class="empty-state">
139
-          <image src="/static/images/empty.png" mode="aspectFit" class="empty-image"></image>
140
-          <text class="empty-text">暂无设备数据</text>
141
-          <text class="empty-subtext">点击下方按钮添加设备</text>
142 123
         </view>
143 124
       </view>
144
-    </scroll-view>
125
+    </mescroll-uni>
145 126
 
146 127
     <!-- 悬浮按钮 -->
147
-    <view class="fab-button" @click="handleAdd" v-if="hasPermission('oa:device:add')">
128
+    <view class="fab-button" @click="handleAdd" v-if="checkPermi(['oa:device:add'])">
148 129
       <uni-icons type="plusempty" size="24" color="#fff"></uni-icons>
149 130
     </view>
150 131
 
@@ -297,15 +278,28 @@
297 278
 
298 279
 <script>
299 280
 import { listDevice, getDevice, delDevice, addDevice, updateDevice } from "@/api/oa/device/device";
281
+import { checkPermi, checkRole } from "@/utils/permission";
282
+import MescrollMixin from '@/uni_modules/mescroll/components/mescroll-uni/mescroll-mixins.js'
300 283
 
301 284
 export default {
302 285
   name: "InstrumentsList",
286
+  mixins: [MescrollMixin],
303 287
   data() {
304 288
     return {
305
-      // 滚动相关
306
-      scrollTop: 0,
307
-      isRefreshing: false,
308
-      hasMore: true,
289
+      // mescroll相关
290
+      mescroll: null, // mescroll实例
291
+      downOption: {
292
+        auto: true,
293
+        textOutOffset: '下拉刷新',
294
+        textLoading: '加载中...'
295
+      },
296
+      upOption: {
297
+        auto: false,
298
+        page: { num: 1, size: 20 },
299
+        noMoreSize: 5,
300
+        empty: { tip: '暂无设备数据' },
301
+        textNoMore: '~ 没有更多数据了 ~'
302
+      },
309 303
 
310 304
       // 筛选相关
311 305
       showFilter: false,
@@ -349,12 +343,55 @@ export default {
349 343
   },
350 344
 
351 345
   onLoad() {
352
-    this.getList();
353 346
     this.getDeptOptions();
354 347
   },
355 348
 
356 349
   methods: {
357
-    // 获取设备列表
350
+    checkPermi,
351
+    checkRole,
352
+
353
+    // 加载数据
354
+    async loadData() {
355
+      try {
356
+        const response = await listDevice(this.queryParams);
357
+        return { data: response.rows || [], total: response.total || 0 };
358
+      } catch (error) {
359
+        console.error('获取设备列表失败:', error);
360
+        return { data: [], error: true };
361
+      }
362
+    },
363
+
364
+    mescrollInit(mescroll) {
365
+      this.mescroll = mescroll;
366
+    },
367
+
368
+    // mescroll下拉刷新回调
369
+    async downCallback() {
370
+      this.deviceList = []; // 清空列表
371
+      this.queryParams.pageNum = 1;
372
+      const res = await this.loadData();
373
+      if (res.error) {
374
+        this.mescroll.endErr();
375
+      } else {
376
+        this.deviceList = res.data;
377
+        this.mescroll.endSuccess(res.data.length, res.total);
378
+      }
379
+      uni.stopPullDownRefresh();
380
+    },
381
+
382
+    // mescroll上拉加载回调
383
+    async upCallback(page) {
384
+      this.queryParams.pageNum = page.num;
385
+      const res = await this.loadData();
386
+      if (res.error) {
387
+        this.mescroll.endErr();
388
+      } else {
389
+        this.deviceList = this.deviceList.concat(res.data);
390
+        this.mescroll.endSuccess(res.data.length, res.total);
391
+      }
392
+    },
393
+
394
+    // 获取设备列表(保留原有方法,用于其他操作后刷新)
358 395
     async getList() {
359 396
       try {
360 397
         const response = await listDevice(this.queryParams);
@@ -364,7 +401,6 @@ export default {
364 401
           this.deviceList = [...this.deviceList, ...(response.rows || [])];
365 402
         }
366 403
         this.total = response.total || 0;
367
-        this.hasMore = this.deviceList.length < this.total;
368 404
       } catch (error) {
369 405
         console.error('获取设备列表失败:', error);
370 406
         uni.showToast({
@@ -395,7 +431,7 @@ export default {
395 431
 
396 432
     handleQuery() {
397 433
       this.queryParams.pageNum = 1;
398
-      this.getList();
434
+      this.downCallback();
399 435
     },
400 436
 
401 437
     clearSearch() {
@@ -407,29 +443,6 @@ export default {
407 443
       this.showFilter = !this.showFilter;
408 444
     },
409 445
 
410
-    // 滚动相关方法
411
-    upper() {
412
-      // 下拉刷新
413
-    },
414
-
415
-    lower() {
416
-      if (this.hasMore) {
417
-        this.queryParams.pageNum++;
418
-        this.getList();
419
-      }
420
-    },
421
-
422
-    scroll(e) {
423
-      this.scrollTop = e.detail.scrollTop;
424
-    },
425
-
426
-    async onRefresh() {
427
-      this.isRefreshing = true;
428
-      this.queryParams.pageNum = 1;
429
-      await this.getList();
430
-      this.isRefreshing = false;
431
-    },
432
-
433 446
     // 设备操作方法
434 447
     handleViewDetail(item) {
435 448
       this.currentDevice = item;
@@ -442,7 +455,6 @@ export default {
442 455
 
443 456
     handleViewLogs() {
444 457
       this.closeDetailPopup();
445
-
446 458
     },
447 459
 
448 460
     handleAdd() {
@@ -482,7 +494,7 @@ export default {
482 494
                 title: '删除成功',
483 495
                 icon: 'success'
484 496
               });
485
-              this.getList();
497
+              this.downCallback(); // 使用mescroll刷新
486 498
             } catch (error) {
487 499
               console.error('删除设备失败:', error);
488 500
               uni.showToast({
@@ -550,7 +562,7 @@ export default {
550 562
         }
551 563
 
552 564
         this.closeFormPopup();
553
-        this.getList();
565
+        this.downCallback(); // 使用mescroll刷新
554 566
       } catch (error) {
555 567
         console.error('保存设备失败:', error);
556 568
         uni.showToast({
@@ -697,14 +709,6 @@ export default {
697 709
   padding: 8px;
698 710
 }
699 711
 
700
-.device-list {
701
-  flex: 1;
702
-}
703
-
704
-.content-wrapper {
705
-  padding: 15px;
706
-}
707
-
708 712
 .device-card {
709 713
   background: #fff;
710 714
   border-radius: 12px;
@@ -802,41 +806,6 @@ export default {
802 806
   }
803 807
 }
804 808
 
805
-.loading-more,
806
-.no-more {
807
-  text-align: center;
808
-  padding: 20px;
809
-
810
-  .loading-text,
811
-  .no-more-text {
812
-    font-size: 14px;
813
-    color: #999;
814
-  }
815
-}
816
-
817
-.empty-state {
818
-  text-align: center;
819
-  padding: 60px 20px;
820
-
821
-  .empty-image {
822
-    width: 120px;
823
-    height: 120px;
824
-    margin-bottom: 20px;
825
-  }
826
-
827
-  .empty-text {
828
-    font-size: 16px;
829
-    color: #666;
830
-    display: block;
831
-    margin-bottom: 8px;
832
-  }
833
-
834
-  .empty-subtext {
835
-    font-size: 14px;
836
-    color: #999;
837
-  }
838
-}
839
-
840 809
 .fab-button {
841 810
   position: fixed;
842 811
   right: 20px;
@@ -925,37 +894,37 @@ export default {
925 894
       border-radius: 8px !important;
926 895
       background: #f8f8f8 !important;
927 896
       transition: all 0.3s ease;
928
-      
897
+
929 898
       &:focus-within {
930 899
         border-color: #667eea !important;
931 900
         background: #fff !important;
932 901
         box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.1) !important;
933 902
       }
934 903
     }
935
-    
904
+
936 905
     .uni-easyinput__content-input {
937 906
       padding: 0 12px !important;
938 907
       font-size: 14px !important;
939 908
       color: #333 !important;
940
-      
909
+
941 910
       &::placeholder {
942 911
         color: #999 !important;
943 912
       }
944 913
     }
945
-    
914
+
946 915
     // textarea 样式
947 916
     &.uni-easyinput--textarea {
948 917
       .uni-easyinput__content {
949 918
         height: auto !important;
950 919
         min-height: 80px !important;
951 920
       }
952
-      
921
+
953 922
       .uni-easyinput__content-textarea {
954 923
         padding: 12px !important;
955 924
         font-size: 14px !important;
956 925
         color: #333 !important;
957 926
         min-height: 60px !important;
958
-        
927
+
959 928
         &::placeholder {
960 929
           color: #999 !important;
961 930
         }

+ 96
- 0
oa-ui-app/pages/oa/staff/README.md Voir le fichier

@@ -0,0 +1,96 @@
1
+# 员工管理模块
2
+
3
+## 功能概述
4
+
5
+移动端员工管理模块提供了完整的员工信息管理功能,包括员工列表查看、详情查看、信息编辑等。
6
+
7
+## 页面结构
8
+
9
+### 1. 员工列表页面 (staffList.vue)
10
+- **功能**: 展示员工列表,支持搜索、筛选、分页加载
11
+- **主要特性**:
12
+  - 搜索栏:支持按姓名搜索
13
+  - 部门筛选:横向滚动的部门选择器
14
+  - 高级筛选:职称、职业资格、学历、状态等筛选条件
15
+  - 员工卡片:展示员工基本信息和头像
16
+  - 下拉刷新和上拉加载更多
17
+
18
+### 2. 员工详情页面 (staffDetail.vue)
19
+- **功能**: 展示员工的完整详细信息
20
+- **主要特性**:
21
+  - 头部信息:头像、姓名、部门、状态
22
+  - 基本信息:年龄、性别、手机、身份证等
23
+  - 工作信息:部门、职务、职称、职业资格、岗级等
24
+  - 教育背景:最高学历、初始学历相关信息
25
+  - 其他信息:家庭住址、紧急联系人等
26
+
27
+### 3. 员工编辑页面 (staffEdit.vue)
28
+- **功能**: 新增或编辑员工信息
29
+- **主要特性**:
30
+  - 表单验证:必填字段验证、格式验证
31
+  - 分类管理:基本信息、工作信息、教育背景、其他信息
32
+  - 多选支持:政治面貌、职业资格等多选字段
33
+  - 日期选择:入职时间、合同时间等日期字段
34
+
35
+## 技术特性
36
+
37
+### 移动端适配
38
+- 使用 uView UI 组件库
39
+- 响应式设计,适配不同屏幕尺寸
40
+- 触摸友好的交互设计
41
+
42
+### 数据管理
43
+- 使用 mescroll-uni 实现下拉刷新和上拉加载
44
+- 分页加载,优化性能
45
+- 本地缓存和状态管理
46
+
47
+### 用户体验
48
+- 加载状态提示
49
+- 错误处理和用户反馈
50
+- 流畅的页面切换动画
51
+
52
+## API 接口
53
+
54
+### 员工相关接口
55
+- `listUser`: 获取员工列表
56
+- `getUser`: 获取员工详情
57
+- `addUser`: 新增员工
58
+- `updateUser`: 更新员工信息
59
+- `delUser`: 删除员工
60
+
61
+### 部门相关接口
62
+- `deptTreeSelectNew`: 获取部门树结构
63
+
64
+## 使用说明
65
+
66
+### 1. 查看员工列表
67
+1. 进入员工列表页面
68
+2. 使用搜索栏按姓名搜索
69
+3. 点击部门标签筛选特定部门员工
70
+4. 点击筛选按钮进行高级筛选
71
+5. 下拉刷新或上拉加载更多
72
+
73
+### 2. 查看员工详情
74
+1. 在员工列表中点击员工卡片
75
+2. 查看员工的完整信息
76
+3. 点击"编辑信息"按钮进入编辑页面
77
+
78
+### 3. 编辑员工信息
79
+1. 在员工详情页面点击"编辑信息"
80
+2. 或直接进入编辑页面新增员工
81
+3. 填写表单信息
82
+4. 点击"保存"提交更改
83
+
84
+## 注意事项
85
+
86
+1. **权限控制**: 确保用户有相应的操作权限
87
+2. **数据验证**: 表单提交前会进行数据验证
88
+3. **网络处理**: 网络异常时会显示相应的错误提示
89
+4. **性能优化**: 大量数据时使用分页加载
90
+
91
+## 样式规范
92
+
93
+- 使用 SCSS 预处理器
94
+- 遵循移动端设计规范
95
+- 统一的颜色和字体规范
96
+- 响应式布局设计

+ 420
- 0
oa-ui-app/pages/oa/staff/staffDetail.vue Voir le fichier

@@ -0,0 +1,420 @@
1
+<template>
2
+  <view class="container">
3
+    <!-- 头部信息 -->
4
+    <view class="header-section">
5
+      <view class="avatar-section">
6
+        <uv-avatar 
7
+          :src="staffInfo.avatar || '/static/images/user.png'"
8
+          size="120"
9
+        />
10
+      </view>
11
+      <view class="basic-info">
12
+        <view class="name">{{ staffInfo.nickName }}</view>
13
+        <view class="dept">{{ staffInfo.dept?.deptName || '未分配部门' }}</view>
14
+        <view class="status">
15
+          <uv-tag 
16
+            :text="getStatusText(staffInfo.status)" 
17
+            :type="getStatusType(staffInfo.status)"
18
+          />
19
+        </view>
20
+      </view>
21
+    </view>
22
+
23
+    <!-- 基本信息 -->
24
+    <view class="info-section">
25
+      <view class="section-title">基本信息</view>
26
+      <view class="info-grid">
27
+        <view class="info-item">
28
+          <text class="label">姓名</text>
29
+          <text class="value">{{ staffInfo.nickName }}</text>
30
+        </view>
31
+        <view class="info-item">
32
+          <text class="label">年龄</text>
33
+          <text class="value">{{ String(getAgeByIdCard(staffInfo.idCard)).slice(0,2) }}岁</text>
34
+        </view>
35
+        <view class="info-item">
36
+          <text class="label">性别</text>
37
+          <text class="value">{{ staffInfo.sex == 0 ? '男' : '女' }}</text>
38
+        </view>
39
+        <view class="info-item">
40
+          <text class="label">手机号码</text>
41
+          <text class="value">{{ staffInfo.phonenumber || '未填写' }}</text>
42
+        </view>
43
+        <view class="info-item">
44
+          <text class="label">身份证</text>
45
+          <text class="value">{{ staffInfo.idCard || '未填写' }}</text>
46
+        </view>
47
+        <view class="info-item">
48
+          <text class="label">籍贯</text>
49
+          <text class="value">{{ staffInfo.nativePlace || '未填写' }}</text>
50
+        </view>
51
+        <view class="info-item">
52
+          <text class="label">民族</text>
53
+          <text class="value">{{ staffInfo.ethnic || '未填写' }}</text>
54
+        </view>
55
+        <view class="info-item">
56
+          <text class="label">政治面貌</text>
57
+          <text class="value">{{ getPoliticalText(staffInfo.politicalAffiliation) || '未填写' }}</text>
58
+        </view>
59
+      </view>
60
+    </view>
61
+
62
+    <!-- 工作信息 -->
63
+    <view class="info-section">
64
+      <view class="section-title">工作信息</view>
65
+      <view class="info-grid">
66
+        <view class="info-item">
67
+          <text class="label">部门</text>
68
+          <text class="value">{{ staffInfo.dept?.deptName || '未分配部门' }}</text>
69
+        </view>
70
+        <view class="info-item">
71
+          <text class="label">职务</text>
72
+          <text class="value">{{ staffInfo.postNames || '未填写' }}</text>
73
+        </view>
74
+        <view class="info-item">
75
+          <text class="label">职称</text>
76
+          <text class="value">{{ getTitleText(staffInfo.titles) || '未填写' }}</text>
77
+        </view>
78
+        <view class="info-item">
79
+          <text class="label">职业资格</text>
80
+          <text class="value">{{ getCertificateText(staffInfo.certificates) || '未填写' }}</text>
81
+        </view>
82
+        <view class="info-item">
83
+          <text class="label">岗级</text>
84
+          <text class="value">{{ getPostLevelText(staffInfo.postLevel, staffInfo.salaryLevel) || '未填写' }}</text>
85
+        </view>
86
+        <view class="info-item">
87
+          <text class="label">入职时间</text>
88
+          <text class="value">{{ formatDate(staffInfo.entryDate) || '未填写' }}</text>
89
+        </view>
90
+        <view class="info-item">
91
+          <text class="label">合同签订</text>
92
+          <text class="value">{{ formatDate(staffInfo.contractSign) || '未填写' }}</text>
93
+        </view>
94
+        <view class="info-item">
95
+          <text class="label">合同期满</text>
96
+          <text class="value">{{ formatDate(staffInfo.contractExpire) || '未填写' }}</text>
97
+        </view>
98
+      </view>
99
+    </view>
100
+
101
+    <!-- 教育背景 -->
102
+    <view class="info-section">
103
+      <view class="section-title">教育背景</view>
104
+      <view class="info-grid">
105
+        <view class="info-item">
106
+          <text class="label">最高学历</text>
107
+          <text class="value">{{ getDegreeText(staffInfo.degree) || '未填写' }}</text>
108
+        </view>
109
+        <view class="info-item">
110
+          <text class="label">最高学历专业</text>
111
+          <text class="value">{{ staffInfo.major || '未填写' }}</text>
112
+        </view>
113
+        <view class="info-item">
114
+          <text class="label">最高学历毕业院校</text>
115
+          <text class="value">{{ staffInfo.graduateSchool || '未填写' }}</text>
116
+        </view>
117
+        <view class="info-item">
118
+          <text class="label">初始学历</text>
119
+          <text class="value">{{ getDegreeText(staffInfo.initialDegree) || '未填写' }}</text>
120
+        </view>
121
+        <view class="info-item">
122
+          <text class="label">初始学历专业</text>
123
+          <text class="value">{{ staffInfo.initialMajor || '未填写' }}</text>
124
+        </view>
125
+        <view class="info-item">
126
+          <text class="label">初始学历毕业院校</text>
127
+          <text class="value">{{ staffInfo.initialSchool || '未填写' }}</text>
128
+        </view>
129
+      </view>
130
+    </view>
131
+
132
+    <!-- 其他信息 -->
133
+    <view class="info-section">
134
+      <view class="section-title">其他信息</view>
135
+      <view class="info-grid">
136
+        <view class="info-item">
137
+          <text class="label">家庭住址</text>
138
+          <text class="value">{{ staffInfo.homePlace || '未填写' }}</text>
139
+        </view>
140
+        <view class="info-item">
141
+          <text class="label">紧急联系人</text>
142
+          <text class="value">{{ staffInfo.contact || '未填写' }}</text>
143
+        </view>
144
+        <view class="info-item">
145
+          <text class="label">紧急联系电话</text>
146
+          <text class="value">{{ staffInfo.telephone || '未填写' }}</text>
147
+        </view>
148
+        <view class="info-item">
149
+          <text class="label">备注</text>
150
+          <text class="value">{{ staffInfo.remark || '无' }}</text>
151
+        </view>
152
+      </view>
153
+    </view>
154
+
155
+    <!-- 操作按钮 -->
156
+    <view class="action-section">
157
+      <uv-button type="primary" @click="handleEdit">编辑信息</uv-button>
158
+    </view>
159
+  </view>
160
+</template>
161
+
162
+<script>
163
+import { getUser } from "@/api/system/user";
164
+
165
+export default {
166
+  data() {
167
+    return {
168
+      staffInfo: {},
169
+      userId: '',
170
+      nickName: ''
171
+    }
172
+  },
173
+  
174
+  onLoad(options) {
175
+    this.userId = options.userId;
176
+    this.nickName = options.nickName;
177
+    this.getStaffInfo();
178
+  },
179
+  
180
+  methods: {
181
+    // 获取员工信息
182
+    async getStaffInfo() {
183
+      try {
184
+        const res = await getUser(this.userId);
185
+        this.staffInfo = res.data;
186
+      } catch (error) {
187
+        console.error('获取员工信息失败:', error);
188
+        uni.showToast({
189
+          title: '获取员工信息失败',
190
+          icon: 'none'
191
+        });
192
+      }
193
+    },
194
+    
195
+    // 获取状态文本
196
+    getStatusText(status) {
197
+      const statusMap = {
198
+        '0': '在职',
199
+        '1': '离职',
200
+        '2': '退休',
201
+        '3': '试用',
202
+        '4': '返聘'
203
+      };
204
+      return statusMap[status] || '未知';
205
+    },
206
+    
207
+    // 获取状态类型
208
+    getStatusType(status) {
209
+      const typeMap = {
210
+        '0': 'success',
211
+        '1': 'error',
212
+        '2': 'warning',
213
+        '3': 'primary',
214
+        '4': 'info'
215
+      };
216
+      return typeMap[status] || 'default';
217
+    },
218
+    
219
+    // 获取职称文本
220
+    getTitleText(titles) {
221
+      const titleMap = {
222
+        '1': '高级工程师',
223
+        '2': '工程师',
224
+        '3': '助理工程师',
225
+        '4': '技术员'
226
+      };
227
+      return titleMap[titles] || '';
228
+    },
229
+    
230
+    // 获取职业资格文本
231
+    getCertificateText(certificates) {
232
+      if (!certificates) return '';
233
+      const certificateMap = {
234
+        '1': '注册测绘师',
235
+        '2': '注册建筑师',
236
+        '3': '注册结构师',
237
+        '4': '注册造价师'
238
+      };
239
+      const certArray = certificates.split(',');
240
+      return certArray.map(cert => certificateMap[cert] || cert).join('、');
241
+    },
242
+    
243
+    // 获取学历文本
244
+    getDegreeText(degree) {
245
+      const degreeMap = {
246
+        '1': '博士',
247
+        '2': '硕士',
248
+        '3': '本科',
249
+        '4': '专科',
250
+        '5': '高中'
251
+      };
252
+      return degreeMap[degree] || '';
253
+    },
254
+    
255
+    // 获取政治面貌文本
256
+    getPoliticalText(politicalAffiliation) {
257
+      if (!politicalAffiliation) return '';
258
+      const politicalMap = {
259
+        '1': '中共党员',
260
+        '2': '中共预备党员',
261
+        '3': '共青团员',
262
+        '4': '民革党员',
263
+        '5': '民盟盟员',
264
+        '6': '民建会员',
265
+        '7': '民进会员',
266
+        '8': '农工党党员',
267
+        '9': '致公党党员',
268
+        '10': '九三学社社员',
269
+        '11': '台盟盟员',
270
+        '12': '无党派人士',
271
+        '13': '群众'
272
+      };
273
+      const politicalArray = politicalAffiliation.split(',');
274
+      return politicalArray.map(political => politicalMap[political] || political).join('、');
275
+    },
276
+    
277
+    // 获取岗级文本
278
+    getPostLevelText(postLevel, salaryLevel) {
279
+      const postLevelMap = {
280
+        '1': '一级',
281
+        '2': '二级',
282
+        '3': '三级',
283
+        '4': '四级',
284
+        '5': '五级'
285
+      };
286
+      const salaryLevelMap = {
287
+        '1': 'A',
288
+        '2': 'B',
289
+        '3': 'C',
290
+        '4': 'D',
291
+        '5': 'E'
292
+      };
293
+      const postText = postLevelMap[postLevel] || '';
294
+      const salaryText = salaryLevelMap[salaryLevel] || '';
295
+      return postText && salaryText ? `${postText}${salaryText}` : '';
296
+    },
297
+    
298
+    // 根据身份证计算年龄
299
+    getAgeByIdCard(idCard) {
300
+      if (!idCard) return 0;
301
+      const birthStr = idCard.substring(6, 14);
302
+      const birthYear = parseInt(birthStr.substring(0, 4), 10);
303
+      const birthMonth = parseInt(birthStr.substring(4, 6), 10) - 1;
304
+      const birthDay = parseInt(birthStr.substring(6, 8), 10);
305
+      const birthDate = new Date(birthYear, birthMonth, birthDay);
306
+      const now = new Date();
307
+      let age = now.getFullYear() - birthDate.getFullYear();
308
+      const monthDiff = now.getMonth() - birthDate.getMonth();
309
+      if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthDate.getDate())) {
310
+        age--;
311
+      }
312
+      return age;
313
+    },
314
+    
315
+    // 格式化日期
316
+    formatDate(dateStr) {
317
+      if (!dateStr) return '';
318
+      const date = new Date(dateStr);
319
+      return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
320
+    },
321
+    
322
+    // 编辑信息
323
+    handleEdit() {
324
+      uni.navigateTo({
325
+        url: `/pages/oa/staff/staffEdit?userId=${this.userId}`
326
+      });
327
+    }
328
+  }
329
+}
330
+</script>
331
+
332
+<style lang="scss" scoped>
333
+.container {
334
+  background-color: #f5f5f5;
335
+  min-height: 100vh;
336
+  padding-bottom: 120rpx;
337
+}
338
+
339
+.header-section {
340
+  background-color: #fff;
341
+  padding: 40rpx 30rpx;
342
+  display: flex;
343
+  align-items: center;
344
+  margin-bottom: 20rpx;
345
+  
346
+  .avatar-section {
347
+    margin-right: 30rpx;
348
+  }
349
+  
350
+  .basic-info {
351
+    flex: 1;
352
+    
353
+    .name {
354
+      font-size: 36rpx;
355
+      font-weight: bold;
356
+      color: #333;
357
+      margin-bottom: 10rpx;
358
+    }
359
+    
360
+    .dept {
361
+      font-size: 28rpx;
362
+      color: #666;
363
+      margin-bottom: 15rpx;
364
+    }
365
+    
366
+    .status {
367
+      display: inline-block;
368
+    }
369
+  }
370
+}
371
+
372
+.info-section {
373
+  background-color: #fff;
374
+  margin-bottom: 20rpx;
375
+  padding: 30rpx;
376
+  
377
+  .section-title {
378
+    font-size: 32rpx;
379
+    font-weight: bold;
380
+    color: #333;
381
+    margin-bottom: 30rpx;
382
+    border-left: 6rpx solid #007aff;
383
+    padding-left: 20rpx;
384
+  }
385
+  
386
+  .info-grid {
387
+    .info-item {
388
+      display: flex;
389
+      margin-bottom: 25rpx;
390
+      
391
+      .label {
392
+        width: 200rpx;
393
+        font-size: 28rpx;
394
+        color: #666;
395
+      }
396
+      
397
+      .value {
398
+        flex: 1;
399
+        font-size: 28rpx;
400
+        color: #333;
401
+        word-break: break-all;
402
+      }
403
+    }
404
+  }
405
+}
406
+
407
+.action-section {
408
+  position: fixed;
409
+  bottom: 0;
410
+  left: 0;
411
+  right: 0;
412
+  background-color: #fff;
413
+  padding: 20rpx 30rpx;
414
+  border-top: 1rpx solid #eee;
415
+  
416
+  .uv-button {
417
+    width: 100%;
418
+  }
419
+}
420
+</style>

+ 419
- 0
oa-ui-app/pages/oa/staff/staffEdit.vue Voir le fichier

@@ -0,0 +1,419 @@
1
+<template>
2
+  <view class="container">
3
+    <uv-form ref="form" :model="form" :rules="rules" label-width="200rpx">
4
+      <!-- 基本信息 -->
5
+      <view class="form-section">
6
+        <view class="section-title">基本信息</view>
7
+        <view class="form-item">
8
+          <uv-form-item label="姓名" prop="nickName">
9
+            <uv-input v-model="form.nickName" placeholder="请输入姓名" />
10
+          </uv-form-item>
11
+        </view>
12
+        <view class="form-item">
13
+          <uv-form-item label="性别">
14
+            <uv-radio-group v-model="form.sex">
15
+              <uv-radio label="0">男</uv-radio>
16
+              <uv-radio label="1">女</uv-radio>
17
+            </uv-radio-group>
18
+          </uv-form-item>
19
+        </view>
20
+        <view class="form-item">
21
+          <uv-form-item label="手机号码" prop="phonenumber">
22
+            <uv-input v-model="form.phonenumber" placeholder="请输入手机号码" />
23
+          </uv-form-item>
24
+        </view>
25
+        <view class="form-item">
26
+          <uv-form-item label="身份证" prop="idCard">
27
+            <uv-input v-model="form.idCard" placeholder="请输入身份证号码" />
28
+          </uv-form-item>
29
+        </view>
30
+        <view class="form-item">
31
+          <uv-form-item label="籍贯">
32
+            <uv-input v-model="form.nativePlace" placeholder="请输入籍贯" />
33
+          </uv-form-item>
34
+        </view>
35
+        <view class="form-item">
36
+          <uv-form-item label="民族">
37
+            <uv-input v-model="form.ethnic" placeholder="请输入民族" />
38
+          </uv-form-item>
39
+        </view>
40
+        <view class="form-item">
41
+          <uv-form-item label="政治面貌">
42
+            <uv-select v-model="form.politicalAffiliation" :options="politicalOptions" placeholder="请选择政治面貌" multiple />
43
+          </uv-form-item>
44
+        </view>
45
+      </view>
46
+
47
+      <!-- 工作信息 -->
48
+      <view class="form-section">
49
+        <view class="section-title">工作信息</view>
50
+        <view class="form-item">
51
+          <uv-form-item label="归属部门" prop="deptId">
52
+            <uv-select v-model="form.deptId" :options="deptOptions" placeholder="请选择归属部门" />
53
+          </uv-form-item>
54
+        </view>
55
+        <view class="form-item">
56
+          <uv-form-item label="入职时间">
57
+            <uv-datetime-picker v-model="form.entryDate" mode="date" placeholder="请选择入职时间" />
58
+          </uv-form-item>
59
+        </view>
60
+        <view class="form-item">
61
+          <uv-form-item label="合同签订">
62
+            <uv-datetime-picker v-model="form.contractSign" mode="date" placeholder="请选择合同签订时间" />
63
+          </uv-form-item>
64
+        </view>
65
+        <view class="form-item">
66
+          <uv-form-item label="合同期满">
67
+            <uv-datetime-picker v-model="form.contractExpire" mode="date" placeholder="请选择合同期满时间" />
68
+          </uv-form-item>
69
+        </view>
70
+        <view class="form-item">
71
+          <uv-form-item label="技术职称">
72
+            <uv-select v-model="form.titles" :options="titleOptions" placeholder="请选择技术职称" />
73
+          </uv-form-item>
74
+        </view>
75
+        <view class="form-item">
76
+          <uv-form-item label="职称专业">
77
+            <uv-input v-model="form.titleProfession" placeholder="请输入职称专业" />
78
+          </uv-form-item>
79
+        </view>
80
+        <view class="form-item">
81
+          <uv-form-item label="职业资格">
82
+            <uv-select v-model="form.certificates" :options="certificateOptions" placeholder="请选择职业资格" multiple />
83
+          </uv-form-item>
84
+        </view>
85
+        <view class="form-item">
86
+          <uv-form-item label="岗级">
87
+            <view class="level-group">
88
+              <uv-select v-model="form.postLevel" :options="postLevelOptions" placeholder="请选择岗级" />
89
+              <uv-select v-model="form.salaryLevel" :options="salaryLevelOptions" placeholder="请选择薪级" />
90
+            </view>
91
+          </uv-form-item>
92
+        </view>
93
+        <view class="form-item">
94
+          <uv-form-item label="状态">
95
+            <uv-radio-group v-model="form.status">
96
+              <uv-radio label="0">在职</uv-radio>
97
+              <uv-radio label="1">离职</uv-radio>
98
+              <uv-radio label="2">退休</uv-radio>
99
+              <uv-radio label="3">试用</uv-radio>
100
+              <uv-radio label="4">返聘</uv-radio>
101
+            </uv-radio-group>
102
+          </uv-form-item>
103
+        </view>
104
+      </view>
105
+
106
+      <!-- 教育背景 -->
107
+      <view class="form-section">
108
+        <view class="section-title">教育背景</view>
109
+        <view class="form-item">
110
+          <uv-form-item label="最高学历">
111
+            <uv-select v-model="form.degree" :options="degreeOptions" placeholder="请选择最高学历" />
112
+          </uv-form-item>
113
+        </view>
114
+        <view class="form-item">
115
+          <uv-form-item label="最高学历专业">
116
+            <uv-input v-model="form.major" placeholder="请输入最高学历专业" />
117
+          </uv-form-item>
118
+        </view>
119
+        <view class="form-item">
120
+          <uv-form-item label="最高学历毕业学校">
121
+            <uv-input v-model="form.graduateSchool" placeholder="请输入最高学历毕业学校" />
122
+          </uv-form-item>
123
+        </view>
124
+        <view class="form-item">
125
+          <uv-form-item label="初始学历">
126
+            <uv-select v-model="form.initialDegree" :options="degreeOptions" placeholder="请选择初始学历" />
127
+          </uv-form-item>
128
+        </view>
129
+        <view class="form-item">
130
+          <uv-form-item label="初始学历专业">
131
+            <uv-input v-model="form.initialMajor" placeholder="请输入初始学历专业" />
132
+          </uv-form-item>
133
+        </view>
134
+        <view class="form-item">
135
+          <uv-form-item label="初始学历毕业学校">
136
+            <uv-input v-model="form.initialSchool" placeholder="请输入初始学历毕业学校" />
137
+          </uv-form-item>
138
+        </view>
139
+      </view>
140
+
141
+      <!-- 其他信息 -->
142
+      <view class="form-section">
143
+        <view class="section-title">其他信息</view>
144
+        <view class="form-item">
145
+          <uv-form-item label="家庭住址">
146
+            <uv-textarea v-model="form.homePlace" placeholder="请输入家庭住址" />
147
+          </uv-form-item>
148
+        </view>
149
+        <view class="form-item">
150
+          <uv-form-item label="紧急联系人">
151
+            <uv-input v-model="form.contact" placeholder="请输入紧急联系人" />
152
+          </uv-form-item>
153
+        </view>
154
+        <view class="form-item">
155
+          <uv-form-item label="紧急联系电话">
156
+            <uv-input v-model="form.telephone" placeholder="请输入紧急联系电话" />
157
+          </uv-form-item>
158
+        </view>
159
+        <view class="form-item">
160
+          <uv-form-item label="备注">
161
+            <uv-textarea v-model="form.remark" placeholder="请输入备注信息" />
162
+          </uv-form-item>
163
+        </view>
164
+      </view>
165
+    </uv-form>
166
+
167
+    <!-- 操作按钮 -->
168
+    <view class="action-section">
169
+      <uv-button type="default" @click="handleCancel">取消</uv-button>
170
+      <uv-button type="primary" @click="handleSubmit">保存</uv-button>
171
+    </view>
172
+  </view>
173
+</template>
174
+
175
+<script>
176
+import { getUser, updateUser, addUser } from "@/api/system/user";
177
+import { deptTreeSelectNew } from "@/api/system/dept";
178
+
179
+export default {
180
+  data() {
181
+    return {
182
+      form: {
183
+        userId: undefined,
184
+        nickName: '',
185
+        sex: '0',
186
+        phonenumber: '',
187
+        idCard: '',
188
+        nativePlace: '',
189
+        ethnic: '',
190
+        politicalAffiliation: [],
191
+        deptId: undefined,
192
+        entryDate: '',
193
+        contractSign: '',
194
+        contractExpire: '',
195
+        titles: '',
196
+        titleProfession: '',
197
+        certificates: [],
198
+        postLevel: '',
199
+        salaryLevel: '',
200
+        status: '0',
201
+        degree: '',
202
+        major: '',
203
+        graduateSchool: '',
204
+        initialDegree: '',
205
+        initialMajor: '',
206
+        initialSchool: '',
207
+        homePlace: '',
208
+        contact: '',
209
+        telephone: '',
210
+        remark: ''
211
+      },
212
+      deptOptions: [],
213
+      politicalOptions: [
214
+        { label: '中共党员', value: '1' },
215
+        { label: '中共预备党员', value: '2' },
216
+        { label: '共青团员', value: '3' },
217
+        { label: '民革党员', value: '4' },
218
+        { label: '民盟盟员', value: '5' },
219
+        { label: '民建会员', value: '6' },
220
+        { label: '民进会员', value: '7' },
221
+        { label: '农工党党员', value: '8' },
222
+        { label: '致公党党员', value: '9' },
223
+        { label: '九三学社社员', value: '10' },
224
+        { label: '台盟盟员', value: '11' },
225
+        { label: '无党派人士', value: '12' },
226
+        { label: '群众', value: '13' }
227
+      ],
228
+      titleOptions: [
229
+        { label: '高级工程师', value: '1' },
230
+        { label: '工程师', value: '2' },
231
+        { label: '助理工程师', value: '3' },
232
+        { label: '技术员', value: '4' }
233
+      ],
234
+      certificateOptions: [
235
+        { label: '注册测绘师', value: '1' },
236
+        { label: '注册建筑师', value: '2' },
237
+        { label: '注册结构师', value: '3' },
238
+        { label: '注册造价师', value: '4' }
239
+      ],
240
+      degreeOptions: [
241
+        { label: '博士', value: '1' },
242
+        { label: '硕士', value: '2' },
243
+        { label: '本科', value: '3' },
244
+        { label: '专科', value: '4' },
245
+        { label: '高中', value: '5' }
246
+      ],
247
+      postLevelOptions: [
248
+        { label: '一级', value: '1' },
249
+        { label: '二级', value: '2' },
250
+        { label: '三级', value: '3' },
251
+        { label: '四级', value: '4' },
252
+        { label: '五级', value: '5' }
253
+      ],
254
+      salaryLevelOptions: [
255
+        { label: 'A', value: '1' },
256
+        { label: 'B', value: '2' },
257
+        { label: 'C', value: '3' },
258
+        { label: 'D', value: '4' },
259
+        { label: 'E', value: '5' }
260
+      ],
261
+      rules: {
262
+        nickName: [
263
+          { required: true, message: '请输入姓名', trigger: 'blur' }
264
+        ],
265
+        phonenumber: [
266
+          { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: '请输入正确的手机号码', trigger: 'blur' }
267
+        ],
268
+        idCard: [
269
+          { pattern: /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/, message: '请输入正确的身份证号码', trigger: 'blur' }
270
+        ],
271
+        deptId: [
272
+          { required: true, message: '请选择归属部门', trigger: 'change' }
273
+        ]
274
+      }
275
+    }
276
+  },
277
+  
278
+  onLoad(options) {
279
+    this.getDeptTree();
280
+    if (options.userId) {
281
+      this.getStaffInfo(options.userId);
282
+    }
283
+  },
284
+  
285
+  methods: {
286
+    // 获取部门树
287
+    async getDeptTree() {
288
+      try {
289
+        const res = await deptTreeSelectNew();
290
+        this.deptOptions = res.data || [];
291
+      } catch (error) {
292
+        console.error('获取部门树失败:', error);
293
+      }
294
+    },
295
+    
296
+    // 获取员工信息
297
+    async getStaffInfo(userId) {
298
+      try {
299
+        const res = await getUser(userId);
300
+        this.form = { ...this.form, ...res.data };
301
+        
302
+        // 处理政治面貌数组
303
+        if (this.form.politicalAffiliation) {
304
+          this.form.politicalAffiliation = this.form.politicalAffiliation.split(',');
305
+        }
306
+        
307
+        // 处理职业资格数组
308
+        if (this.form.certificates) {
309
+          this.form.certificates = this.form.certificates.split(',');
310
+        }
311
+      } catch (error) {
312
+        console.error('获取员工信息失败:', error);
313
+        uni.showToast({
314
+          title: '获取员工信息失败',
315
+          icon: 'none'
316
+        });
317
+      }
318
+    },
319
+    
320
+    // 提交表单
321
+    handleSubmit() {
322
+      this.$refs.form.validate(async (valid) => {
323
+        if (valid) {
324
+          try {
325
+            // 处理数组字段
326
+            const submitData = { ...this.form };
327
+            submitData.politicalAffiliation = this.form.politicalAffiliation.join(',');
328
+            submitData.certificates = this.form.certificates.join(',');
329
+            
330
+            if (this.form.userId) {
331
+              // 更新
332
+              await updateUser(submitData);
333
+              uni.showToast({
334
+                title: '更新成功',
335
+                icon: 'success'
336
+              });
337
+            } else {
338
+              // 新增
339
+              await addUser(submitData);
340
+              uni.showToast({
341
+                title: '添加成功',
342
+                icon: 'success'
343
+              });
344
+            }
345
+            
346
+            // 返回上一页
347
+            setTimeout(() => {
348
+              uni.navigateBack();
349
+            }, 1500);
350
+          } catch (error) {
351
+            console.error('保存失败:', error);
352
+            uni.showToast({
353
+              title: '保存失败',
354
+              icon: 'none'
355
+            });
356
+          }
357
+        }
358
+      });
359
+    },
360
+    
361
+    // 取消
362
+    handleCancel() {
363
+      uni.navigateBack();
364
+    }
365
+  }
366
+}
367
+</script>
368
+
369
+<style lang="scss" scoped>
370
+.container {
371
+  background-color: #f5f5f5;
372
+  min-height: 100vh;
373
+  padding-bottom: 120rpx;
374
+}
375
+
376
+.form-section {
377
+  background-color: #fff;
378
+  margin-bottom: 20rpx;
379
+  padding: 30rpx;
380
+  
381
+  .section-title {
382
+    font-size: 32rpx;
383
+    font-weight: bold;
384
+    color: #333;
385
+    margin-bottom: 30rpx;
386
+    border-left: 6rpx solid #007aff;
387
+    padding-left: 20rpx;
388
+  }
389
+  
390
+  .form-item {
391
+    margin-bottom: 30rpx;
392
+  }
393
+  
394
+  .level-group {
395
+    display: flex;
396
+    gap: 20rpx;
397
+    
398
+    .uv-select {
399
+      flex: 1;
400
+    }
401
+  }
402
+}
403
+
404
+.action-section {
405
+  position: fixed;
406
+  bottom: 0;
407
+  left: 0;
408
+  right: 0;
409
+  background-color: #fff;
410
+  padding: 20rpx 30rpx;
411
+  border-top: 1rpx solid #eee;
412
+  display: flex;
413
+  gap: 20rpx;
414
+  
415
+  .uv-button {
416
+    flex: 1;
417
+  }
418
+}
419
+</style>

+ 734
- 0
oa-ui-app/pages/oa/staff/staffList.vue Voir le fichier

@@ -0,0 +1,734 @@
1
+<template>
2
+  <view class="container">
3
+    <!-- 搜索栏 -->
4
+    <view class="search-bar">
5
+      <view class="search-input">
6
+        <input class="search-input-field" v-model="queryParams.nickName" placeholder="请输入姓名搜索" @input="handleQuery"
7
+          @confirm="handleQuery" />
8
+      </view>
9
+      <view class="filter-btn" @click="showFilter = true">
10
+        <text class="filter-icon">筛选</text>
11
+      </view>
12
+    </view>
13
+
14
+    <!-- 部门选择 -->
15
+    <view class="dept-section" v-if="deptOptions.length > 0">
16
+      <view class="dept-header">
17
+        <text class="dept-title">部门</text>
18
+        <text class="dept-name">{{ currentDeptName || '全部部门' }}</text>
19
+      </view>
20
+      <scroll-view class="dept-scroll" scroll-x>
21
+        <view class="dept-list">
22
+          <view class="dept-item" :class="{ active: !queryParams.deptId }" @click="selectDept(null, '全部部门')">
23
+            全部
24
+          </view>
25
+          <view v-for="dept in deptOptions" :key="dept.id" class="dept-item"
26
+            :class="{ active: queryParams.deptId === dept.id }" @click="selectDept(dept.id, dept.label)">
27
+            {{ dept.label }}
28
+          </view>
29
+        </view>
30
+      </scroll-view>
31
+    </view>
32
+
33
+    <!-- 状态选择 -->
34
+    <view class="status-section">
35
+      <view class="status-header">
36
+        <text class="status-title">状态</text>
37
+        <text class="status-name">{{ getStatusLabel(queryParams.status) || '在职' }}</text>
38
+      </view>
39
+      <scroll-view class="status-scroll" scroll-x>
40
+        <view class="status-list">
41
+          <view class="status-item" :class="{ active: queryParams.status === '0' }" @click="selectStatus('0')">
42
+            在职
43
+          </view>
44
+          <view class="status-item" :class="{ active: queryParams.status === '1' }" @click="selectStatus('1')">
45
+            离职
46
+          </view>
47
+          <view class="status-item" :class="{ active: queryParams.status === '2' }" @click="selectStatus('2')">
48
+            退休
49
+          </view>
50
+        </view>
51
+      </scroll-view>
52
+    </view>
53
+
54
+    <!-- 筛选弹窗 -->
55
+    <view v-if="showFilter" class="filter-overlay" @click="showFilter = false">
56
+      <view class="filter-popup" @click.stop>
57
+        <view class="filter-header">
58
+          <text class="filter-title">筛选条件</text>
59
+          <view class="filter-close" @click="showFilter = false">
60
+            <text>✕</text>
61
+          </view>
62
+        </view>
63
+        <view class="filter-content">
64
+          <view class="filter-item">
65
+            <text class="filter-label">职称</text>
66
+            <picker v-model="queryParams.titles" :range="titleOptions" range-key="label" @change="onTitleChange">
67
+              <view class="picker-view">
68
+                {{ getTitleLabel(queryParams.titles) || '请选择职称' }}
69
+              </view>
70
+            </picker>
71
+          </view>
72
+          <view class="filter-item">
73
+            <text class="filter-label">状态</text>
74
+            <picker v-model="queryParams.status" :range="statusOptions" range-key="label" @change="onStatusChange">
75
+              <view class="picker-view">
76
+                {{ getStatusLabel(queryParams.status) || '请选择状态' }}
77
+              </view>
78
+            </picker>
79
+          </view>
80
+        </view>
81
+        <view class="filter-actions">
82
+          <button class="btn-default" @click="resetQuery">重置</button>
83
+          <button class="btn-primary" @click="applyFilter">确定</button>
84
+        </view>
85
+      </view>
86
+    </view>
87
+
88
+    <!-- 员工列表 -->
89
+    <mescroll-uni ref="mescrollRef" @init="mescrollInit" @down="downCallback" @up="upCallback" :down="downOption"
90
+      :up="upOption" :fixed="false" :top="10" style="padding: 30rpx;">
91
+      <view class="staff-list">
92
+        <view v-for="(item, index) in staffList" :key="index" class="staff-card" @click="handleView(item)">
93
+          <view class="staff-header">
94
+            <view class="staff-avatar">
95
+              <image class="avatar-image" :src="item.avatar || '/static/images/user.png'" mode="aspectFill" />
96
+            </view>
97
+            <view class="staff-info">
98
+              <view class="staff-name">{{ item.nickName }}</view>
99
+              <view class="staff-dept">{{ (item.dept && item.dept.deptName) || '未分配部门' }}</view>
100
+              <view class="staff-status">
101
+                <text class="status-tag" :class="'status-' + item.status">
102
+                  {{ getStatusText(item.status) }}
103
+                </text>
104
+              </view>
105
+            </view>
106
+            <view class="staff-actions">
107
+              <text class="arrow-icon">></text>
108
+            </view>
109
+          </view>
110
+
111
+          <view class="staff-details">
112
+            <view class="detail-item">
113
+              <text class="detail-label">年龄:</text>
114
+              <text class="detail-value">{{ String(getAgeByIdCard(item.idCard)).slice(0, 2) }}岁</text>
115
+            </view>
116
+            <view class="detail-item">
117
+              <text class="detail-label">性别:</text>
118
+              <text class="detail-value">{{ item.sex == 0 ? '男' : '女' }}</text>
119
+            </view>
120
+            <view class="detail-item">
121
+              <text class="detail-label">手机:</text>
122
+              <text class="detail-value">{{ item.phonenumber || '未填写' }}</text>
123
+            </view>
124
+            <view class="detail-item">
125
+              <text class="detail-label">职称:</text>
126
+              <text class="detail-value">{{ getTitleText(item.titles) || '未填写' }}</text>
127
+            </view>
128
+            <view class="detail-item">
129
+              <text class="detail-label">职务:</text>
130
+              <text class="detail-value">{{ item.postNames || '未填写' }}</text>
131
+            </view>
132
+            <view class="detail-item">
133
+              <text class="detail-label">入职时间:</text>
134
+              <text class="detail-value">{{ formatDate(item.entryDate) || '未填写' }}</text>
135
+            </view>
136
+          </view>
137
+        </view>
138
+      </view>
139
+    </mescroll-uni>
140
+  </view>
141
+</template>
142
+
143
+<script>
144
+import { listUser, getUser, delUser, addUser, updateUser, resetUserPwd, changeUserStatus } from "@/api/system/user";
145
+import { deptTreeSelectNew } from "@/api/system/dept";
146
+import MescrollMixin from '@/uni_modules/mescroll/components/mescroll-uni/mescroll-mixins.js';
147
+
148
+export default {
149
+  mixins: [MescrollMixin],
150
+  data() {
151
+    return {
152
+      mescroll: null, // mescroll实例
153
+      downOption: {
154
+        auto: true,
155
+        textOutOffset: '下拉刷新',
156
+        textLoading: '加载中...'
157
+      },
158
+      upOption: {
159
+        auto: false,
160
+        page: { num: 1, size: 10 },
161
+        noMoreSize: 5,
162
+        empty: { tip: '暂无更多员工数据' },
163
+        textNoMore: '~ 没有更多数据了 ~'
164
+      },
165
+      staffList: [],
166
+      deptOptions: [],
167
+      currentDeptName: '',
168
+      showFilter: false,
169
+      queryParams: {
170
+        pageNum: 1,
171
+        pageSize: 10,
172
+        nickName: '',
173
+        titles: '',
174
+        certificates: '',
175
+        degree: '',
176
+        status: '0',
177
+        deptId: undefined
178
+      },
179
+      // 筛选选项
180
+      titleOptions: [
181
+        { label: '高级工程师', value: '1' },
182
+        { label: '工程师', value: '2' },
183
+        { label: '助理工程师', value: '3' },
184
+        { label: '技术员', value: '4' }
185
+      ],
186
+      certificateOptions: [
187
+        { label: '注册测绘师', value: '1' },
188
+        { label: '注册建筑师', value: '2' },
189
+        { label: '注册结构师', value: '3' },
190
+        { label: '注册造价师', value: '4' }
191
+      ],
192
+      degreeOptions: [
193
+        { label: '博士', value: '1' },
194
+        { label: '硕士', value: '2' },
195
+        { label: '本科', value: '3' },
196
+        { label: '专科', value: '4' },
197
+        { label: '高中', value: '5' }
198
+      ],
199
+      statusOptions: [
200
+        { label: '在职', value: '0' },
201
+        { label: '离职', value: '1' },
202
+        { label: '退休', value: '2' },
203
+        { label: '试用', value: '3' },
204
+        { label: '返聘', value: '4' }
205
+      ]
206
+    }
207
+  },
208
+  onLoad: function (options) {
209
+    this.getDeptTree();
210
+    uni.startPullDownRefresh();
211
+  },
212
+
213
+  methods: {
214
+    mescrollInit(mescroll) {
215
+      this.mescroll = mescroll;
216
+    },
217
+
218
+    // 获取部门树
219
+    async getDeptTree() {
220
+      try {
221
+        const res = await deptTreeSelectNew();
222
+        this.deptOptions = res.data[0].children || [];
223
+      } catch (error) {
224
+        console.error('获取部门树失败:', error);
225
+      }
226
+    },
227
+
228
+    // 选择部门
229
+    selectDept(deptId, deptName) {
230
+      this.queryParams.deptId = deptId;
231
+      this.currentDeptName = deptName;
232
+      this.handleQuery();
233
+    },
234
+
235
+    // 选择状态
236
+    selectStatus(status) {
237
+      this.queryParams.status = status;
238
+      this.handleQuery();
239
+    },
240
+
241
+    // 搜索
242
+    handleQuery() {
243
+      this.queryParams.pageNum = 1;
244
+      this.staffList = [];
245
+      this.mescroll.resetUpScroll();
246
+    },
247
+
248
+    // 重置查询
249
+    resetQuery() {
250
+      this.queryParams = {
251
+        pageNum: 1,
252
+        pageSize: 10,
253
+        nickName: '',
254
+        titles: '',
255
+        certificates: '',
256
+        degree: '',
257
+        status: '0', // 默认选中在职
258
+        deptId: undefined
259
+      };
260
+      this.currentDeptName = '';
261
+      this.showFilter = false;
262
+      this.handleQuery();
263
+    },
264
+
265
+    // 应用筛选
266
+    applyFilter() {
267
+      this.showFilter = false;
268
+      this.handleQuery();
269
+    },
270
+
271
+    // 处理职称选择变化
272
+    onTitleChange(e) {
273
+      const option = this.titleOptions[e.detail.value];
274
+      this.queryParams.titles = option ? option.value : '';
275
+    },
276
+
277
+    // 处理状态选择变化
278
+    onStatusChange(e) {
279
+      const option = this.statusOptions[e.detail.value];
280
+      this.queryParams.status = option ? option.value : '';
281
+    },
282
+
283
+    // 获取职称标签
284
+    getTitleLabel(value) {
285
+      const item = this.titleOptions.find(item => item.value === value);
286
+      return item ? item.label : '';
287
+    },
288
+
289
+    // 获取状态标签
290
+    getStatusLabel(value) {
291
+      const item = this.statusOptions.find(item => item.value === value);
292
+      return item ? item.label : '';
293
+    },
294
+
295
+    // 加载数据
296
+    async loadData() {
297
+      try {
298
+        const res = await listUser(this.queryParams);
299
+        return { data: res.rows, total: res.total };
300
+      } catch (e) {
301
+        console.error('加载员工数据失败:', e);
302
+        return { data: [], error: true };
303
+      }
304
+    },
305
+
306
+    // mescroll下拉刷新回调
307
+    async downCallback() {
308
+      this.staffList = []; // 清空列表
309
+      this.queryParams.pageNum = 1;
310
+      const res = await this.loadData();
311
+      if (res.error) {
312
+        this.mescroll.endErr();
313
+      } else {
314
+        this.staffList = res.data;
315
+        this.mescroll.endSuccess(res.data.length, res.total);
316
+      }
317
+      uni.stopPullDownRefresh();
318
+    },
319
+
320
+    // mescroll上拉加载回调
321
+    async upCallback(page) {
322
+      this.queryParams.pageNum = page.num;
323
+      const res = await this.loadData();
324
+      if (res.error) {
325
+        this.mescroll.endErr();
326
+      } else {
327
+        this.staffList = this.staffList.concat(res.data);
328
+        this.mescroll.endSuccess(res.data.length, res.total);
329
+      }
330
+    },
331
+
332
+    // 查看员工详情
333
+    handleView(item) {
334
+      uni.navigateTo({
335
+        url: `/pages/oa/staff/staffDetail?userId=${item.userId}&nickName=${item.nickName}`
336
+      });
337
+    },
338
+
339
+    // 获取状态文本
340
+    getStatusText(status) {
341
+      const statusMap = {
342
+        '0': '在职',
343
+        '1': '离职',
344
+        '2': '退休',
345
+        '3': '试用',
346
+        '4': '返聘'
347
+      };
348
+      return statusMap[status] || '未知';
349
+    },
350
+
351
+    // 获取状态类型
352
+    getStatusType(status) {
353
+      const typeMap = {
354
+        '0': 'success',
355
+        '1': 'error',
356
+        '2': 'warning',
357
+        '3': 'primary',
358
+        '4': 'info'
359
+      };
360
+      return typeMap[status] || 'default';
361
+    },
362
+
363
+    // 获取职称文本
364
+    getTitleText(titles) {
365
+      const titleMap = {
366
+        '1': '高级工程师',
367
+        '2': '工程师',
368
+        '3': '助理工程师',
369
+        '4': '技术员'
370
+      };
371
+      return titleMap[titles] || '';
372
+    },
373
+
374
+    // 根据身份证计算年龄
375
+    getAgeByIdCard(idCard) {
376
+      if (!idCard) return 0;
377
+      const birthStr = idCard.substring(6, 14);
378
+      const birthYear = parseInt(birthStr.substring(0, 4), 10);
379
+      const birthMonth = parseInt(birthStr.substring(4, 6), 10) - 1;
380
+      const birthDay = parseInt(birthStr.substring(6, 8), 10);
381
+      const birthDate = new Date(birthYear, birthMonth, birthDay);
382
+      const now = new Date();
383
+      let age = now.getFullYear() - birthDate.getFullYear();
384
+      const monthDiff = now.getMonth() - birthDate.getMonth();
385
+      if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birthDate.getDate())) {
386
+        age--;
387
+      }
388
+      return age;
389
+    },
390
+
391
+    // 格式化日期
392
+    formatDate(dateStr) {
393
+      if (!dateStr) return '';
394
+      const date = new Date(dateStr);
395
+      return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
396
+    }
397
+  },
398
+}
399
+</script>
400
+
401
+<style lang="scss" scoped>
402
+.container {
403
+  padding: 10rpx;
404
+  background-color: #f5f5f5;
405
+  height: 100vh;
406
+  display: flex;
407
+  flex-direction: column;
408
+}
409
+
410
+.search-bar {
411
+  display: flex;
412
+  align-items: center;
413
+  padding: 20rpx 30rpx;
414
+  background-color: #fff;
415
+  border-bottom: 1rpx solid #eee;
416
+
417
+  .search-input {
418
+    flex: 1;
419
+    margin-right: 20rpx;
420
+
421
+    .search-input-field {
422
+      width: 100%;
423
+      height: 60rpx;
424
+      padding: 0 20rpx;
425
+      border: 1rpx solid #ddd;
426
+      border-radius: 30rpx;
427
+      font-size: 28rpx;
428
+    }
429
+  }
430
+
431
+  .filter-btn {
432
+    padding: 10rpx 20rpx;
433
+    background-color: #f5f5f5;
434
+    border-radius: 20rpx;
435
+
436
+    .filter-icon {
437
+      font-size: 26rpx;
438
+      color: #666;
439
+    }
440
+  }
441
+}
442
+
443
+.dept-section {
444
+  background-color: #fff;
445
+  padding: 20rpx 30rpx;
446
+  border-bottom: 1rpx solid #eee;
447
+
448
+  .dept-header {
449
+    display: flex;
450
+    justify-content: space-between;
451
+    align-items: center;
452
+    margin-bottom: 20rpx;
453
+
454
+    .dept-title {
455
+      font-size: 28rpx;
456
+      color: #333;
457
+      font-weight: bold;
458
+    }
459
+
460
+    .dept-name {
461
+      font-size: 24rpx;
462
+      color: #666;
463
+    }
464
+  }
465
+
466
+  .dept-scroll {
467
+    white-space: nowrap;
468
+
469
+    .dept-list {
470
+      display: flex;
471
+
472
+      .dept-item {
473
+        display: inline-block;
474
+        padding: 10rpx 20rpx;
475
+        margin-right: 20rpx;
476
+        background-color: #f5f5f5;
477
+        border-radius: 20rpx;
478
+        font-size: 24rpx;
479
+        color: #666;
480
+        white-space: nowrap;
481
+
482
+        &.active {
483
+          background-color: #007aff;
484
+          color: #fff;
485
+        }
486
+      }
487
+    }
488
+  }
489
+}
490
+
491
+.status-section {
492
+  background-color: #fff;
493
+  padding: 20rpx 30rpx;
494
+  border-bottom: 1rpx solid #eee;
495
+
496
+  .status-header {
497
+    display: flex;
498
+    justify-content: space-between;
499
+    align-items: center;
500
+    margin-bottom: 20rpx;
501
+
502
+    .status-title {
503
+      font-size: 28rpx;
504
+      color: #333;
505
+      font-weight: bold;
506
+    }
507
+
508
+    .status-name {
509
+      font-size: 24rpx;
510
+      color: #666;
511
+    }
512
+  }
513
+
514
+  .status-scroll {
515
+    white-space: nowrap;
516
+
517
+    .status-list {
518
+      display: flex;
519
+
520
+      .status-item {
521
+        display: inline-block;
522
+        padding: 10rpx 20rpx;
523
+        margin-right: 20rpx;
524
+        background-color: #f5f5f5;
525
+        border-radius: 20rpx;
526
+        font-size: 24rpx;
527
+        color: #666;
528
+        white-space: nowrap;
529
+
530
+        &.active {
531
+          background-color: #007aff;
532
+          color: #fff;
533
+        }
534
+      }
535
+    }
536
+  }
537
+}
538
+
539
+.filter-overlay {
540
+  position: fixed;
541
+  top: 0;
542
+  left: 0;
543
+  right: 0;
544
+  bottom: 0;
545
+  background-color: rgba(0, 0, 0, 0.5);
546
+  z-index: 1000;
547
+  display: flex;
548
+  align-items: flex-end;
549
+}
550
+
551
+.filter-popup {
552
+  background-color: #fff;
553
+  border-radius: 20rpx 20rpx 0 0;
554
+  padding: 40rpx;
555
+  width: 100%;
556
+  max-height: 80vh;
557
+
558
+  .filter-header {
559
+    display: flex;
560
+    justify-content: space-between;
561
+    align-items: center;
562
+    margin-bottom: 40rpx;
563
+
564
+    .filter-title {
565
+      font-size: 32rpx;
566
+      font-weight: bold;
567
+      color: #333;
568
+    }
569
+
570
+    .filter-close {
571
+      padding: 10rpx;
572
+
573
+      text {
574
+        font-size: 24rpx;
575
+        color: #999;
576
+      }
577
+    }
578
+  }
579
+
580
+  .filter-content {
581
+    .filter-item {
582
+      margin-bottom: 30rpx;
583
+
584
+      .filter-label {
585
+        display: block;
586
+        font-size: 28rpx;
587
+        color: #333;
588
+        margin-bottom: 10rpx;
589
+      }
590
+
591
+      picker {
592
+        .picker-view {
593
+          padding: 20rpx;
594
+          background-color: #f5f5f5;
595
+          border-radius: 10rpx;
596
+          font-size: 28rpx;
597
+          color: #333;
598
+        }
599
+      }
600
+    }
601
+  }
602
+
603
+  .filter-actions {
604
+    display: flex;
605
+    justify-content: space-between;
606
+    margin-top: 40rpx;
607
+    gap: 20rpx;
608
+
609
+    button {
610
+      flex: 1;
611
+      height: 80rpx;
612
+      border-radius: 10rpx;
613
+      font-size: 28rpx;
614
+      border: none;
615
+
616
+      &.btn-default {
617
+        background-color: #f5f5f5;
618
+        color: #666;
619
+      }
620
+
621
+      &.btn-primary {
622
+        background-color: #007aff;
623
+        color: #fff;
624
+      }
625
+    }
626
+  }
627
+}
628
+
629
+.staff-list {
630
+  .staff-card {
631
+    background-color: #fff;
632
+    border-radius: 20rpx;
633
+    margin-bottom: 20rpx;
634
+    padding: 30rpx;
635
+    box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
636
+
637
+    .staff-header {
638
+      display: flex;
639
+      align-items: center;
640
+      margin-bottom: 20rpx;
641
+
642
+      .staff-avatar {
643
+        margin-right: 20rpx;
644
+
645
+        .avatar-image {
646
+          width: 60rpx;
647
+          height: 60rpx;
648
+          border-radius: 50%;
649
+        }
650
+      }
651
+
652
+      .staff-info {
653
+        flex: 1;
654
+
655
+        .staff-name {
656
+          font-size: 32rpx;
657
+          font-weight: bold;
658
+          color: #333;
659
+          margin-bottom: 10rpx;
660
+        }
661
+
662
+        .staff-dept {
663
+          font-size: 24rpx;
664
+          color: #666;
665
+          margin-bottom: 10rpx;
666
+        }
667
+
668
+        .staff-status {
669
+          display: inline-block;
670
+
671
+          .status-tag {
672
+            padding: 4rpx 12rpx;
673
+            border-radius: 10rpx;
674
+            font-size: 20rpx;
675
+
676
+            &.status-0 {
677
+              background-color: #e6f7ff;
678
+              color: #1890ff;
679
+            }
680
+
681
+            &.status-1 {
682
+              background-color: #fff2e8;
683
+              color: #fa8c16;
684
+            }
685
+
686
+            &.status-2 {
687
+              background-color: #f6ffed;
688
+              color: #52c41a;
689
+            }
690
+
691
+            &.status-3 {
692
+              background-color: #f9f0ff;
693
+              color: #722ed1;
694
+            }
695
+
696
+            &.status-4 {
697
+              background-color: #f0f0f0;
698
+              color: #8c8c8c;
699
+            }
700
+          }
701
+        }
702
+      }
703
+
704
+      .staff-actions {
705
+        padding: 10rpx;
706
+
707
+        .arrow-icon {
708
+          font-size: 24rpx;
709
+          color: #999;
710
+        }
711
+      }
712
+    }
713
+
714
+    .staff-details {
715
+      .detail-item {
716
+        display: flex;
717
+        margin-bottom: 15rpx;
718
+
719
+        .detail-label {
720
+          width: 140rpx;
721
+          font-size: 26rpx;
722
+          color: #999;
723
+        }
724
+
725
+        .detail-value {
726
+          flex: 1;
727
+          font-size: 26rpx;
728
+          color: #333;
729
+        }
730
+      }
731
+    }
732
+  }
733
+}
734
+</style>

+ 11
- 1
oa-ui-app/pages/work/index.vue Voir le fichier

@@ -2,7 +2,7 @@
2 2
  * @Author: ysh
3 3
  * @Date: 2025-01-16 11:17:08
4 4
  * @LastEditors: Please set LastEditors
5
- * @LastEditTime: 2025-08-04 11:12:27
5
+ * @LastEditTime: 2025-08-07 13:47:32
6 6
 -->
7 7
 <template>
8 8
   <view class="work-container">
@@ -96,6 +96,12 @@ export default {
96 96
           icon: '/static/images/work/declare.png',
97 97
           url: 'declare',
98 98
           hasPermi: checkPermi(['oa:declare:list'])
99
+        },
100
+        {
101
+          name: '人事管理',
102
+          icon: '/static/images/work/staff.png',
103
+          url: 'staff',
104
+          hasPermi: checkPermi(['system:user:list'])
99 105
         }
100 106
       ],
101 107
       flowList: ['借款审批', '用车审批', '设备审批', '工作填报'],
@@ -152,6 +158,10 @@ export default {
152 158
         uni.navigateTo({
153 159
           url: '/pages/oa/declare/declareList'
154 160
         })
161
+      } else if (type == 'staff') {
162
+        uni.navigateTo({
163
+          url: '/pages/oa/staff/staffList'
164
+        })
155 165
       }
156 166
       else {
157 167
         this.$modal.showToast('模块建设中~')

Loading…
Annuler
Enregistrer