更新若干事项

This commit is contained in:
hero920103 committed 2026-10-26 08:54:34 +08:00
1 parent 68cdaf38fa
commit 9898962836
30 files changed
+7715

No files matched your search

@@ -0,0 +1,314 @@
<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>