Files
yunzerwebsiteallinone/backend/src/views/apps/oa/scheduling/components/shiftDialog.vue
T
2026-10-26 08:54:34 +08:00

315 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<el-dialog
:model-value="modelValue"
title="班次管理"
width="900px"
destroy-on-close
@update:model-value="emit('update:modelValue', $event)"
@open="loadShifts"
>
<div class="shift-toolbar">
<div class="shift-tip">班次用于定义上下班时间与休息时段,可被排班方案和临时调配复用。</div>
<el-button type="primary" :icon="Plus" @click="openCreate">新增班次</el-button>
</div>
<el-table :data="shifts" v-loading="loading" size="small" max-height="320">
<el-table-column label="班次" min-width="150">
<template #default="{ row }">
<span class="shift-dot" :style="{ background: row.color }" />
<span>{{ row.name }}</span>
<el-tag v-if="row.is_default === 1" size="small" type="success" class="ml6">默认</el-tag>
</template>
</el-table-column>
<el-table-column prop="short_name" label="简称" width="90" />
<el-table-column label="上班时间" width="150">
<template #default="{ row }">{{ row.work_start }} - {{ row.work_end }}</template>
</el-table-column>
<el-table-column label="休息时段" width="140">
<template #default="{ row }">
{{ row.rest_start ? `${row.rest_start} - ${row.rest_end}` : "无" }}
</template>
</el-table-column>
<el-table-column label="工时" width="80">
<template #default="{ row }">{{ row.work_hours }}h</template>
</el-table-column>
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'" size="small">
{{ row.status === 1 ? "启用" : "停用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="130" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openEdit(row)">编辑</el-button>
<el-button link type="danger" @click="removeShift(row)">删除</el-button>
</template>
</el-table-column>
<template #empty><el-empty description="还没有班次,请先新增一个" :image-size="60" /></template>
</el-table>
<div v-if="formVisible" class="shift-form">
<div class="form-title">{{ editingID ? "编辑班次" : "新增班次" }}</div>
<el-form ref="formRef" :model="form" :rules="rules" label-width="92px">
<div class="form-grid">
<el-form-item label="班次名称" prop="name">
<el-input v-model="form.name" placeholder="如:早班 / 晚班 / 行政班" maxlength="50" />
</el-form-item>
<el-form-item label="简称">
<el-input v-model="form.short_name" placeholder="看板色块上的短文本" maxlength="20" />
</el-form-item>
<el-form-item label="上班时间" prop="work_start">
<el-time-picker v-model="form.work_start" value-format="HH:mm" format="HH:mm" placeholder="上班" style="width: 100%" />
</el-form-item>
<el-form-item label="下班时间" prop="work_end">
<el-time-picker v-model="form.work_end" value-format="HH:mm" format="HH:mm" placeholder="下班" style="width: 100%" />
</el-form-item>
<el-form-item label="休息开始">
<el-time-picker v-model="form.rest_start" value-format="HH:mm" format="HH:mm" placeholder="选填" clearable style="width: 100%" />
</el-form-item>
<el-form-item label="休息结束">
<el-time-picker v-model="form.rest_end" value-format="HH:mm" format="HH:mm" placeholder="选填" clearable style="width: 100%" />
</el-form-item>
<el-form-item label="标识颜色">
<el-color-picker v-model="form.color" :predefine="COLORS" />
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.sort" :min="0" :max="999" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="默认班次">
<el-switch v-model="form.is_default" />
</el-form-item>
<el-form-item label="状态">
<el-radio-group v-model="form.status">
<el-radio :value="1">启用</el-radio>
<el-radio :value="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注" class="span-2">
<el-input v-model="form.remark" maxlength="255" placeholder="选填" />
</el-form-item>
</div>
<div class="form-actions">
<span class="hours-tip">自动计算工时:{{ computedHours }} 小时</span>
<el-button @click="formVisible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="saveShift">保存班次</el-button>
</div>
</el-form>
</div>
</el-dialog>
</template>
<script setup>
import { computed, reactive, ref } from 'vue'
import { Plus } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { createShift, deleteShift, getShiftList, updateShift } from '@/api/oaScheduling'
defineProps({ modelValue: Boolean })
const emit = defineEmits(['update:modelValue', 'changed'])
const COLORS = ['#409EFF', '#67C23A', '#E6A23C', '#F56C6C', '#909399', '#9254DE', '#13C2C2']
const loading = ref(false)
const saving = ref(false)
const shifts = ref([])
const formVisible = ref(false)
const editingID = ref(0)
const formRef = ref(null)
const form = reactive({
name: '',
short_name: '',
color: '#409EFF',
work_start: '09:00',
work_end: '18:00',
rest_start: '12:00',
rest_end: '13:00',
is_default: false,
status: 1,
sort: 0,
remark: ''
})
const rules = {
name: [{ required: true, message: '请输入班次名称', trigger: 'blur' }],
work_start: [{ required: true, message: '请选择上班时间', trigger: 'change' }],
work_end: [{ required: true, message: '请选择下班时间', trigger: 'change' }]
}
function unwrap(res) {
if (res && typeof res === 'object' && 'code' in res) {
if (Number(res.code) !== 200) throw new Error(res.msg || '请求失败')
return res.data
}
return res
}
const computedHours = computed(() => {
const toMinutes = (value) => {
if (!value) return null
const [hour, minute] = String(value).split(':').map(Number)
if (Number.isNaN(hour) || Number.isNaN(minute)) return null
return hour * 60 + minute
}
let start = toMinutes(form.work_start)
let end = toMinutes(form.work_end)
if (start === null || end === null) return 0
if (end <= start) end += 24 * 60
let span = end - start
let restStart = toMinutes(form.rest_start)
let restEnd = toMinutes(form.rest_end)
if (restStart !== null && restEnd !== null) {
if (restEnd <= restStart) restEnd += 24 * 60
const overlap = Math.max(0, Math.min(restEnd, end) - Math.max(restStart, start))
span -= overlap
}
return Math.max(0, Math.round((span / 60) * 100) / 100)
})
async function loadShifts() {
loading.value = true
try {
const data = unwrap(await getShiftList()) || {}
shifts.value = data.list || []
} catch (error) {
ElMessage.error(error.message || '加载班次失败')
} finally {
loading.value = false
}
}
function openCreate() {
editingID.value = 0
Object.assign(form, {
name: '', short_name: '', color: '#409EFF',
work_start: '09:00', work_end: '18:00', rest_start: '12:00', rest_end: '13:00',
is_default: false, status: 1, sort: shifts.value.length, remark: ''
})
formVisible.value = true
}
function openEdit(row) {
editingID.value = row.id
Object.assign(form, {
name: row.name,
short_name: row.short_name || '',
color: row.color || '#409EFF',
work_start: row.work_start,
work_end: row.work_end,
rest_start: row.rest_start || '',
rest_end: row.rest_end || '',
is_default: row.is_default === 1,
status: row.status,
sort: row.sort || 0,
remark: row.remark || ''
})
formVisible.value = true
}
async function saveShift() {
if (!formRef.value) return
const valid = await formRef.value.validate().catch(() => false)
if (!valid) return
if (form.rest_start && !form.rest_end) {
ElMessage.warning('请选择休息结束时间')
return
}
if (!form.rest_start && form.rest_end) {
ElMessage.warning('请选择休息开始时间')
return
}
saving.value = true
try {
const payload = { ...form, work_hours: computedHours.value }
if (editingID.value) {
unwrap(await updateShift(editingID.value, payload))
ElMessage.success('班次已更新')
} else {
unwrap(await createShift(payload))
ElMessage.success('班次已创建')
}
formVisible.value = false
await loadShifts()
emit('changed')
} catch (error) {
ElMessage.error(error.message || '保存班次失败')
} finally {
saving.value = false
}
}
async function removeShift(row) {
try {
await ElMessageBox.confirm(`确认删除班次「${row.name}」?已被排班使用的班次不能删除。`, '删除班次', {
type: 'warning'
})
} catch {
return
}
try {
unwrap(await deleteShift(row.id))
ElMessage.success('班次已删除')
await loadShifts()
emit('changed')
} catch (error) {
ElMessage.error(error.message || '删除失败')
}
}
defineExpose({ loadShifts })
</script>
<style scoped lang="less">
.shift-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.shift-tip {
color: #909399;
font-size: 13px;
}
.shift-dot {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
margin-right: 6px;
}
.ml6 {
margin-left: 6px;
}
.shift-form {
margin-top: 16px;
padding: 16px;
border-radius: 8px;
background: #f7f9fc;
border: 1px solid #ebeef5;
}
.form-title {
font-weight: 600;
margin-bottom: 12px;
}
.form-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 0 16px;
}
.span-2 {
grid-column: span 3;
}
.form-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.hours-tip {
margin-right: auto;
color: #909399;
font-size: 13px;
}
</style>