Files
bj_power/bj_power_mes/frontend/src/pages/DailyPlan.vue
T
SunYF 37f10d3a13 refactor: 重构产线工位与备料流程,移除冗余字段
1. 移除工单、工件、日排产中的冗余工位组合/接驳台字段
2. 新增工位接驳台编码字段与唯一约束,关联WMS接驳台主数据
3. 重构日排产为工位产量分配模式,替代原接驳台列表
4. 新增备料单工位级字段与分批出库支持
5. 移除定时同步可生产数量,改为WMS实时拉取接口
6. 过滤操作日志,排除登录/退出相关日志
7. 调整仪表盘工序展示逻辑为今日派工路线
8. 新增接驳台维护菜单与基础数据
2026-09-19 07:50:21 +08:00

311 lines
15 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-card>
<el-form inline>
<el-form-item label="工单号">
<el-select v-model="query.orderNo" filterable allow-create clearable placeholder="请选择/输入工单号" style="width:220px" @change="load">
<el-option v-for="w in workOrders" :key="w.workOrderNo" :label="w.workOrderNo" :value="w.workOrderNo" />
</el-select>
</el-form-item>
<el-form-item label="排产日期">
<el-date-picker v-model="query.planDate" type="date" value-format="YYYY-MM-DD" placeholder="请选择日期" @change="load" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" type="success" @click="openAdd">新建排产</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border size="small">
<el-table-column prop="orderNo" label="工单号" min-width="180" />
<el-table-column prop="planDate" label="排产日期" width="120" />
<el-table-column prop="planQty" label="计划数量" width="100" />
<el-table-column prop="completedQty" label="已完成" width="90" />
<el-table-column label="送达接驳台(派生)" min-width="160">
<template #default="{ row }">
<el-tag v-for="d in (dockByDate[row.planDate] || [])" :key="d" size="small" class="dock-tag">{{ d }}</el-tag>
<span v-if="!(dockByDate[row.planDate] || []).length" class="dock-empty">未派工</span>
</template>
</el-table-column>
<el-table-column label="执行状态" width="110">
<template #default="{ row }">
<el-tag :type="planStatusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="operator" label="操作人" min-width="90" />
<el-table-column label="操作" width="330">
<template #default="{ row }">
<el-button size="small" type="primary" link @click="openDetail(row)">详情</el-button>
<el-button v-if="can('produce.workorder:dailyplan') && (row.status === 'PENDING' || row.status === 'CONFIRMED')"
size="small" :type="row.status === 'CONFIRMED' ? 'warning' : 'success'" @click="toggleStatus(row)">
{{ row.status === 'CONFIRMED' ? '停用' : '启用' }}
</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" size="small" @click="openEdit(row)">编辑</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" size="small" type="danger" @click="del(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dialog.show" :title="dialog.id ? '编辑排产' : '新建排产'" width="600px">
<el-form :model="dialog" label-width="92px">
<el-form-item label="工单号">
<el-select v-model="dialog.orderNo" filterable allow-create placeholder="请选择/输入工单号" style="width:100%" @change="onOrderChange">
<el-option v-for="w in workOrders" :key="w.workOrderNo" :label="w.workOrderNo" :value="w.workOrderNo" />
</el-select>
</el-form-item>
<!-- 选工单号后自动带出只读 -->
<el-form-item label="产品型号"><el-input :model-value="dialog.productType" disabled placeholder="随工单号自动带出" /></el-form-item>
<el-form-item label="产品编号"><el-input :model-value="dialog.productCode" disabled placeholder="随工单号自动带出" /></el-form-item>
<el-form-item label="产品名称"><el-input :model-value="dialog.productName" disabled placeholder="随工单号自动带出" /></el-form-item>
<el-form-item label="工单数量"><el-input :model-value="dialog.woQty" disabled placeholder="随工单号自动带出" /></el-form-item>
<!-- 新建日期范围 + 排产模式一排多天参考工单自动排产」) -->
<template v-if="!dialog.id">
<el-form-item label="排产日期范围">
<el-date-picker v-model="dialog.planRange" type="daterange" value-format="YYYY-MM-DD"
start-placeholder="开始日期" end-placeholder="结束日期" style="width:100%" />
</el-form-item>
<el-form-item label="排产模式">
<el-radio-group v-model="dialog.mode">
<el-radio value="AVG">平均分摊</el-radio>
<el-radio value="FIXED">固定日产</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="dialog.mode === 'FIXED' ? '每日产量' : '总数量'">
<el-input-number v-model="dialog.planQty" :min="1" style="width:100%" />
</el-form-item>
<el-alert type="info" :closable="false" style="margin-bottom:12px"
:title="`共 ${previewDates.length} 天:${previewDates.join('、') || '-'}`" />
<el-alert type="info" :closable="false" style="margin-bottom:12px"
title="送达接驳台与工位产量分配由「工位派工」按排产日自动派生,此处无需手填;如需调整工位产量,启用后点击「编辑」设置。" />
</template>
<!-- 编辑单日 -->
<template v-else>
<el-form-item label="排产日期">
<el-date-picker v-model="dialog.planDate" type="date" value-format="YYYY-MM-DD" style="width:100%" @change="loadAlloc" />
</el-form-item>
<el-form-item label="计划数量">
<el-input-number v-model="dialog.planQty" :min="1" style="width:100%" />
</el-form-item>
<el-form-item label="工位产量分配">
<div v-if="dispatchStations.length" style="width:100%">
<div v-for="st in dispatchStations" :key="st" style="display:flex;gap:8px;align-items:center;margin-bottom:6px">
<span style="width:150px">工位{{ st }}<template v-if="stationNameMap[st]">{{ stationNameMap[st] }}</template></span>
<el-input-number v-model="alloc[st]" :min="0" :max="dialog.planQty" controls-position="right" style="width:150px" />
<span style="color:#909399;font-size:12px"></span>
</div>
<div style="font-size:12px;color:#909399">已分配合计{{ allocTotal }} / 计划 {{ dialog.planQty }}建议相等留空或 0 的工位不分担产量</div>
</div>
<el-alert v-else type="info" :closable="false" title="该排产日暂无工位派工,无法分配工位产量;请先在「工位」页完成当日工位↔工序派工。" />
</el-form-item>
</template>
</el-form>
<template #footer>
<el-button @click="dialog.show=false">取消</el-button>
<el-button v-if="can('produce.workorder:dailyplan')" type="primary" @click="save">保存</el-button>
</template>
</el-dialog>
<!-- 排产详情只读列表点详情打开不可编辑 -->
<DetailDrawer :visible="detail.show" @update:visible="detail.show = $event"
:title="detail.title" :fields="detail.fields" :data="detail.row" />
</el-card>
<PageHelp :data="helpDailyPlan" page="DailyPlan" />
</template>
<style scoped>
.dock-tag { margin-right: 4px; }
.dock-empty { color: #999; font-size: 12px; }
</style>
<script setup>
import { reactive, ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import PageHelp from '../components/PageHelp.vue'
import DetailDrawer from '../components/DetailDrawer.vue'
import { helpDailyPlan } from '../help'
const query = reactive({ orderNo: '', planDate: '' })
const list = ref([])
const workOrders = ref([])
const productTypes = ref([])
const dialog = reactive({ show: false, id: 0, orderNo: '', planDate: '', planRange: [], mode: 'AVG', planQty: 1, productType: '', productCode: '', productName: '', woQty: '', stationPlanQty: {} })
// 工位派工派生:工位号→接驳台编码、工位号→名称
const stationDockMap = ref({})
const stationNameMap = ref({})
// 排产日→送达接驳台列表(派生展示)
const dockByDate = ref({})
// 编辑时工位产量分配
const dispatchStations = ref([])
const alloc = reactive({})
const allocTotal = computed(() => Object.values(alloc).reduce((a, b) => a + (Number(b) || 0), 0))
// 新建排产「日期范围 + 模式」预览:展开为去重升序日期列表(仅前端提示,后端以 planRange 为准)
const previewDates = computed(() => {
const r = dialog.planRange
if (!r || r.length !== 2 || !r[0] || !r[1]) return []
const p = (n) => String(n).padStart(2, '0')
const start = new Date(r[0] + 'T00:00:00')
const end = new Date(r[1] + 'T00:00:00')
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || end < start) return []
const out = []
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
out.push(`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`)
}
return out
})
const statusOptions = [
{ v: 'PENDING', l: '未启用' }, { v: 'CONFIRMED', l: '已启用' },
{ v: 'PROCESSING', l: '执行中' }, { v: 'DONE', l: '已完成' }, { v: 'CANCELLED', l: '已取消' }
]
const statusText = (s) => (statusOptions.find((x) => x.v === s) || {}).l || s
const planStatusTag = (s) =>
s === 'DONE' ? 'success' : s === 'PROCESSING' ? 'warning' : s === 'CONFIRMED' ? 'primary' : 'info'
async function loadStations() {
const sts = (await request.get('/stations')) || []
const dm = {}
const nm = {}
sts.forEach((s) => { dm[s.stationNo] = s.dockCode || ''; nm[s.stationNo] = s.name })
stationDockMap.value = dm
stationNameMap.value = nm
}
async function load() {
list.value = (await request.get('/daily-plans', { params: query })) || []
await refreshDocks()
}
// 根据各排产日的工位派工路线,派生送达接驳台(展示用,来源于 station_process + station.dockCode
async function refreshDocks() {
const dates = [...new Set(list.value.map((r) => r.planDate))]
const out = {}
for (const d of dates) {
const r = await request.get('/station-process/route', { params: { effectDate: d, shift: '' } })
const stations = (r && r.stations) || []
const seen = {}
const ds = []
stations.forEach((n) => {
const dc = stationDockMap.value[n]
if (dc && !seen[dc]) { seen[dc] = true; ds.push(dc) }
})
out[d] = ds
}
dockByDate.value = out
}
// 加载某排产日的工位派工,初始化工位产量分配
async function loadAlloc(date) {
const r = await request.get('/station-process/route', { params: { effectDate: date, shift: '' } })
const stations = (r && r.stations) || []
dispatchStations.value = stations
const init = {}
stations.forEach((st) => { init[st] = 0 })
const spq = dialog.stationPlanQty || {}
const hasAny = stations.some((st) => spq[String(st)] != null)
if (hasAny) {
stations.forEach((st) => { if (spq[String(st)] != null) init[st] = spq[String(st)] })
} else if (stations.length) {
const base = Math.floor(dialog.planQty / stations.length)
stations.forEach((st, i) => { init[st] = base + (i < dialog.planQty % stations.length ? 1 : 0) })
}
Object.keys(alloc).forEach((k) => { delete alloc[k] })
Object.assign(alloc, init)
}
// 选工单号自动带出其产品信息(只读展示,避免手填错位)
function loadWorkOrderInfo() {
const wo = workOrders.value.find((w) => w.workOrderNo === dialog.orderNo)
if (!wo) {
dialog.productType = ''; dialog.productCode = ''; dialog.productName = ''; dialog.woQty = ''
return
}
dialog.productCode = wo.productCode || ''
dialog.productName = wo.productName || ''
dialog.woQty = wo.quantity || 0
const pt = productTypes.value.find((p) => p.id === wo.productTypeId)
dialog.productType = pt ? (pt.name + (pt.code ? ' / ' + pt.code : '')) : ''
}
function onOrderChange() {
loadWorkOrderInfo()
}
function openAdd() {
const today = new Date().toISOString().slice(0, 10)
Object.assign(dialog, {
show: true, id: 0, orderNo: '', planDate: '', planRange: [today, today], mode: 'AVG',
planQty: 1, productType: '', productCode: '', productName: '', woQty: '', stationPlanQty: {}
})
}
function openEdit(row) {
const spq = row.stationPlanQty || {}
Object.assign(dialog, {
show: true, id: row.id, orderNo: row.orderNo, planDate: row.planDate, planRange: [],
mode: 'AVG', planQty: row.planQty, productType: '', productCode: '', productName: '', woQty: '', stationPlanQty: spq
})
loadWorkOrderInfo()
loadAlloc(row.planDate)
}
async function save() {
if (!dialog.orderNo) return ElMessage.warning('请选择工单号')
if (dialog.id) {
// 编辑单条日排产:工位产量分配按派工工位拆分
const spq = {}
dispatchStations.value.forEach((st) => {
if (alloc[st]) spq[String(st)] = Number(alloc[st]) || 0
})
if (!dialog.planDate) return ElMessage.warning('请选择排产日期')
await request.put(`/daily-plans/${dialog.id}`, {
id: dialog.id, orderNo: dialog.orderNo, planDate: dialog.planDate, planQty: dialog.planQty, stationPlanQty: spq
})
} else {
// 新建批量:按日期范围 + 模式生成多天日排产
if (!dialog.planRange || dialog.planRange.length !== 2 || !dialog.planRange[0] || !dialog.planRange[1])
return ElMessage.warning('请选择排产日期范围')
if (!dialog.planQty || dialog.planQty < 1) return ElMessage.warning('请填写数量')
await request.post('/daily-plans', {
orderNo: dialog.orderNo, planRange: dialog.planRange, mode: dialog.mode, planQty: dialog.planQty
})
}
ElMessage.success('保存成功')
dialog.show = false
load()
}
// 排产详情(只读,列表点「详情」打开)
const detail = reactive({ show: false, title: '排产详情', fields: [], row: null })
function openDetail(row) {
detail.fields = [
{ key: 'orderNo', label: '工单号' },
{ key: 'planDate', label: '排产日期' },
{ key: 'planQty', label: '计划数量' },
{ key: 'completedQty', label: '已完成' },
{ key: 'status', label: '执行状态', type: 'enum', map: statusOptions.reduce((m, x) => { m[x.v] = x.l; return m }, {}) },
{ key: 'operator', label: '操作人' }
]
detail.row = row
detail.show = true
}
async function del(row) {
await ElMessageBox.confirm('确认删除该排产?', '提示', { type: 'warning' })
await request.delete(`/daily-plans/${row.id}`)
load()
}
// 启用(PENDING→CONFIRMED) / 停用(CONFIRMED→PENDING):自动排产生成的是 PENDING,需人工启用后才生效
async function toggleStatus(row) {
const target = row.status === 'CONFIRMED' ? 'PENDING' : 'CONFIRMED'
const label = target === 'CONFIRMED' ? '启用' : '停用'
await ElMessageBox.confirm(`确认${label}该排产(${row.orderNo} ${row.planDate})?`, '提示', { type: 'warning' })
await request.put(`/daily-plans/${row.id}`, {
orderNo: row.orderNo, planDate: row.planDate, planQty: row.planQty, status: target
})
ElMessage.success(`已${label}`)
load()
}
onMounted(async () => {
workOrders.value = (await request.get('/work-orders')) || []
productTypes.value = (await request.get('/product-types')) || []
await loadStations()
load()
})
</script>