Просмотр исходного кода

feat(排班管理): 新增排班管理功能模块

添加排班管理功能,包括排班列表查询、统一排班生成和排班删除功能
前端实现排班管理的页面展示和交互逻辑
后端提供排班相关的API接口
huoyi месяцев назад: 2
Родитель
Сommit
b792b681ae
2 измененных файлов с 438 добавлено и 0 удалено
  1. 27 0
      src/api/attendance/shiftScheduling.js
  2. 411 0
      src/views/attendance/shiftScheduling/index.vue

+ 27 - 0
src/api/attendance/shiftScheduling.js

@@ -0,0 +1,27 @@
1
+import request from '@/utils/request'
2
+
3
+// 查询排班列表
4
+export function listShiftScheduling(query) {
5
+  return request({
6
+    url: '/item/simpleDutySchedule/list',
7
+    method: 'get',
8
+    params: query
9
+  })
10
+}
11
+
12
+//生成统一排班计划(支持单班、两班倒、多班倒等模式)
13
+export function generateUnifiedSchedule(data) {
14
+  return request({
15
+    url: '/item/simpleDutySchedule/generateUnifiedSchedule',
16
+    method: 'post',
17
+    data: data
18
+  })
19
+}
20
+
21
+//删除值班安排
22
+export function delSimpleDutySchedule(scheduleIds) {
23
+  return request({
24
+    url: `/item/simpleDutySchedule/${scheduleIds}`,
25
+    method: 'delete'
26
+  })
27
+}

+ 411 - 0
src/views/attendance/shiftScheduling/index.vue

