| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container scheduling-container"> |
| | | <!-- çéåºå --> |
| | | <div class="filter-section"> |
| | | <el-form :inline="true" :model="filterForm" class="filter-form"> |
| | | <el-form-item label="åå·¥å§åï¼"> |
| | | <el-input |
| | | v-model="filterForm.staffName" |
| | | placeholder="请è¾å
¥åå·¥å§å" |
| | | clearable |
| | | style="width: 150px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="çæ¬¡ç±»åï¼"> |
| | | <el-select v-model="filterForm.shiftType" placeholder="è¯·éæ©çæ¬¡" clearable style="width: 120px"> |
| | | <el-option v-for="item in shift_type" :label="item.label" :value="item.value" :key="item.value"/> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="æ¥æèå´ï¼"> |
| | | <el-date-picker |
| | | v-model="filterForm.dateRange" |
| | | type="daterange" |
| | | range-separator="è³" |
| | | start-placeholder="å¼å§æ¥æ" |
| | | end-placeholder="ç»ææ¥æ" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | style="width: 250px" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" @click="handleFilter"> |
| | | <el-icon><Search/></el-icon> |
| | | çé |
| | | </el-button> |
| | | <el-button @click="resetFilter"> |
| | | <el-icon><Refresh/></el-icon> |
| | | éç½® |
| | | </el-button> |
| | | <el-button @click="handleExport"> |
| | | <el-icon><Download/></el-icon> |
| | | å¯¼åº |
| | | </el-button> |
| | | <el-button type="primary" @click="openScheduleDialog('add')"> |
| | | <el-icon><Plus/></el-icon> |
| | | æ°å¢æç |
| | | </el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | |
| | | <!-- æçè¡¨æ ¼ --> |
| | | <div class="table-section"> |
| | | <el-table |
| | | :data="scheduleList" |
| | | border |
| | | :loading="tableLoading" |
| | | stripe |
| | | style="width: 100%" |
| | | height="calc(100vh - 18.5em)" |
| | | @selection-change="handleSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="55"/> |
| | | <el-table-column prop="staffName" label="åå·¥å§å" width="120"/> |
| | | <el-table-column prop="staffNo" label="å工工å·" width="100"/> |
| | | <el-table-column prop="department" label="é¨é¨" width="120"> |
| | | <template #default="scope"> |
| | | {{ (department_type.find(i => i.value === String(scope.row.department)) || {}).label }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="shiftType" label="çæ¬¡ç±»å" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getShiftTagType(scope.row.shiftType)"> |
| | | {{ (shift_type.find(i => i.value === String(scope.row.shiftType)) || {}).label }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="workDate" label="工使¥æ" width="120"/> |
| | | <el-table-column prop="startTime" label="å¼å§æ¶é´" width="100"/> |
| | | <el-table-column prop="endTime" label="ç»ææ¶é´" width="100"/> |
| | | <el-table-column prop="workHours" label="工使¶é¿" width="100"> |
| | | <template #default="scope"> |
| | | {{ scope.row.workHours }}å°æ¶ |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStatusTagType(scope.row.status)"> |
| | | {{ (schedule_status.find(i => i.value === String(scope.row.status)) || {}).label }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="remark" label="夿³¨" min-width="150"/> |
| | | <el-table-column label="æä½" width="200" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | @click="openScheduleDialog('edit', scope.row)" |
| | | > |
| | | ç¼è¾ |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | @click="handleDelete(scope.row)" |
| | | > |
| | | å é¤ |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <pagination |
| | | v-if="tableCount > 0" |
| | | :total="tableCount" |
| | | :page="filterForm.current" |
| | | :limit="filterForm.size" |
| | | @pagination="paginationChange" |
| | | /> |
| | | </div> |
| | | |
| | | <!-- æ¹éæä½ --> |
| | | <div class="batch-actions" v-if="selectedRows.length > 0"> |
| | | <el-button |
| | | type="danger" |
| | | @click="handleBatchDelete" |
| | | :disabled="selectedRows.length === 0" |
| | | > |
| | | æ¹éå é¤ ({{ selectedRows.length }}) |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- æçæ°å¢/ç¼è¾å¯¹è¯æ¡ --> |
| | | <el-dialog |
| | | v-model="scheduleDialog" |
| | | :title="dialogType === 'add' ? 'æ°å¢æç' : 'ç¼è¾æç'" |
| | | width="700px" |
| | | @close="closeScheduleDialog" |
| | | > |
| | | <el-form |
| | | :model="scheduleForm" |
| | | :rules="scheduleRules" |
| | | ref="scheduleFormRef" |
| | | label-width="120px" |
| | | > |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="åå·¥å§åï¼" prop="staffId"> |
| | | <el-select v-model="scheduleForm.staffId" placeholder="请è¾å
¥åå·¥å§å" style="width: 100%" |
| | | @change="handleSelectStaff"> |
| | | <el-option v-for="item in personList" :label="item.staffName" :value="item.id" :key="item.id"/> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å工工å·ï¼" prop="staffNo"> |
| | | <el-input :disabled="true" v-model="scheduleForm.staffNo" placeholder=""/> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="é¨é¨ï¼" prop="department"> |
| | | <el-select v-model="scheduleForm.department" placeholder="è¯·éæ©é¨é¨" style="width: 100%"> |
| | | <el-option v-for="item in department_type" :label="item.label" :value="item.value" :key="item.value"/> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="çæ¬¡ç±»åï¼" prop="shiftType"> |
| | | <el-select v-model="scheduleForm.shiftType" placeholder="è¯·éæ©çæ¬¡" style="width: 100%"> |
| | | <el-option v-for="item in shift_type" :label="item.label" :value="item.value" :key="item.value"/> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="工使¥æï¼" prop="workDate"> |
| | | <el-date-picker |
| | | v-model="scheduleForm.workDate" |
| | | type="date" |
| | | placeholder="鿩工使¥æ" |
| | | style="width: 100%" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç¶æï¼" prop="status"> |
| | | <el-select v-model="scheduleForm.status" placeholder="è¯·éæ©ç¶æ" style="width: 100%"> |
| | | <el-option v-for="item in schedule_status" :label="item.label" :value="item.value" :key="item.value"/> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¶é´ï¼" prop="startTime"> |
| | | <el-time-picker |
| | | v-model="scheduleForm.startTime" |
| | | placeholder="éæ©å¼å§æ¶é´" |
| | | style="width: 100%" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»ææ¶é´ï¼" prop="endTime"> |
| | | <el-time-picker |
| | | v-model="scheduleForm.endTime" |
| | | placeholder="éæ©ç»ææ¶é´" |
| | | style="width: 100%" |
| | | format="HH:mm" |
| | | value-format="HH:mm" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <el-row :gutter="20"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="夿³¨ï¼" prop="remark"> |
| | | <el-input |
| | | v-model="scheduleForm.remark" |
| | | type="textarea" |
| | | :rows="3" |
| | | placeholder="请è¾å
¥å¤æ³¨ä¿¡æ¯" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitScheduleForm">确认</el-button> |
| | | <el-button @click="closeScheduleDialog">åæ¶</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import {ref, reactive, computed, onMounted, getCurrentInstance} from 'vue' |
| | | import {ElMessage, ElMessageBox} from 'element-plus' |
| | | import {useDict} from "@/utils/dict.js" |
| | | import {Plus, Download, Search, Refresh} from '@element-plus/icons-vue' |
| | | import {save, del, delByIds, listPage} from "@/api/personnelManagement/scheduling.js" |
| | | import dayjs from "dayjs"; |
| | | import pagination from "@/components/PIMTable/Pagination.vue"; |
| | | import {staffOnJobListPage} from "@/api/personnelManagement/staffOnJob.js"; |
| | | |
| | | const { proxy } = getCurrentInstance(); |
| | | |
| | | const tableCount = ref(0) |
| | | // ååºå¼æ°æ® |
| | | const scheduleDialog = ref(false) |
| | | const dialogType = ref('add') |
| | | const selectedRows = ref([]) |
| | | const scheduleFormRef = ref() |
| | | |
| | | // çé表å |
| | | const filterForm = reactive({ |
| | | staffName: '', |
| | | shiftType: '', |
| | | dateRange: [], |
| | | current:1, |
| | | size: 10 |
| | | }) |
| | | |
| | | // æç表å |
| | | const scheduleForm = reactive({ |
| | | id: '', |
| | | staffId: '', |
| | | staffNo: '', |
| | | department: '', |
| | | shiftType: '', |
| | | workDate: '', |
| | | startTime: '', |
| | | endTime: '', |
| | | workStartTime: '', |
| | | workEndTime: '', |
| | | workHours: 0, |
| | | status: '', |
| | | remark: '' |
| | | }) |
| | | |
| | | // 表åéªè¯è§å |
| | | const scheduleRules = reactive({ |
| | | staffId: [{required: true, message: 'è¯·éæ©åå·¥', trigger: 'change'}], |
| | | department: [{required: true, message: 'è¯·éæ©é¨é¨', trigger: 'change'}], |
| | | shiftType: [{required: true, message: 'è¯·éæ©çæ¬¡ç±»å', trigger: 'change'}], |
| | | workDate: [{required: true, message: 'è¯·éæ©å·¥ä½æ¥æ', trigger: 'change'}], |
| | | startTime: [{required: true, message: 'è¯·éæ©å¼å§æ¶é´', trigger: 'change'}], |
| | | endTime: [{required: true, message: 'è¯·éæ©ç»ææ¶é´', trigger: 'change'}], |
| | | status: [{required: true, message: 'è¯·éæ©ç¶æ', trigger: 'change'}] |
| | | }) |
| | | const tableLoading = ref(false) |
| | | |
| | | //åå
¸ |
| | | const {department_type, schedule_status, shift_type} = useDict("department_type", "schedule_status", "shift_type") |
| | | // 人åå表 |
| | | const personList = ref([]); |
| | | |
| | | // 模ææçæ°æ® |
| | | const scheduleList = ref([]) |
| | | |
| | | |
| | | /** |
| | | * è·åå½åå¨è人åå表 |
| | | */ |
| | | const getPersonList = () => { |
| | | staffOnJobListPage({ |
| | | current: -1, |
| | | size: -1, |
| | | staffState: 1 |
| | | }).then(res => { |
| | | personList.value = res.data.records || [] |
| | | }) |
| | | }; |
| | | const paginationChange = (obj) => { |
| | | filterForm.current = obj.page; |
| | | filterForm.size = obj.limit; |
| | | handleFilter(); |
| | | }; |
| | | |
| | | const handleSelectStaff = (val) => { |
| | | let obj = personList.value.find(item => item.id === val) |
| | | scheduleForm.staffNo = obj.staffNo |
| | | |
| | | } |
| | | |
| | | // è·åçæ¬¡æ ç¾ç±»å |
| | | const getShiftTagType = (shiftType) => { |
| | | const typeMap = Object.fromEntries(shift_type.value.map(i => [i.value, i.elTagType])) |
| | | return typeMap[shiftType] || 'info' |
| | | } |
| | | |
| | | // è·åç¶ææ ç¾ç±»å |
| | | const getStatusTagType = (status) => { |
| | | const typeMap = Object.fromEntries(schedule_status.value.map(i => [i.value, i.elTagType])) |
| | | return typeMap[status] || 'info' |
| | | } |
| | | |
| | | // çé |
| | | const handleFilter = async () => { |
| | | tableLoading.value = true |
| | | let searchForm = { |
| | | ...filterForm, |
| | | ...(filterForm.dateRange.length > 0 && { |
| | | startDate: filterForm.dateRange[0], |
| | | endDate: filterForm.dateRange[1], |
| | | }) |
| | | } |
| | | let resp = await listPage(searchForm) |
| | | tableCount.value = resp.data.total |
| | | scheduleList.value = resp.data.records.map(it => { |
| | | return { |
| | | ...it, |
| | | 'startTime': dayjs(it.workStartTime).format('HH:mm'), |
| | | 'endTime': dayjs(it.workEndTime).format('HH:mm'), |
| | | } |
| | | }) |
| | | tableLoading.value = false |
| | | |
| | | } |
| | | |
| | | // éç½®çé |
| | | const resetFilter = () => { |
| | | filterForm.staffName = '' |
| | | filterForm.shiftType = '' |
| | | filterForm.dateRange = [] |
| | | } |
| | | |
| | | // æå¼æçå¯¹è¯æ¡ |
| | | const openScheduleDialog = (type, data) => { |
| | | dialogType.value = type |
| | | scheduleDialog.value = true |
| | | getPersonList() |
| | | if (type === 'edit' && data) { |
| | | // ç¼è¾æ¨¡å¼ï¼å¤å¶æ°æ® |
| | | Object.assign(scheduleForm, {...data}) |
| | | } else { |
| | | // æ°å¢æ¨¡å¼ï¼é置表å |
| | | Object.keys(scheduleForm).forEach(key => { |
| | | scheduleForm[key] = '' |
| | | }) |
| | | // scheduleForm.status = '已宿' |
| | | scheduleForm.workDate = new Date().toISOString().split('T')[0] |
| | | } |
| | | } |
| | | |
| | | // å
³éæçå¯¹è¯æ¡ |
| | | const closeScheduleDialog = () => { |
| | | scheduleFormRef.value?.resetFields() |
| | | scheduleDialog.value = false |
| | | } |
| | | |
| | | // 计ç®å·¥ä½æ¶é¿ |
| | | const calculateWorkHours = () => { |
| | | if (scheduleForm.workDate && scheduleForm.startTime && scheduleForm.endTime) { |
| | | // ä½¿ç¨ workDate ä¸ startTime å endTime ç»å |
| | | const startDateTime = new Date(`${scheduleForm.workDate} ${scheduleForm.startTime}`) |
| | | const endDateTime = new Date(`${scheduleForm.workDate} ${scheduleForm.endTime}`) |
| | | |
| | | // å¤ç跨天æ
åµï¼ç»ææ¶é´æ©äºå¼å§æ¶é´ï¼ |
| | | if (endDateTime < startDateTime) { |
| | | // 跨天ï¼å°ç»ææ¥æå ä¸å¤© |
| | | endDateTime.setDate(endDateTime.getDate() + 1) |
| | | } |
| | | // 计ç®å·¥ä½æ¶é¿ï¼å°æ¶ï¼ |
| | | const diffMs = endDateTime - startDateTime |
| | | const diffHours = diffMs / (1000 * 60 * 60) |
| | | scheduleForm.workHours = Math.round(diffHours * 100) / 100 |
| | | scheduleForm.workStartTime = dayjs(startDateTime).format("YYYY-MM-DD HH:mm:ss") |
| | | scheduleForm.workEndTime = dayjs(endDateTime).format("YYYY-MM-DD HH:mm:ss") |
| | | |
| | | } |
| | | } |
| | | |
| | | // æäº¤æç表å |
| | | const submitScheduleForm = async () => { |
| | | const valid = await scheduleFormRef.value.validate() |
| | | if (!valid) return |
| | | |
| | | calculateWorkHours() |
| | | const newSchedule = {...scheduleForm} |
| | | |
| | | try { |
| | | await save(newSchedule) |
| | | ElMessage.success('ä¿åæçæå') |
| | | |
| | | handleFilter() |
| | | closeScheduleDialog() |
| | | } catch (err) { |
| | | ElMessage.error('ä¿å失败') |
| | | } |
| | | } |
| | | |
| | | // å 餿ç |
| | | const handleDelete = (row) => { |
| | | ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤ ${row.staffName} çæçè®°å½åï¼`, |
| | | 'å é¤æç¤º', |
| | | { |
| | | confirmButtonText: '确认', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | } |
| | | ).then(() => { |
| | | del(row.id) |
| | | ElMessage.success('å 餿å') |
| | | handleFilter() |
| | | }).catch(() => { |
| | | ElMessage.info('已忶å é¤') |
| | | }) |
| | | } |
| | | |
| | | // æ¹éå é¤ |
| | | const handleBatchDelete = () => { |
| | | if (selectedRows.value.length === 0) { |
| | | ElMessage.warning('è¯·éæ©è¦å é¤çè®°å½') |
| | | return |
| | | } |
| | | |
| | | ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤éä¸ç ${selectedRows.value.length} æ¡æçè®°å½åï¼`, |
| | | 'æ¹éå é¤æç¤º', |
| | | { |
| | | confirmButtonText: '确认', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | } |
| | | ).then(() => { |
| | | delByIds(selectedRows.value.map(item => item.id)) |
| | | handleFilter() |
| | | ElMessage.success('æ¹éå 餿å') |
| | | }).catch(() => { |
| | | ElMessage.info('已忶å é¤') |
| | | }) |
| | | } |
| | | |
| | | // éæ©ååäºä»¶ |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRows.value = selection |
| | | } |
| | | |
| | | // å¯¼åº |
| | | const handleExport = () => { |
| | | let searchForm = { |
| | | ...filterForm, |
| | | ...(filterForm.dateRange.length > 0 && { |
| | | startDate: filterForm.dateRange[0], |
| | | endDate: filterForm.dateRange[1], |
| | | }) |
| | | } |
| | | proxy.download('/staff/staffScheduling/export', {}, '人åæç.xlsx') |
| | | } |
| | | |
| | | // çå½å¨æ |
| | | onMounted(() => { |
| | | // 页é¢åå§å |
| | | handleFilter() |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .scheduling-container { |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | min-height: 100vh; |
| | | } |
| | | |
| | | .page-header { |
| | | text-align: center; |
| | | margin-bottom: 30px; |
| | | padding: 20px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | border-radius: 12px; |
| | | color: white; |
| | | } |
| | | |
| | | .page-header h2 { |
| | | color: white; |
| | | margin-bottom: 10px; |
| | | font-size: 28px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .page-header p { |
| | | color: rgba(255, 255, 255, 0.9); |
| | | font-size: 14px; |
| | | margin: 0 0 15px 0; |
| | | } |
| | | |
| | | .header-controls { |
| | | display: flex; |
| | | justify-content: center; |
| | | gap: 15px; |
| | | } |
| | | |
| | | .filter-section { |
| | | background: white; |
| | | padding: 20px; |
| | | border-radius: 8px; |
| | | margin-bottom: 20px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .filter-form { |
| | | margin: 0; |
| | | } |
| | | |
| | | .table-section { |
| | | background: white; |
| | | border-radius: 8px; |
| | | overflow: hidden; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .batch-actions { |
| | | background: white; |
| | | padding: 15px 20px; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | :deep(.el-form-item__label) { |
| | | font-weight: 500; |
| | | color: #303133; |
| | | } |
| | | |
| | | :deep(.el-input__wrapper) { |
| | | box-shadow: 0 0 0 1px #dcdfe6 inset; |
| | | } |
| | | |
| | | :deep(.el-input__wrapper:hover) { |
| | | box-shadow: 0 0 0 1px #c0c4cc inset; |
| | | } |
| | | |
| | | :deep(.el-input__wrapper.is-focus) { |
| | | box-shadow: 0 0 0 1px #409eff inset; |
| | | } |
| | | |
| | | /* ååºå¼è®¾è®¡ */ |
| | | @media (max-width: 768px) { |
| | | .scheduling-container { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .page-header { |
| | | padding: 15px; |
| | | } |
| | | |
| | | .page-header h2 { |
| | | font-size: 24px; |
| | | } |
| | | |
| | | .header-controls { |
| | | flex-direction: column; |
| | | gap: 10px; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .filter-form .el-form-item { |
| | | margin-bottom: 10px; |
| | | } |
| | | } |
| | | </style> |