2026-08-29 15:30:25 +08:00
|
|
|
|
<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>
|
2026-08-31 08:06:55 +08:00
|
|
|
|
<el-button v-if="can('produce.workorder:dailyplan')" type="success" @click="openAdd">新建排产</el-button>
|
2026-08-29 15:30:25 +08:00
|
|
|
|
</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" />
|
2026-09-04 14:18:53 +08:00
|
|
|
|
<el-table-column label="送达接驳台" min-width="150">
|
|
|
|
|
|
<template #default="{ row }">
|
|
|
|
|
|
<el-tag v-for="d in (row.dockCodes||[])" :key="d" size="small" class="dock-tag">{{ d }}</el-tag>
|
|
|
|
|
|
<span v-if="!(row.dockCodes||[]).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>
|
2026-08-29 15:30:25 +08:00
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="operator" label="操作人" min-width="90" />
|
2026-09-18 13:54:30 +08:00
|
|
|
|
<el-table-column label="操作" width="330">
|
2026-08-29 15:30:25 +08:00
|
|
|
|
<template #default="{ row }">
|
2026-09-18 13:54:30 +08:00
|
|
|
|
<el-button size="small" type="primary" link @click="openDetail(row)">详情</el-button>
|
2026-09-17 12:49:07 +08:00
|
|
|
|
<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>
|
2026-08-31 08:06:55 +08:00
|
|
|
|
<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>
|
2026-08-29 15:30:25 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
2026-09-18 13:54:30 +08:00
|
|
|
|
<el-dialog v-model="dialog.show" :title="dialog.id ? '编辑排产' : '新建排产'" width="560px">
|
|
|
|
|
|
<el-form :model="dialog" label-width="92px">
|
2026-08-29 15:30:25 +08:00
|
|
|
|
<el-form-item label="工单号">
|
2026-09-18 13:54:30 +08:00
|
|
|
|
<el-select v-model="dialog.orderNo" filterable allow-create placeholder="请选择/输入工单号" style="width:100%" @change="onOrderChange">
|
2026-08-29 15:30:25 +08:00
|
|
|
|
<el-option v-for="w in workOrders" :key="w.workOrderNo" :label="w.workOrderNo" :value="w.workOrderNo" />
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-form-item>
|
2026-09-18 13:54:30 +08:00
|
|
|
|
<!-- 选工单号后自动带出(只读) -->
|
|
|
|
|
|
<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('、') || '-'}`" />
|
|
|
|
|
|
</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%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item label="计划数量">
|
|
|
|
|
|
<el-input-number v-model="dialog.planQty" :min="1" style="width:100%" />
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-09-04 14:18:53 +08:00
|
|
|
|
<el-form-item label="送达接驳台" prop="dockCodes">
|
|
|
|
|
|
<el-select v-model="dialog.dockCodes" multiple filterable clearable placeholder="该工单物料送达哪些接驳台(可多选)" style="width:100%">
|
|
|
|
|
|
<el-option-group label="产线接驳台(DOCK n↔工位n)">
|
|
|
|
|
|
<el-option v-for="n in 20" :key="n" :label="`DOCK${String(n).padStart(2,'0')} · 工位${n}`" :value="`DOCK${String(n).padStart(2,'0')}`" />
|
|
|
|
|
|
</el-option-group>
|
|
|
|
|
|
<el-option-group label="库房">
|
|
|
|
|
|
<el-option label="DOCK21 · 库房收货" value="DOCK21" />
|
|
|
|
|
|
</el-option-group>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<div class="dock-tip">选需送达的接驳台;建议为当前工单上次使用的接驳台</div>
|
|
|
|
|
|
</el-form-item>
|
2026-08-29 15:30:25 +08:00
|
|
|
|
</el-form>
|
|
|
|
|
|
<template #footer>
|
|
|
|
|
|
<el-button @click="dialog.show=false">取消</el-button>
|
2026-08-31 08:06:55 +08:00
|
|
|
|
<el-button v-if="can('produce.workorder:dailyplan')" type="primary" @click="save">保存</el-button>
|
2026-08-29 15:30:25 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
</el-dialog>
|
2026-09-18 13:54:30 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 排产详情(只读,列表点「详情」打开,不可编辑) -->
|
|
|
|
|
|
<DetailDrawer :visible="detail.show" @update:visible="detail.show = $event"
|
|
|
|
|
|
:title="detail.title" :fields="detail.fields" :data="detail.row" />
|
2026-08-29 15:30:25 +08:00
|
|
|
|
</el-card>
|
2026-09-07 11:58:19 +08:00
|
|
|
|
<PageHelp :data="helpDailyPlan" page="DailyPlan" />
|
2026-08-29 15:30:25 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-09-04 14:18:53 +08:00
|
|
|
|
<style scoped>
|
|
|
|
|
|
.dock-tag { margin-right: 4px; }
|
|
|
|
|
|
.dock-empty { color: #999; font-size: 12px; }
|
|
|
|
|
|
.dock-tip { width: 100%; font-size: 12px; color: #888780; line-height: 1.5; }
|
|
|
|
|
|
</style>
|
|
|
|
|
|
|
2026-08-29 15:30:25 +08:00
|
|
|
|
<script setup>
|
2026-09-18 13:54:30 +08:00
|
|
|
|
import { reactive, ref, computed, onMounted } from 'vue'
|
2026-08-29 15:30:25 +08:00
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
|
|
|
import request from '../utils/request'
|
2026-08-31 08:06:55 +08:00
|
|
|
|
import { can } from '../utils/perm'
|
2026-08-29 15:30:25 +08:00
|
|
|
|
import PageHelp from '../components/PageHelp.vue'
|
2026-09-18 13:54:30 +08:00
|
|
|
|
import DetailDrawer from '../components/DetailDrawer.vue'
|
2026-08-29 15:30:25 +08:00
|
|
|
|
import { helpDailyPlan } from '../help'
|
|
|
|
|
|
|
|
|
|
|
|
const query = reactive({ orderNo: '', planDate: '' })
|
|
|
|
|
|
const list = ref([])
|
|
|
|
|
|
const workOrders = ref([])
|
2026-09-18 13:54:30 +08:00
|
|
|
|
const productTypes = ref([])
|
|
|
|
|
|
const dialog = reactive({ show: false, id: 0, orderNo: '', planDate: '', planRange: [], mode: 'AVG', planQty: 1, dockCodes: [], productType: '', productCode: '', productName: '', woQty: '' })
|
|
|
|
|
|
|
|
|
|
|
|
// 新建排产「日期范围 + 模式」预览:展开为去重升序日期列表(仅前端提示,后端以 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
|
|
|
|
|
|
})
|
2026-08-29 15:30:25 +08:00
|
|
|
|
|
2026-09-04 14:18:53 +08:00
|
|
|
|
const dockOptions = [...Array.from({ length: 20 }, (_, i) => `DOCK${String(i + 1).padStart(2, '0')}`), 'DOCK21']
|
2026-08-29 15:30:25 +08:00
|
|
|
|
const statusOptions = [
|
2026-09-17 12:49:07 +08:00
|
|
|
|
{ v: 'PENDING', l: '未启用' }, { v: 'CONFIRMED', l: '已启用' },
|
|
|
|
|
|
{ v: 'PROCESSING', l: '执行中' }, { v: 'DONE', l: '已完成' }, { v: 'CANCELLED', l: '已取消' }
|
2026-08-29 15:30:25 +08:00
|
|
|
|
]
|
|
|
|
|
|
const statusText = (s) => (statusOptions.find((x) => x.v === s) || {}).l || s
|
2026-09-04 14:18:53 +08:00
|
|
|
|
const planStatusTag = (s) =>
|
2026-09-17 12:49:07 +08:00
|
|
|
|
s === 'DONE' ? 'success' : s === 'PROCESSING' ? 'warning' : s === 'CONFIRMED' ? 'primary' : 'info'
|
2026-08-29 15:30:25 +08:00
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
list.value = (await request.get('/daily-plans', { params: query })) || []
|
|
|
|
|
|
}
|
2026-09-04 14:18:53 +08:00
|
|
|
|
// 推荐:该工单上次使用的接驳台
|
|
|
|
|
|
async function loadSuggest() {
|
|
|
|
|
|
if (!dialog.orderNo) return
|
|
|
|
|
|
try {
|
|
|
|
|
|
const d = await request.get('/daily-plans/suggest-docks', { params: { orderNo: dialog.orderNo } })
|
|
|
|
|
|
dialog.dockCodes = d?.dockCodes || []
|
|
|
|
|
|
} catch { dialog.dockCodes = [] }
|
|
|
|
|
|
}
|
2026-09-18 13:54:30 +08:00
|
|
|
|
// 选工单号自动带出其产品信息(只读展示,避免手填错位)
|
|
|
|
|
|
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() {
|
|
|
|
|
|
loadSuggest()
|
|
|
|
|
|
loadWorkOrderInfo()
|
|
|
|
|
|
}
|
2026-08-29 15:30:25 +08:00
|
|
|
|
function openAdd() {
|
2026-09-18 13:54:30 +08:00
|
|
|
|
const today = new Date().toISOString().slice(0, 10)
|
|
|
|
|
|
Object.assign(dialog, {
|
|
|
|
|
|
show: true, id: 0, orderNo: '', planDate: '', planRange: [today, today], mode: 'AVG',
|
|
|
|
|
|
planQty: 1, dockCodes: [], productType: '', productCode: '', productName: '', woQty: ''
|
|
|
|
|
|
})
|
2026-08-29 15:30:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
function openEdit(row) {
|
2026-09-18 13:54:30 +08:00
|
|
|
|
Object.assign(dialog, {
|
|
|
|
|
|
show: true, id: row.id, orderNo: row.orderNo, planDate: row.planDate, planRange: [],
|
|
|
|
|
|
mode: 'AVG', planQty: row.planQty, dockCodes: row.dockCodes || [],
|
|
|
|
|
|
productType: '', productCode: '', productName: '', woQty: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
loadWorkOrderInfo()
|
2026-08-29 15:30:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
async function save() {
|
2026-09-18 13:54:30 +08:00
|
|
|
|
if (!dialog.orderNo) return ElMessage.warning('请选择工单号')
|
2026-09-04 14:18:53 +08:00
|
|
|
|
if (!dialog.dockCodes.length) return ElMessage.warning('请确认送达接驳台(AGV 配送目标)')
|
2026-09-18 13:54:30 +08:00
|
|
|
|
if (dialog.id) {
|
|
|
|
|
|
// 编辑单条日排产
|
|
|
|
|
|
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, dockCodes: dialog.dockCodes
|
|
|
|
|
|
})
|
|
|
|
|
|
} 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, dockCodes: dialog.dockCodes
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
2026-08-29 15:30:25 +08:00
|
|
|
|
ElMessage.success('保存成功')
|
|
|
|
|
|
dialog.show = false
|
|
|
|
|
|
load()
|
|
|
|
|
|
}
|
2026-09-18 13:54:30 +08:00
|
|
|
|
|
|
|
|
|
|
// 排产详情(只读,列表点「详情」打开)
|
|
|
|
|
|
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: 'dockCodes', label: '送达接驳台', format: (v) => (v && v.length ? v.join('、') : '未指定') },
|
|
|
|
|
|
{ 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
|
|
|
|
|
|
}
|
2026-08-29 15:30:25 +08:00
|
|
|
|
async function del(row) {
|
|
|
|
|
|
await ElMessageBox.confirm('确认删除该排产?', '提示', { type: 'warning' })
|
|
|
|
|
|
await request.delete(`/daily-plans/${row.id}`)
|
|
|
|
|
|
load()
|
|
|
|
|
|
}
|
2026-09-17 12:49:07 +08:00
|
|
|
|
// 启用(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,
|
|
|
|
|
|
dockCodes: row.dockCodes || [], status: target
|
|
|
|
|
|
})
|
|
|
|
|
|
ElMessage.success(`已${label}`)
|
|
|
|
|
|
load()
|
|
|
|
|
|
}
|
2026-08-29 15:30:25 +08:00
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
workOrders.value = (await request.get('/work-orders')) || []
|
2026-09-18 13:54:30 +08:00
|
|
|
|
productTypes.value = (await request.get('/product-types')) || []
|
2026-08-29 15:30:25 +08:00
|
|
|
|
load()
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|