@@ -0,0 +1,411 @@
1
+<template>
2
+  <div class="app-container">
3
+    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch">
4
+      <el-form-item label="部门" prop="departmentName">
5
+        <el-input v-model="queryParams.departmentName" placeholder="请输入部门名称" clearable @keyup.enter="handleQuery" />
6
+      </el-form-item>
7
+      <el-form-item label="值班日期" prop="dutyDate">
8
+        <el-date-picker clearable v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-"
9
+          start-placeholder="开始日期" end-placeholder="结束日期">
10
+        </el-date-picker>
11
+      </el-form-item>
12
+      <el-form-item label="状态" prop="status">
13
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable style="width: 240px">
14
+          <el-option label="停用" :value="0" />
15
+          <el-option label="正常" :value="1" />
16
+        </el-select>
17
+      </el-form-item>
18
+      <el-form-item>
19
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
20
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
21
+      </el-form-item>
22
+    </el-form>
23
+
24
+    <el-row :gutter="10" class="mb8">
25
+      <el-col :span="1.5">
26
+        <el-button type="primary" plain icon="Plus" @click="handleAdd"
27
+          v-hasPermi="['attendance:shiftScheduling:add']">生成排班</el-button>
28
+      </el-col>
29
+      <el-col :span="1.5">
30
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete"
31
+          v-hasPermi="['attendance:shiftScheduling:remove']">删除</el-button>
32
+      </el-col>
33
+      <!-- <el-col :span="1.5">
34
+        <el-button type="warning" plain icon="Download" @click="handleExport"
35
+          v-hasPermi="['attendance:shiftScheduling:export']">导出</el-button>
36
+      </el-col>
37
+      <el-col :span="1.5">
38
+        <el-button type="info" plain icon="Upload" @click="handleImport"
39
+          v-hasPermi="['attendance:shiftScheduling:import']">导入</el-button>
40
+      </el-col> -->
41
+      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
42
+    </el-row>
43
+
44
+    <el-table v-loading="loading" :data="shiftSchedulingList" @selection-change="handleSelectionChange">
45
+      <el-table-column type="selection" width="55" align="center" />
46
+      <el-table-column label="部门" align="center" prop="departmentName" />
47
+      <el-table-column label="值班日期" align="center" prop="dutyDate" />
48
+      <el-table-column label="开始时间" align="center" prop="startTime">
49
+        <template #default="scope">
50
+          {{ scope.row.startTime ? scope.row.startTime.split(' ')[0] : '' }}
51
+        </template>
52
+      </el-table-column>
53
+      <el-table-column label="结束时间" align="center" prop="endTime">
54
+        <template #default="scope">
55
+          {{ scope.row.endTime ? scope.row.endTime.split(' ')[0] : '' }}
56
+        </template>
57
+      </el-table-column>
58
+      <el-table-column label="是否跨天" align="center" prop="crossDay">
59
+        <template #default="scope">
60
+          {{ scope.row.crossDay == '1' ? '是' : '否' }}
61
+        </template>
62
+      </el-table-column>
63
+      <el-table-column label="状态" align="center" prop="status">
64
+        <template #default="scope">
65
+          {{ scope.row.status == '1' ? '正常' : '停用' }}
66
+        </template>
67
+      </el-table-column>
68
+      <el-table-column label="创建者" align="center" prop="createBy" />
69
+      <el-table-column label="创建时间" align="center" prop="createTime" />
70
+      <!-- <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
71
+        <template #default="scope">
72
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)"
73
+            v-hasPermi="['attendance:shiftScheduling:edit']">修改</el-button>
74
+          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)"
75
+            v-hasPermi="['attendance:shiftScheduling:remove']">删除</el-button>
76
+        </template>
77
+      </el-table-column> -->
78
+    </el-table>
79
+
80
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
81
+      v-model:limit="queryParams.pageSize" @pagination="getList" />
82
+
83
+    <!-- 添加或修改排班对话框 -->
84
+    <el-dialog :title="title" v-model="open" :width="1000" append-to-body>
85
+      <el-form ref="shiftSchedulingRef" :model="form" :rules="rules" label-width="80px">
86
+        <el-row :gutter="20">
87
+          <el-col :span="9">
88
+            <el-form-item label="开始时间" prop="startDate">
89
+              <el-date-picker clearable style="width:100%" v-model="form.startDate" type="date" format="YYYY-MM-DD"
90
+                value-format="YYYY-MM-DD" placeholder="请选择开始时间" @change="calculateDuration">
91
+              </el-date-picker>
92
+            </el-form-item>
93
+          </el-col>
94
+          <el-col :span="9">
95
+            <el-form-item label="结束时间" prop="endTime">
96
+              <el-date-picker clearable style="width:100%" v-model="form.endTime" type="date" format="YYYY-MM-DD"
97
+                value-format="YYYY-MM-DD" placeholder="请选择结束时间" @change="calculateDuration">
98
+              </el-date-picker>
99
+            </el-form-item>
100
+          </el-col>
101
+          <el-col :span="6" style="display: flex;align-items: center;height: 33px;">
102
+
103
+            共<el-input v-model="form.daysToGenerate" placeholder="自动计算" disabled readonly
104
+              style="width:100%;padding-left:10px;padding-right:10px;" />天
105
+
106
+          </el-col>
107
+        </el-row>
108
+
109
+
110
+
111
+        <el-row :gutter="20">
112
+          <el-col :span="12">
113
+            <el-form-item label="开始大队" prop="startDeptId">
114
+              <el-select v-model="form.startDeptId" placeholder="请选择开始大队" style="width:100%">
115
+                <el-option v-for="dept in departmentList" :key="dept.deptId" :label="dept.deptName"
116
+                  :value="dept.deptId" />
117
+              </el-select>
118
+            </el-form-item>
119
+          </el-col>
120
+          <el-col :span="12">
121
+            <el-form-item label="大队顺序" prop="deptIds">
122
+              <el-select v-model="form.deptIds" multiple placeholder="请选择大队顺序" style="width:100%">
123
+                <el-option v-for="dept in departmentList" :key="dept.deptId" :label="dept.deptName"
124
+                  :value="dept.deptId" />
125
+              </el-select>
126
+            </el-form-item>
127
+          </el-col>
128
+        </el-row>
129
+
130
+        <el-form-item label="班次安排" prop="shifts">
131
+          <div style="display: flex; align-items: center; width:100%">
132
+            <el-input-number v-model="form.shiftStart" :min="0" :max="24" controls-position="right" style="width:120px"
133
+              :default-value="9" />
134
+            <span style="margin: 0 10px; color: #666;">-</span>
135
+            <el-input-number v-model="form.shiftEnd" :min="0" :max="24" controls-position="right" style="width:120px"
136
+              :default-value="9" />
137
+          </div>
138
+        </el-form-item>
139
+      </el-form>
140
+      <template #footer>
141
+        <div class="dialog-footer">
142
+          <el-button type="primary" @click="submitForm">确 定</el-button>
143
+          <el-button @click="cancel">取 消</el-button>
144
+        </div>
145
+      </template>
146
+    </el-dialog>
147
+
148
+  </div>
149
+</template>
150
+
151
+<script setup name="ShiftScheduling">
152
+import { listShiftScheduling, generateUnifiedSchedule, delSimpleDutySchedule } from "@/api/attendance/shiftScheduling"
153
+import { listDept } from "@/api/system/dept"
154
+import useUserStore from '@/store/modules/user'
155
+
156
+const { proxy } = getCurrentInstance()
157
+const userStore = useUserStore()
158
+
159
+
160
+
161
+const shiftSchedulingList = ref([])
162
+const open = ref(false)
163
+const loading = ref(true)
164
+const showSearch = ref(true)
165
+const ids = ref([])
166
+const single = ref(true)
167
+const multiple = ref(true)
168
+const total = ref(0)
169
+const title = ref("")
170
+const dateRange = ref([])
171
+const departmentList = ref([])
172
+
173
+import { getToken } from "@/utils/auth"
174
+
175
+
176
+const data = reactive({
177
+  form: {},
178
+  queryParams: {
179
+    pageNum: 1,
180
+    pageSize: 10,
181
+    departmentName: null,
182
+    dutyDate: null,
183
+    status: null
184
+  },
185
+  rules: {
186
+    startDate: [
187
+      { required: true, message: "开始时间不能为空", trigger: "blur" }
188
+    ],
189
+    endTime: [
190
+      { required: true, message: "结束时间不能为空", trigger: "blur" }
191
+    ],
192
+    startDeptId: [
193
+      { required: true, message: "开始大队不能为空", trigger: "change" }
194
+    ],
195
+    deptIds: [
196
+      { required: true, message: "大队顺序不能为空", trigger: "change" }
197
+    ],
198
+    shifts: [
199
+      {
200
+        required: true,
201
+        validator: (rule, value, callback) => {
202
+          if (!form.value.shiftStart && !form.value.shiftEnd) {
203
+            callback(new Error("班次安排不能为空"))
204
+          } else {
205
+            callback()
206
+          }
207
+        },
208
+        trigger: "change"
209
+      }
210
+    ],
211
+
212
+  }
213
+})
214
+
215
+const upload = reactive({
216
+  // 是否显示弹出层
217
+  open: false,
218
+  // 弹出层标题
219
+  title: "",
220
+  // 是否禁用上传
221
+  isUploading: false,
222
+  // 是否更新已经存在的用户数据
223
+  updateSupport: 0,
224
+  // 设置上传的请求头部
225
+  headers: { Authorization: "Bearer " + getToken() },
226
+  // 上传的地址
227
+  url: import.meta.env.VITE_APP_BASE_API + "/attendance/shiftScheduling/importData"
228
+})
229
+
230
+const { queryParams, form, rules } = toRefs(data)
231
+
232
+/** 查询排班列表 */
233
+function getList() {
234
+  loading.value = true
235
+  let params = {
236
+    ...queryParams.value,
237
+    startTime: dateRange.value && dateRange.value[0],
238
+    endTime: dateRange.value && dateRange.value[1]
239
+  }
240
+
241
+  listShiftScheduling(params).then(response => {
242
+    shiftSchedulingList.value = response.rows
243
+    total.value = response.total
244
+    loading.value = false
245
+  })
246
+}
247
+
248
+// 计算持续时间
249
+const calculateDuration = () => {
250
+  if (form.value.startDate && form.value.endTime) {
251
+    const start = new Date(form.value.startDate)
252
+    const end = new Date(form.value.endTime)
253
+    const diffTime = Math.abs(end - start)
254
+    const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
255
+    form.value.daysToGenerate = diffDays
256
+
257
+    // 判断是否跨天
258
+    form.value.crossDay = start.getDate() !== end.getDate() ? '1' : '0'
259
+  }
260
+}
261
+
262
+// 取消按钮
263
+function cancel() {
264
+  open.value = false
265
+  reset()
266
+}
267
+
268
+// 表单重置
269
+function reset() {
270
+  form.value = {
271
+    startDate: null,
272
+    endTime: null,
273
+    daysToGenerate: null,
274
+    crossDay: '0',
275
+    startDeptId: null,
276
+    deptIds: [],
277
+    shifts: null,
278
+    shiftStart: 9,
279
+    shiftEnd: 9
280
+  }
281
+  proxy.resetForm("shiftSchedulingRef")
282
+}
283
+
284
+/** 搜索按钮操作 */
285
+function handleQuery() {
286
+  queryParams.value.pageNum = 1
287
+  getList()
288
+}
289
+
290
+/** 重置按钮操作 */
291
+function resetQuery() {
292
+  dateRange.value = []
293
+  proxy.resetForm("queryRef")
294
+  handleQuery()
295
+}
296
+
297
+// 多选框选中数据
298
+function handleSelectionChange(selection) {
299
+  ids.value = selection.map(item => item.id)
300
+  single.value = selection.length != 1
301
+  multiple.value = !selection.length
302
+}
303
+
304
+/** 新增按钮操作 */
305
+function handleAdd() {
306
+  reset()
307
+  open.value = true
308
+  title.value = "生成排班"
309
+}
310
+
311
+
312
+/** 提交按钮 */
313
+function submitForm() {
314
+  proxy.$refs["shiftSchedulingRef"].validate(async valid => {
315
+    if (valid) {
316
+      const submitData = await prepareFormData()
317
+
318
+
319
+
320
+      if (form.value.id != null) {
321
+        updateShiftScheduling(submitData).then(response => {
322
+          proxy.$modal.msgSuccess("修改成功")
323
+          open.value = false
324
+          getList()
325
+        })
326
+      } else {
327
+        // 使用生成统一排班接口
328
+        generateUnifiedSchedule(submitData).then(response => {
329
+          proxy.$modal.msgSuccess("排班生成成功")
330
+          open.value = false
331
+          getList()
332
+        }).catch(error => {
333
+          console.error('生成排班失败:', error)
334
+          proxy.$modal.msgError("排班生成失败")
335
+        })
336
+      }
337
+    }
338
+  })
339
+}
340
+
341
+/** 删除按钮操作 */
342
+function handleDelete(row) {
343
+  const _ids = row.id || ids.value
344
+  proxy.$modal.confirm('是否确认删除排班数据项?').then(function () {
345
+
346
+    return delSimpleDutySchedule(_ids)
347
+  }).then(() => {
348
+    getList()
349
+    proxy.$modal.msgSuccess("删除成功")
350
+  }).catch(() => { })
351
+}
352
+
353
+
354
+
355
+// 获取大队列表
356
+const fetchDepartmentList = async () => {
357
+  try {
358
+    const response = await listDept()
359
+    departmentList.value = (response.data || []).filter(item => item.deptType === 'BRIGADE')
360
+  } catch (error) {
361
+    console.error('获取大队列表失败:', error)
362
+    departmentList.value = []
363
+  }
364
+}
365
+
366
+// 提交表单前的数据处理
367
+const prepareFormData = async () => {
368
+  const formData = { ...form.value }
369
+
370
+  // 获取当前登录用户的stationId - 尝试多种可能的字段名
371
+  const info = await userStore.getInfo()
372
+
373
+  // 如果找到了stationId,将其添加到表单数据中
374
+  if (info) {
375
+    formData.stationId = info.userInfo && info.userInfo.stationId
376
+  }
377
+
378
+  // 为createBy和createTime字段赋值
379
+  // createBy: 当前登录用户的昵称
380
+  formData.createBy = userStore.nickName || userStore.name || ''
381
+
382
+  // createTime: 当前时间,格式为YYYY-MM-DD
383
+  const now = new Date()
384
+  formData.createTime = now.toISOString().substring(0, 10)
385
+
386
+
387
+  formData.shifts = `${formData.shiftStart || 0}-${formData.shiftEnd || 0}`
388
+
389
+  // 处理开始日期,从开始时间中提取日期部分
390
+  if (formData.startDate) {
391
+    formData.startDate = formData.startDate.split(' ')[0]
392
+  }
393
+  if (formData.deptIds) {
394
+    formData.deptIds = formData.deptIds.join(',')
395
+  }
396
+  delete formData.endTime
397
+  delete formData.shiftStart
398
+  delete formData.shiftEnd
399
+
400
+  return formData
401
+}
402
+
403
+onMounted(() => {
404
+  getList()
405
+  fetchDepartmentList()
406
+})
407
+
408
+
409
+
410
+
411
+</script>