新增生产-前缫看台排班,并导入机组车台与看台排班数据

功能:
- 后端新增 KtpbCtgs / KtpbDcgs / KtpbZbzs 三张表、KtpbService 与 api/prod/ktpb 接口
  (车头工、挡车工、值班长分页查询与维护,挡车工按车间+班次+机组整组替换保存)
- 挡车数按 F9 口径自动重算:接壤车台各计 0.5,保存时服务端重新计算并校验车台范围
- 前端新增 /prod/qianxian/ktpb 看台排班页(三页签 + 车间/班次/机组过滤 + 批量编辑弹窗)
- 新增通用职工选择弹窗 EmployeeSelect(单选/多选,按部门过滤)
- 生产-前缫菜单下新增「看台排班」,权限 prod:ktpb:list

数据导入:
- 重新生成 biz_prod_qs_jzct.json,新增 ktpb_ctg / ktpb_dcg / ktpb_zbz 三个内嵌资源
- F9 部门 Id 经 DeptData 映射到本系统部门;职工按原始雪花 Id 关联
- 剔除软删除/脏数据:机组车台 24 条、车头工 33 条、挡车工 202 条(34 组)、值班长 7 条
- 导入幂等:机组车台按车间+机组 upsert,排班按业务唯一键去重,已维护的组不覆盖

其他:同步工作进度-12 文档与开发数据库快照
This commit is contained in:
yanpeng200212
2026-10-22 10:49:13 +08:00
parent a8890c81e9
commit a30d1cdcce
16 changed files with 2123 additions and 422 deletions
@@ -0,0 +1,144 @@
<script setup>
/**
* 职工选择弹窗(单选 / 多选)
* 对应 WinForms 端的 F9ModalPageUserControl 职工选择页,数据源为 /api/sys/employee。
*/
import { ref, watch } from 'vue';
import { ElMessage } from 'element-plus';
import request from '../../utils/request';
const props = defineProps({
modelValue: Boolean,
title: { type: String, default: '选择职工' },
multiple: Boolean,
depId: { type: [Number, String], default: 0 },
width: { type: String, default: '820px' },
});
const emit = defineEmits(['update:modelValue', 'confirm']);
const visible = ref(false);
const loading = ref(false);
const rows = ref([]);
const total = ref(0);
const keyword = ref('');
const pageIndex = ref(1);
const pageSize = ref(20);
const current = ref(null);
const selected = ref([]);
watch(() => props.modelValue, async (v) => {
visible.value = v;
if (!v) return;
keyword.value = '';
pageIndex.value = 1;
current.value = null;
selected.value = [];
await query();
});
watch(visible, (v) => emit('update:modelValue', v));
async function query() {
loading.value = true;
try {
const res = await request.get('/sys/employee', {
params: {
depId: props.depId || 0,
like: keyword.value,
pageIndex: pageIndex.value,
pageSize: pageSize.value,
},
});
rows.value = res.data?.items || [];
total.value = Number(res.data?.total || 0);
} catch {
rows.value = [];
total.value = 0;
} finally {
loading.value = false;
}
}
function onSearch() {
pageIndex.value = 1;
query();
}
function onPageChange(p) {
pageIndex.value = p;
query();
}
function onRowClick(row) {
if (props.multiple) return;
current.value = row;
}
function onRowDblClick(row) {
if (props.multiple) return;
current.value = row;
confirm();
}
function confirm() {
const picked = props.multiple ? selected.value : current.value ? [current.value] : [];
if (!picked.length) {
ElMessage.warning('请选择职工');
return;
}
visible.value = false;
emit('confirm', props.multiple ? picked.map((x) => ({ ...x })) : { ...picked[0] });
}
</script>
<template>
<el-dialog v-model="visible" :title="title" :width="width" append-to-body destroy-on-close>
<div class="es-toolbar">
<el-input
v-model="keyword"
placeholder="姓名 / 工号"
clearable
style="width: 200px"
@keyup.enter="onSearch"
@clear="onSearch"
/>
<el-button type="primary" @click="onSearch">查询</el-button>
<span v-if="multiple" class="es-tip">已选 {{ selected.length }} 人</span>
</div>
<el-table
v-loading="loading"
:data="rows"
border
size="small"
height="400"
highlight-current-row
@row-click="onRowClick"
@row-dblclick="onRowDblClick"
@selection-change="selected = $event"
>
<el-table-column v-if="multiple" type="selection" width="45" />
<el-table-column type="index" label="序号" width="55" />
<el-table-column prop="name" label="姓名" width="110" />
<el-table-column prop="code" label="工号" width="120" />
<el-table-column prop="depName" label="部门" min-width="140" show-overflow-tooltip />
<el-table-column prop="gangwei" label="岗位" min-width="120" show-overflow-tooltip />
</el-table>
<el-pagination
class="es-page"
:current-page="pageIndex"
:page-size="pageSize"
:total="total"
layout="total, prev, pager, next"
@current-change="onPageChange"
/>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="confirm">确定</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.es-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.es-tip { color: var(--el-text-color-secondary); font-size: 13px; }
.es-page { margin-top: 10px; justify-content: flex-end; }
</style>
+1
View File
@@ -65,6 +65,7 @@ export const componentMap = {
'prod/lsjl/index': () => import('../views/prod/lsjl/index.vue'),
'prod/xdjy/index': () => import('../views/prod/xdjy/index.vue'),
'prod/hbjy/index': () => import('../views/prod/hbjy/index.vue'),
'prod/ktpb/index': () => import('../views/prod/ktpb/index.vue'),
'notification/messages/index': () => import('../views/notification/messages/index.vue'),
'notification/subscribe/index': () => import('../views/notification/subscribe/index.vue'),
'notification/type/index': () => import('../views/notification/type/index.vue'),
+650
View File
@@ -0,0 +1,650 @@
<script setup>
/**
* 看台排班(生产-前缫)
* 对应 WinForms 端 WinClient\F9.Business.Prod\QS\KTPB\Page_KTPB
* - 车头工:一个「车间+班次+机组」一条记录(2 名车头工)
* - 挡车工:一个「车间+班次+机组」一组记录,按车台区间分派挡车工,挡车数自动计算
* - 值班长:一个「车间+班次」可有多名值班长
*/
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import request from '../../../utils/request';
import EmployeeSelect from '../../../components/select/EmployeeSelect.vue';
const TABS = [
{ name: 'dcg', label: '挡车工' },
{ name: 'ctg', label: '车头工' },
{ name: 'zbz', label: '值班长' },
];
const BANCI = [
{ value: 1, label: '甲班' },
{ value: 2, label: '乙班' },
];
const tab = ref('dcg');
const deps = ref([]);
const jizuOptions = ref([]);
const filter = reactive({ depId: 0, banci: 1, jizu: 0, keyword: '' });
const state = reactive({
dcg: { rows: [], total: 0, loading: false, pageIndex: 1, pageSize: 20 },
ctg: { rows: [], total: 0, loading: false, pageIndex: 1, pageSize: 20 },
zbz: { rows: [], total: 0, loading: false, pageIndex: 1, pageSize: 20 },
});
const cur = computed(() => state[tab.value]);
const depName = computed(() => deps.value.find((d) => d.id === filter.depId)?.name || '');
const banciName = computed(() => BANCI.find((b) => b.value === filter.banci)?.label || '');
// ==================== 通用查询 ====================
async function loadDeps() {
const res = await request.get('/prod/ktpb/qianxian-deps');
deps.value = res.data || [];
if (deps.value.length && !filter.depId) filter.depId = deps.value[0].id;
}
async function loadJizu() {
if (!filter.depId) {
jizuOptions.value = [];
return;
}
const res = await request.get('/prod/ktpb/jizu-options', { params: { depId: filter.depId } });
jizuOptions.value = res.data || [];
if (!jizuOptions.value.some((x) => x.jizu === filter.jizu)) {
filter.jizu = jizuOptions.value.length ? jizuOptions.value[0].jizu : 0;
}
}
async function loadList(resetPage = false) {
const s = cur.value;
if (resetPage) s.pageIndex = 1;
s.loading = true;
try {
const res = await request.get(`/prod/ktpb/${tab.value}`, {
params: {
depId: filter.depId || 0,
banci: filter.banci || null,
jizu: tab.value === 'zbz' ? null : filter.jizu || null,
like: filter.keyword,
pageIndex: s.pageIndex,
pageSize: s.pageSize,
},
});
s.rows = res.data?.items || [];
s.total = Number(res.data?.total || 0);
} finally {
s.loading = false;
}
}
async function onDepChange() {
await loadJizu();
await loadList(true);
}
async function onSearch() {
await loadList(true);
}
async function onPageChange(p) {
cur.value.pageIndex = p;
await loadList();
}
async function onTabChange(name) {
await loadList();
}
onMounted(async () => {
await loadDeps();
await loadJizu();
await loadList(true);
});
// ==================== 挡车工:整组批量编辑 ====================
const dcgDlg = reactive({
visible: false,
loading: false,
saving: false,
depId: 0,
banci: 1,
jizu: 0,
chetaiStart: 0,
chetaiEnd: 0,
rows: [],
});
const dcgDepName = computed(() => deps.value.find((d) => d.id === dcgDlg.depId)?.name || '');
const dcgBanciName = computed(() => BANCI.find((b) => b.value === dcgDlg.banci)?.label || '');
const dcgTotalChetai = computed(() =>
dcgDlg.chetaiStart && dcgDlg.chetaiEnd ? dcgDlg.chetaiEnd - dcgDlg.chetaiStart + 1 : 0
);
const dcgAssigned = computed(() => {
const set = new Set();
dcgDlg.rows.forEach((r) => {
const s = Number(r.chetaiStart) || 0;
const e = Number(r.chetaiEnd) || 0;
for (let i = s; i <= e; i++) set.add(i);
});
return set.size;
});
async function openDcgGroup() {
if (!filter.depId) return ElMessage.warning('请先选择车间');
if (!filter.jizu) return ElMessage.warning('请先选择机组');
const opt = jizuOptions.value.find((x) => x.jizu === filter.jizu);
dcgDlg.depId = filter.depId;
dcgDlg.banci = filter.banci;
dcgDlg.jizu = filter.jizu;
dcgDlg.chetaiStart = opt?.chetaiStart || 0;
dcgDlg.chetaiEnd = opt?.chetaiEnd || 0;
dcgDlg.visible = true;
dcgDlg.loading = true;
try {
const res = await request.get('/prod/ktpb/dcg/group', {
params: { depId: dcgDlg.depId, banci: dcgDlg.banci, jizu: dcgDlg.jizu },
});
dcgDlg.rows = (res.data || []).map((x) => ({
id: x.id,
ygId: x.ygId,
ygName: x.ygName,
chetaiStart: x.chetaiStart,
chetaiEnd: x.chetaiEnd,
dangcheshu: x.dangcheshu,
}));
recalcDangche(dcgDlg.rows);
} finally {
dcgDlg.loading = false;
}
}
function draftDcgRow(name, start, end) {
dcgDlg.rows.push({ id: 0, ygId: 0, ygName: name, chetaiStart: start, chetaiEnd: end, dangcheshu: 0 });
}
function appendEmployees(picked) {
const exist = new Set(dcgDlg.rows.map((r) => String(r.ygId)));
let next = dcgDlg.chetaiStart;
if (dcgDlg.rows.length) {
next = Math.max(...dcgDlg.rows.map((r) => Number(r.chetaiEnd) || 0)) + 1;
}
picked.forEach((e) => {
if (exist.has(String(e.id))) return;
if (next > dcgDlg.chetaiEnd) next = dcgDlg.chetaiEnd;
draftDcgRow(e.name, next, next);
exist.add(String(e.id));
next += 1;
});
recalcDangche(dcgDlg.rows);
}
function removeDcgRow(index) {
dcgDlg.rows.splice(index, 1);
recalcDangche(dcgDlg.rows);
}
function clearDcgRows() {
dcgDlg.rows = [];
}
/**
* 挡车数计算(与后端 KtpbService.RecalcDangcheshu 完全一致):
* 单台车权重 1;若该车台被多条记录共享,且同时是某记录起点与某记录终点(接壤),权重 0.5。
*/
function recalcDangche(rows) {
const counts = {};
const starts = new Set();
const ends = new Set();
rows.forEach((r) => {
const s = Number(r.chetaiStart) || 0;
const e = Number(r.chetaiEnd) || 0;
if (!s || e < s) return;
starts.add(s);
ends.add(e);
for (let i = s; i <= e; i++) counts[i] = (counts[i] || 0) + 1;
});
rows.forEach((r) => {
const s = Number(r.chetaiStart) || 0;
const e = Number(r.chetaiEnd) || 0;
let total = 0;
for (let i = s; i <= e; i++) {
let w = 1;
if ((counts[i] || 0) > 1 && starts.has(i) && ends.has(i)) w = 0.5;
total += w;
}
r.dangcheshu = Math.round(total * 100) / 100;
});
}
const empPicker = reactive({ visible: false, multiple: false, target: '' });
function pickEmployee(target) {
empPicker.target = target;
empPicker.multiple = target === 'dcg';
empPicker.visible = true;
}
function onEmployeePicked(picked) {
if (empPicker.target === 'dcg') {
appendEmployees(picked);
} else if (empPicker.target === 'ctg1') {
ctgForm.ygid1 = picked.id;
ctgForm.ygid1Name = picked.name;
} else if (empPicker.target === 'ctg2') {
ctgForm.ygid2 = picked.id;
ctgForm.ygid2Name = picked.name;
} else if (empPicker.target === 'zbz') {
zbzForm.ygId = picked.id;
zbzForm.ygName = picked.name;
}
}
async function saveDcgGroup() {
for (const r of dcgDlg.rows) {
if (!r.ygId) return ElMessage.warning('存在未选择挡车工的记录');
if (!r.chetaiStart) return ElMessage.warning(`挡车工「${r.ygName}」请填写起始车台`);
if (!r.chetaiEnd || Number(r.chetaiEnd) < Number(r.chetaiStart)) {
return ElMessage.warning(`挡车工「${r.ygName}」结束车台不能小于起始车台`);
}
if (Number(r.chetaiStart) < dcgDlg.chetaiStart || Number(r.chetaiEnd) > dcgDlg.chetaiEnd) {
return ElMessage.warning(`挡车工「${r.ygName}」车台范围必须在本机组 ${dcgDlg.chetaiStart}-${dcgDlg.chetaiEnd} 内`);
}
}
recalcDangche(dcgDlg.rows);
dcgDlg.saving = true;
try {
const res = await request.post('/prod/ktpb/dcg/batch', {
depId: dcgDlg.depId,
banci: dcgDlg.banci,
jizu: dcgDlg.jizu,
items: dcgDlg.rows.map((r) => ({
ygId: r.ygId,
ygName: r.ygName,
chetaiStart: Number(r.chetaiStart),
chetaiEnd: Number(r.chetaiEnd),
dangcheshu: r.dangcheshu,
})),
});
ElMessage.success(res.message || '保存成功');
dcgDlg.visible = false;
await loadList();
} finally {
dcgDlg.saving = false;
}
}
async function deleteDcg(row) {
await ElMessageBox.confirm(
`确定删除「${row.deptName} ${row.banciName}班 机组${row.jizu}」中 ${row.ygName} 的排班吗?`,
'提示',
{ type: 'warning' }
);
await request.delete(`/prod/ktpb/dcg/${row.id}`);
ElMessage.success('删除成功');
await loadList();
}
// ==================== 车头工 ====================
const ctgDlg = reactive({ visible: false, saving: false, id: 0 });
const ctgForm = reactive({
depId: 0,
banci: 1,
jizu: 0,
ygid1: 0,
ygid1Name: '',
ygid2: 0,
ygid2Name: '',
});
function openCtg(row) {
ctgDlg.id = row?.id || 0;
ctgForm.depId = row?.depId || filter.depId;
ctgForm.banci = row?.banci || filter.banci;
ctgForm.jizu = row?.jizu || filter.jizu;
ctgForm.ygid1 = row?.ygid1 || 0;
ctgForm.ygid1Name = row?.ygid1Name || '';
ctgForm.ygid2 = row?.ygid2 || 0;
ctgForm.ygid2Name = row?.ygid2Name || '';
ctgDlg.visible = true;
}
async function saveCtg() {
if (!ctgForm.depId) return ElMessage.warning('请选择车间');
if (!ctgForm.jizu) return ElMessage.warning('请选择机组');
if (!ctgForm.ygid1) return ElMessage.warning('请选择车头工1');
ctgDlg.saving = true;
try {
const payload = {
depId: ctgForm.depId,
banci: ctgForm.banci,
jizu: ctgForm.jizu,
ygid1: ctgForm.ygid1,
ygid2: ctgForm.ygid2 || 0,
};
if (ctgDlg.id) {
await request.put(`/prod/ktpb/ctg/${ctgDlg.id}`, payload);
ElMessage.success('修改成功');
} else {
await request.post('/prod/ktpb/ctg', payload);
ElMessage.success('新增成功');
}
ctgDlg.visible = false;
await loadList();
} finally {
ctgDlg.saving = false;
}
}
async function deleteCtg(row) {
await ElMessageBox.confirm(`确定删除「${row.deptName} ${row.banciName}班 机组${row.jizu}」的车头工排班吗?`, '提示', {
type: 'warning',
});
await request.delete(`/prod/ktpb/ctg/${row.id}`);
ElMessage.success('删除成功');
await loadList();
}
// ==================== 值班长 ====================
const zbzDlg = reactive({ visible: false, saving: false, id: 0 });
const zbzForm = reactive({ depId: 0, banci: 1, ygId: 0, ygName: '' });
function openZbz(row) {
zbzDlg.id = row?.id || 0;
zbzForm.depId = row?.depId || filter.depId;
zbzForm.banci = row?.banci || filter.banci;
zbzForm.ygId = row?.ygId || 0;
zbzForm.ygName = row?.ygName || '';
zbzDlg.visible = true;
}
async function saveZbz() {
if (!zbzForm.depId) return ElMessage.warning('请选择车间');
if (!zbzForm.ygId) return ElMessage.warning('请选择值班长');
zbzDlg.saving = true;
try {
const payload = { depId: zbzForm.depId, banci: zbzForm.banci, ygId: zbzForm.ygId };
if (zbzDlg.id) {
await request.put(`/prod/ktpb/zbz/${zbzDlg.id}`, payload);
ElMessage.success('修改成功');
} else {
await request.post('/prod/ktpb/zbz', payload);
ElMessage.success('新增成功');
}
zbzDlg.visible = false;
await loadList();
} finally {
zbzDlg.saving = false;
}
}
async function deleteZbz(row) {
await ElMessageBox.confirm(`确定删除「${row.deptName} ${row.banciName}班」值班长 ${row.ygName} 的排班吗?`, '提示', {
type: 'warning',
});
await request.delete(`/prod/ktpb/zbz/${row.id}`);
ElMessage.success('删除成功');
await loadList();
}
</script>
<template>
<div class="ktpb-page">
<el-card shadow="never">
<el-tabs v-model="tab" @tab-change="onTabChange">
<el-tab-pane v-for="t in TABS" :key="t.name" :label="t.label" :name="t.name" />
</el-tabs>
<div class="toolbar">
<span class="lbl">车间</span>
<el-select v-model="filter.depId" placeholder="全部车间" clearable style="width: 160px" @change="onDepChange">
<el-option v-for="d in deps" :key="d.id" :label="d.name" :value="d.id" />
</el-select>
<span class="lbl">班次</span>
<el-select v-model="filter.banci" style="width: 110px" @change="onSearch">
<el-option v-for="b in BANCI" :key="b.value" :label="b.label" :value="b.value" />
</el-select>
<template v-if="tab !== 'zbz'">
<span class="lbl">机组</span>
<el-select v-model="filter.jizu" placeholder="全部机组" clearable style="width: 130px" @change="onSearch">
<el-option
v-for="j in jizuOptions"
:key="j.jizu"
:label="`机组${j.jizu}(${j.chetaiStart}-${j.chetaiEnd})`"
:value="j.jizu"
/>
</el-select>
</template>
<el-input
v-model="filter.keyword"
placeholder="姓名 / 车台"
clearable
style="width: 180px"
@keyup.enter="onSearch"
@clear="onSearch"
/>
<el-button type="primary" @click="onSearch">查询</el-button>
<div class="right">
<el-button v-if="tab === 'dcg'" type="primary" @click="openDcgGroup">排班维护(按机组)</el-button>
<el-button v-else type="primary" @click="tab === 'ctg' ? openCtg(null) : openZbz(null)">新增</el-button>
</div>
</div>
<!-- 挡车工 -->
<el-table v-if="tab === 'dcg'" v-loading="state.dcg.loading" :data="state.dcg.rows" border size="small">
<el-table-column type="index" label="序号" width="55" />
<el-table-column prop="deptName" label="车间" min-width="120" show-overflow-tooltip />
<el-table-column label="班次" width="80" align="center">
<template #default="{ row }">{{ row.banciName }}班</template>
</el-table-column>
<el-table-column prop="jizu" label="机组" width="70" align="center" />
<el-table-column prop="ygName" label="挡车工" width="110" />
<el-table-column label="车台范围" width="110" align="center">
<template #default="{ row }">{{ row.chetaiStart }} - {{ row.chetaiEnd }}</template>
</el-table-column>
<el-table-column prop="dangcheshu" label="挡车数" width="90" align="right" />
<el-table-column label="操作" width="90" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="danger" @click="deleteDcg(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 车头工 -->
<el-table v-else-if="tab === 'ctg'" v-loading="state.ctg.loading" :data="state.ctg.rows" border size="small">
<el-table-column type="index" label="序号" width="55" />
<el-table-column prop="deptName" label="车间" min-width="120" show-overflow-tooltip />
<el-table-column label="班次" width="80" align="center">
<template #default="{ row }">{{ row.banciName }}班</template>
</el-table-column>
<el-table-column prop="jizu" label="机组" width="70" align="center" />
<el-table-column prop="ygid1Name" label="车头工1" width="120" />
<el-table-column prop="ygid2Name" label="车头工2" width="120" />
<el-table-column label="操作" width="130" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openCtg(row)">修改</el-button>
<el-button link type="danger" @click="deleteCtg(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 值班长 -->
<el-table v-else v-loading="state.zbz.loading" :data="state.zbz.rows" border size="small">
<el-table-column type="index" label="序号" width="55" />
<el-table-column prop="deptName" label="车间" min-width="140" show-overflow-tooltip />
<el-table-column label="班次" width="90" align="center">
<template #default="{ row }">{{ row.banciName }}班</template>
</el-table-column>
<el-table-column prop="ygName" label="值班长" width="140" />
<el-table-column label="操作" width="130" align="center" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="openZbz(row)">修改</el-button>
<el-button link type="danger" @click="deleteZbz(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
class="pager"
:current-page="cur.pageIndex"
:page-size="cur.pageSize"
:total="cur.total"
layout="total, prev, pager, next, jumper"
@current-change="onPageChange"
/>
</el-card>
<!-- ============ 挡车工整组排班 ============ -->
<el-dialog v-model="dcgDlg.visible" title="挡车工排班维护" width="880px" destroy-on-close>
<div class="dlg-head">
<el-tag type="info">{{ dcgDepName }}</el-tag>
<el-tag type="info">{{ dcgBanciName }}</el-tag>
<el-tag type="info">机组 {{ dcgDlg.jizu }}(车台 {{ dcgDlg.chetaiStart }}-{{ dcgDlg.chetaiEnd }})</el-tag>
<el-tag :type="dcgAssigned === dcgTotalChetai ? 'success' : 'warning'">
已分配 {{ dcgAssigned }} / {{ dcgTotalChetai }} 台
</el-tag>
</div>
<div class="dlg-toolbar">
<el-button type="primary" size="small" @click="pickEmployee('dcg')">添加挡车工</el-button>
<el-button size="small" @click="clearDcgRows">清空</el-button>
<span class="hint">车台结束号与下一行起始号接壤时,该车台各按 0.5 台计(挡车数自动计算)</span>
</div>
<el-table v-loading="dcgDlg.loading" :data="dcgDlg.rows" border size="small" height="380">
<el-table-column type="index" label="序号" width="55" />
<el-table-column label="挡车工" min-width="150">
<template #default="{ row, $index }">
<span v-if="row.ygName">{{ row.ygName }}</span>
<el-button v-else link type="primary" @click="pickEmployee('dcg')">选择</el-button>
</template>
</el-table-column>
<el-table-column label="起始车台" width="120" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.chetaiStart"
:min="dcgDlg.chetaiStart"
:max="dcgDlg.chetaiEnd"
:controls="false"
size="small"
style="width: 90px"
@change="recalcDangche(dcgDlg.rows)"
/>
</template>
</el-table-column>
<el-table-column label="结束车台" width="120" align="center">
<template #default="{ row }">
<el-input-number
v-model="row.chetaiEnd"
:min="dcgDlg.chetaiStart"
:max="dcgDlg.chetaiEnd"
:controls="false"
size="small"
style="width: 90px"
@change="recalcDangche(dcgDlg.rows)"
/>
</template>
</el-table-column>
<el-table-column prop="dangcheshu" label="挡车数" width="90" align="right" />
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" @click="removeDcgRow($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
<template #footer>
<el-button @click="dcgDlg.visible = false">取消</el-button>
<el-button type="primary" :loading="dcgDlg.saving" @click="saveDcgGroup">保存</el-button>
</template>
</el-dialog>
<!-- ============ 车头工 ============ -->
<el-dialog v-model="ctgDlg.visible" :title="ctgDlg.id ? '修改车头工排班' : '新增车头工排班'" width="520px">
<el-form label-width="90px">
<el-form-item label="车间">
<el-select v-model="ctgForm.depId" placeholder="请选择车间" style="width: 100%">
<el-option v-for="d in deps" :key="d.id" :label="d.name" :value="d.id" />
</el-select>
</el-form-item>
<el-form-item label="班次">
<el-select v-model="ctgForm.banci" style="width: 100%">
<el-option v-for="b in BANCI" :key="b.value" :label="b.label" :value="b.value" />
</el-select>
</el-form-item>
<el-form-item label="机组">
<el-input-number v-model="ctgForm.jizu" :min="1" :max="99" :controls="false" style="width: 100%" />
</el-form-item>
<el-form-item label="车头工1">
<el-input :model-value="ctgForm.ygid1Name" readonly placeholder="点击选择">
<template #append>
<el-button @click="pickEmployee('ctg1')">选择</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item label="车头工2">
<el-input :model-value="ctgForm.ygid2Name" readonly placeholder="点击选择(可空)">
<template #append>
<el-button @click="pickEmployee('ctg2')">选择</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="ctgDlg.visible = false">取消</el-button>
<el-button type="primary" :loading="ctgDlg.saving" @click="saveCtg">保存</el-button>
</template>
</el-dialog>
<!-- ============ 值班长 ============ -->
<el-dialog v-model="zbzDlg.visible" :title="zbzDlg.id ? '修改值班长排班' : '新增值班长排班'" width="480px">
<el-form label-width="90px">
<el-form-item label="车间">
<el-select v-model="zbzForm.depId" placeholder="请选择车间" style="width: 100%">
<el-option v-for="d in deps" :key="d.id" :label="d.name" :value="d.id" />
</el-select>
</el-form-item>
<el-form-item label="班次">
<el-select v-model="zbzForm.banci" style="width: 100%">
<el-option v-for="b in BANCI" :key="b.value" :label="b.label" :value="b.value" />
</el-select>
</el-form-item>
<el-form-item label="值班长">
<el-input :model-value="zbzForm.ygName" readonly placeholder="点击选择">
<template #append>
<el-button @click="pickEmployee('zbz')">选择</el-button>
</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="zbzDlg.visible = false">取消</el-button>
<el-button type="primary" :loading="zbzDlg.saving" @click="saveZbz">保存</el-button>
</template>
</el-dialog>
<EmployeeSelect
v-model="empPicker.visible"
:multiple="empPicker.multiple"
:dep-id="dcgDlg.visible ? dcgDlg.depId : filter.depId"
@confirm="onEmployeePicked"
/>
</div>
</template>
<style scoped>
.ktpb-page :deep(.el-card__body) { padding-top: 6px; }
.toolbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }
.toolbar .lbl { color: var(--el-text-color-regular); font-size: 13px; }
.toolbar .right { margin-left: auto; }
.pager { margin-top: 10px; justify-content: flex-end; }
.dlg-head { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
.dlg-toolbar { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.dlg-toolbar .hint { color: var(--el-text-color-secondary); font-size: 12px; }
</style>