feat: 新增多类功能并优化现有流程
1. 工序工位支持0号上线位,更新解析逻辑与注释 2. 物料清单模块重命名为产品物料清单并优化文案 3. WMS与工位端新增用户/角色详情抽屉组件 4. 工单模块新增自动生成工单号、工位分组选择器 5. 日排产支持批量生成与详情查看 6. 工单列表重构操作菜单与表单优化
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<el-drawer v-model="vis" :title="title" :size="size" destroy-on-close append-to-body>
|
||||
<el-descriptions v-if="data" :column="column" border size="small">
|
||||
<el-descriptions-item v-for="f in fields" :key="f.key" :label="f.label">
|
||||
<slot v-if="f.slot" :name="f.slot" :row="data" />
|
||||
<template v-else>{{ fmt(f, data) }}</template>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-empty v-else description="暂无数据" />
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
title: { type: String, default: '详情' },
|
||||
fields: { type: Array, default: () => [] },
|
||||
data: { type: Object, default: null },
|
||||
size: { type: String, default: '480px' },
|
||||
column: { type: Number, default: 1 }
|
||||
})
|
||||
const emit = defineEmits(['update:visible'])
|
||||
const vis = computed({
|
||||
get: () => props.visible,
|
||||
set: (v) => emit('update:visible', v)
|
||||
})
|
||||
|
||||
function fmtTime(v) {
|
||||
if (v === null || v === undefined || v === '') return '-'
|
||||
let n = typeof v === 'string' ? Number(v) : v
|
||||
if (isNaN(n)) return v
|
||||
if (n > 1e12) n = Math.floor(n / 1000)
|
||||
if (n <= 0) return '-'
|
||||
const d = new Date(n * 1000)
|
||||
const p = (x) => String(x).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
|
||||
}
|
||||
|
||||
function fmt(f, data) {
|
||||
if (!data) return '-'
|
||||
const v = data[f.key]
|
||||
if (f.format && typeof f.format === 'function') {
|
||||
const r = f.format(v, data)
|
||||
return r === null || r === undefined || r === '' ? '-' : r
|
||||
}
|
||||
let out = v
|
||||
if (f.type === 'time') out = fmtTime(v)
|
||||
else if (f.type === 'bool') out = v ? '是' : '否'
|
||||
else if (f.type === 'enum' && f.map) out = (f.map[v] ?? v)
|
||||
if (out === null || out === undefined || out === '') return '-'
|
||||
return out
|
||||
}
|
||||
</script>
|
||||
@@ -6,14 +6,29 @@
|
||||
<span class="seq-summary">{{ summary }}</span>
|
||||
</div>
|
||||
<el-checkbox-group v-model="codes" class="seq-group">
|
||||
<el-checkbox v-for="i in stations" :key="i" :label="i" class="seq-item" :disabled="props.disabledCodes.includes(i)">工位{{ i }}<template v-if="props.disabledCodes.includes(i)">(已完成)</template></el-checkbox>
|
||||
<template v-for="g in groups" :key="g.key">
|
||||
<div class="seq-group-title" :class="{ 'seq-empty': g.empty }">
|
||||
{{ g.title }}({{ g.stations.length }} 个工位)
|
||||
</div>
|
||||
<el-checkbox
|
||||
v-for="st in g.stations"
|
||||
:key="st.stationNo"
|
||||
:label="st.stationNo"
|
||||
class="seq-item"
|
||||
:disabled="props.disabledCodes.includes(st.stationNo)">
|
||||
<span class="seq-no">工位{{ st.stationNo }}</span>
|
||||
<span v-if="st.flowName" class="seq-flow">{{ st.flowName }}</span>
|
||||
<span v-else class="seq-flow seq-noflow">未绑工艺</span>
|
||||
</el-checkbox>
|
||||
</template>
|
||||
</el-checkbox-group>
|
||||
<div class="seq-hint">相同「工艺流程」的工位归到同一组——勾选后这些工位将共用同一套工艺步骤。</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { loadStationNos } from '../utils/stations'
|
||||
import { loadStations } from '../utils/stations'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, default: '' },
|
||||
@@ -22,9 +37,10 @@ const props = defineProps({
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
// 工位数以数据库为准:新增工位只需往 station 表插一行,前端自动出现。
|
||||
// 加载完整工位对象(含 flowId/flowName),用于按工艺流程分组展示。
|
||||
const stations = ref([])
|
||||
onMounted(async () => {
|
||||
try { stations.value = await loadStationNos() } catch { stations.value = [] }
|
||||
try { stations.value = await loadStations() } catch { stations.value = [] }
|
||||
})
|
||||
|
||||
function seqToCodes(s) {
|
||||
@@ -32,7 +48,7 @@ function seqToCodes(s) {
|
||||
const out = []
|
||||
String(s).split(',').forEach((p) => {
|
||||
const n = Number(p)
|
||||
if (n >= 1 && !out.includes(n)) out.push(n)
|
||||
if (n >= 0 && !out.includes(n)) out.push(n)
|
||||
})
|
||||
return out.sort((a, b) => a - b)
|
||||
}
|
||||
@@ -42,6 +58,29 @@ const codes = computed({
|
||||
set: (v) => emit('update:modelValue', [...v].sort((a, b) => a - b).join(','))
|
||||
})
|
||||
|
||||
// 按工艺流程分组:相同 flowId 的工位归到一组,直观看出哪些工位共工艺。
|
||||
// flowId=0(未绑定)单独成组排在最后。
|
||||
const groups = computed(() => {
|
||||
const map = new Map()
|
||||
for (const st of stations.value) {
|
||||
const key = st.flowId || 0
|
||||
if (!map.has(key)) map.set(key, { flowId: key, flowName: st.flowName || '', stations: [] })
|
||||
map.get(key).stations.push(st)
|
||||
}
|
||||
const arr = Array.from(map.values())
|
||||
arr.sort((a, b) => {
|
||||
if (a.flowId === 0) return 1
|
||||
if (b.flowId === 0) return -1
|
||||
return (a.flowName || '').localeCompare(b.flowName || '', 'zh')
|
||||
})
|
||||
return arr.map((g) => ({
|
||||
key: g.flowId,
|
||||
title: g.flowId ? `工艺流程:${g.flowName}` : '未绑定工艺流程',
|
||||
empty: g.flowId === 0,
|
||||
stations: [...g.stations].sort((a, b) => (a.stationNo || 0) - (b.stationNo || 0))
|
||||
}))
|
||||
})
|
||||
|
||||
const summary = computed(() => {
|
||||
const c = seqToCodes(props.modelValue)
|
||||
if (!c.length) return '未选择工位'
|
||||
@@ -50,7 +89,7 @@ const summary = computed(() => {
|
||||
})
|
||||
|
||||
function checkAll() {
|
||||
codes.value = [...stations.value].sort((a, b) => a - b)
|
||||
codes.value = [...stations.value].map((s) => s.stationNo).sort((a, b) => a - b)
|
||||
}
|
||||
function clearAll() {
|
||||
codes.value = []
|
||||
@@ -61,6 +100,12 @@ function clearAll() {
|
||||
.seq-picker { width: 100%; }
|
||||
.seq-actions { margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
|
||||
.seq-summary { color: #1668dc; font-size: 12px; }
|
||||
.seq-group { display: flex; flex-wrap: wrap; gap: 4px 8px; }
|
||||
.seq-group { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: flex-start; }
|
||||
.seq-group-title { width: 100%; font-size: 12px; font-weight: 600; color: #303133; background: #f5f7fa; padding: 4px 8px; border-radius: 4px; margin-top: 4px; }
|
||||
.seq-group-title.seq-empty { color: #909399; font-weight: 500; }
|
||||
.seq-item { margin-right: 0; }
|
||||
.seq-no { font-weight: 500; }
|
||||
.seq-flow { margin-left: 6px; font-size: 12px; color: #1668dc; background: #ecf3fe; padding: 0 6px; border-radius: 3px; }
|
||||
.seq-flow.seq-noflow { color: #909399; background: #f4f4f5; }
|
||||
.seq-hint { font-size: 12px; color: #909399; margin-top: 8px; line-height: 1.5; }
|
||||
</style>
|
||||
|
||||
@@ -79,7 +79,7 @@ export const helpDailyPlan = {
|
||||
}
|
||||
|
||||
export const helpBom = {
|
||||
title: '物料清单',
|
||||
title: '产品物料清单',
|
||||
overview:
|
||||
'物料清单 = 生产 1 台该产品需要哪些物料、各用多少。\n按「产品编号 + 物料清单名称」维护:同一产品编号可并存多份物料清单(选用制)——不同厂家供货、含半成品的结构差异,都用不同的物料清单名称区分(缺省叫「默认」,不用 V1/V2 版本号)。\n工单建单时选定其中一份,之后该工单的备料算料、装机绑定校验都锁定这份物料清单,不同厂家的料单互不串。\n\n与 WMS 的关系:本页(MES)只维护「产品由哪几种料、各用几个」;每种物料叫什么、规格、是电气件(SN)还是结构件(批次)、库存有多少,统一在 WMS(库房)的「物料档案」里维护,两套系统互相解耦、通过图号对齐。\n\n编码对应规则:本页的「图号」必须与 WMS 物料档案里的「图号」完全一致;「管理方式」也要对应(电气件=SN 逐件,结构件=批次)。编码对不上,WMS 将无法为对应工单备料/出库。\n\n保护规则:某份物料清单一旦被工单选用过,就不允许再从中移除物料(防止历史工单追溯断链);新建/改名的物料清单名称在本页「物料清单名称」下拉直接输入即可创建。\n\n数据链路:先建「产品型号」→ 到本页选产品编号 + 命名物料清单,逐条添加物料 → 建工单时选定物料清单 → 仓库才能为对应工单领料。',
|
||||
fields: [
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<el-card>
|
||||
<el-form inline>
|
||||
<el-form-item label="产品编号">
|
||||
<el-select v-model="productCode" filterable clearable placeholder="选择产品编号查看其物料清单" style="width:240px" @change="onProductChange">
|
||||
<el-select v-model="productCode" filterable clearable placeholder="选择产品编号查看其产品物料清单" style="width:240px" @change="onProductChange">
|
||||
<el-option v-for="p in products" :key="p.code" :label="`${p.code} ${p.name || ''}`" :value="p.code" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -10,7 +10,7 @@
|
||||
<el-select v-model="bomName" filterable allow-create default-first-option placeholder="同一型号多份物料清单按名称区分" style="width:220px" @change="load">
|
||||
<el-option v-for="b in bomNames" :key="b.bomName" :label="`${b.bomName}(${b.itemCount}料)`" :value="b.bomName" />
|
||||
</el-select>
|
||||
<div style="color:#909399;font-size:12px;line-height:1.4;width:100%">同一型号可并存多份物料清单(不同厂家/含半成品),可输入新名称创建;工单建单时按名选用。</div>
|
||||
<div style="color:#909399;font-size:12px;line-height:1.4;width:100%">同一型号可并存多份产品物料清单(不同厂家/含半成品),可输入新名称创建;工单建单时按名选用。</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
@@ -63,7 +63,7 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="add.show" title="添加物料清单" width="520px">
|
||||
<el-dialog v-model="add.show" title="添加产品物料清单" width="520px">
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:12px"
|
||||
title="图号须与 WMS(库房)「物料档案」的图号完全一致,才能备料/出库。管理方式:电气件按 SN 逐件、结构件按批次。" />
|
||||
<el-form :model="add" label-width="90px">
|
||||
|
||||
@@ -32,8 +32,9 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" min-width="90" />
|
||||
<el-table-column label="操作" width="230">
|
||||
<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' ? '停用' : '启用' }}
|
||||
@@ -44,19 +45,47 @@
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog v-model="dialog.show" title="新建/编辑排产" width="520px">
|
||||
<el-form :model="dialog" label-width="90px">
|
||||
<el-dialog v-model="dialog.show" :title="dialog.id ? '编辑排产' : '新建排产'" width="560px">
|
||||
<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="loadSuggest">
|
||||
<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-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>
|
||||
<!-- 选工单号后自动带出(只读) -->
|
||||
<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>
|
||||
|
||||
<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)">
|
||||
@@ -74,6 +103,10 @@
|
||||
<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>
|
||||
@@ -85,17 +118,34 @@
|
||||
</style>
|
||||
|
||||
<script setup>
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
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 dialog = reactive({ show: false, id: 0, orderNo: '', planDate: '', planQty: 1, dockCodes: [] })
|
||||
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
|
||||
})
|
||||
|
||||
const dockOptions = [...Array.from({ length: 20 }, (_, i) => `DOCK${String(i + 1).padStart(2, '0')}`), 'DOCK21']
|
||||
const statusOptions = [
|
||||
@@ -117,22 +167,77 @@ async function loadSuggest() {
|
||||
dialog.dockCodes = d?.dockCodes || []
|
||||
} catch { dialog.dockCodes = [] }
|
||||
}
|
||||
// 选工单号自动带出其产品信息(只读展示,避免手填错位)
|
||||
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()
|
||||
}
|
||||
function openAdd() {
|
||||
Object.assign(dialog, { show: true, id: 0, orderNo: '', planDate: new Date().toISOString().slice(0, 10), planQty: 1, dockCodes: [] })
|
||||
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: ''
|
||||
})
|
||||
}
|
||||
function openEdit(row) {
|
||||
Object.assign(dialog, { show: true, id: row.id, orderNo: row.orderNo, planDate: row.planDate, planQty: row.planQty, dockCodes: row.dockCodes || [] })
|
||||
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()
|
||||
}
|
||||
async function save() {
|
||||
if (!dialog.orderNo || !dialog.planDate) return ElMessage.warning('请选择工单号和排产日期')
|
||||
if (!dialog.orderNo) return ElMessage.warning('请选择工单号')
|
||||
if (!dialog.dockCodes.length) return ElMessage.warning('请确认送达接驳台(AGV 配送目标)')
|
||||
const payload = { orderNo: dialog.orderNo, planDate: dialog.planDate, planQty: dialog.planQty, dockCodes: dialog.dockCodes }
|
||||
if (dialog.id) await request.put(`/daily-plans/${dialog.id}`, payload)
|
||||
else await request.post('/daily-plans', payload)
|
||||
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
|
||||
})
|
||||
}
|
||||
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: '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
|
||||
}
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm('确认删除该排产?', '提示', { type: 'warning' })
|
||||
await request.delete(`/daily-plans/${row.id}`)
|
||||
@@ -152,6 +257,7 @@ async function toggleStatus(row) {
|
||||
}
|
||||
onMounted(async () => {
|
||||
workOrders.value = (await request.get('/work-orders')) || []
|
||||
productTypes.value = (await request.get('/product-types')) || []
|
||||
load()
|
||||
})
|
||||
</script>
|
||||
@@ -57,12 +57,11 @@
|
||||
<el-table-column label="完成日期" width="110">
|
||||
<template #default="{ row }">{{ fmtDateOnly(row.dueDate) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="420" fixed="right">
|
||||
<el-table-column label="开始时间" width="110">
|
||||
<template #default="{ row }">{{ fmtDateOnly(row.planStart) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="row.status === 'CREATED'">
|
||||
<el-button v-if="can('produce.workorder:edit')" size="small" type="primary" plain @click="openSeq(row)">工位组合</el-button>
|
||||
<el-button v-if="can('produce.workorder:edit')" size="small" type="warning" @click="openEdit(row)">编辑</el-button>
|
||||
</template>
|
||||
<el-button v-if="can('produce.workorder:dailyplan') && !['DONE','CANCELLED'].includes(row.status)"
|
||||
size="small" type="success" plain @click="openAutoSchedule(row)">自动排产</el-button>
|
||||
<el-dropdown
|
||||
@@ -75,7 +74,18 @@
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-button v-if="can('produce.workorder:delete') && ['CREATED','PENDING','CONFIRMED'].includes(row.status)" size="small" type="danger" @click="del(row)">删除</el-button>
|
||||
<el-dropdown
|
||||
v-if="(row.status === 'CREATED' && can('produce.workorder:edit')) || (can('produce.workorder:delete') && ['CREATED','PENDING','CONFIRMED'].includes(row.status))"
|
||||
trigger="click" @command="(cmd) => moreAction(row, cmd)">
|
||||
<el-button size="small">更多 ▾</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item v-if="row.status === 'CREATED'" command="seq">工位组合</el-dropdown-item>
|
||||
<el-dropdown-item v-if="can('produce.workorder:edit') && row.status === 'CREATED'" command="edit">编辑</el-dropdown-item>
|
||||
<el-dropdown-item v-if="can('produce.workorder:delete') && ['CREATED','PENDING','CONFIRMED'].includes(row.status)" command="del" divided>删除</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -84,23 +94,31 @@
|
||||
<!-- 新建/编辑工单 -->
|
||||
<el-dialog v-model="dialog.show" :title="dialog.id ? '编辑工单' : '新建工单'" width="560px">
|
||||
<el-form :model="dialog" label-width="90px">
|
||||
<el-form-item label="工单号"><el-input v-model="dialog.workOrderNo" /></el-form-item>
|
||||
<el-form-item label="产品型号">
|
||||
<el-select v-model="dialog.productTypeId" placeholder="请选择产品型号" style="width:100%">
|
||||
<el-form-item label="工单号" required><el-input v-model="dialog.workOrderNo" /></el-form-item>
|
||||
<el-form-item label="产品型号" required>
|
||||
<el-select v-model="dialog.productTypeId" placeholder="---请选择---" style="width:100%" @change="onProductTypeChange">
|
||||
<el-option v-for="p in productTypes" :key="p.id" :label="p.name + (p.code ? ' / ' + p.code : '')" :value="p.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品编号"><el-input v-model="dialog.productCode" placeholder="请输入产品编号" /></el-form-item>
|
||||
<el-form-item label="产品名称"><el-input v-model="dialog.productName" placeholder="请输入产品名称" /></el-form-item>
|
||||
<el-form-item label="产品编号">
|
||||
<el-input v-model="dialog.productCode" placeholder="随产品型号自动带出" :disabled="dialog.productTypeId > 0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="产品名称">
|
||||
<el-input v-model="dialog.productName" placeholder="随产品型号自动带出" :disabled="dialog.productTypeId > 0" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同号"><el-input v-model="dialog.contractNo" placeholder="一个合同对应一个工程编号" /></el-form-item>
|
||||
<el-form-item label="工程编号"><el-input v-model="dialog.projectNo" placeholder="工程编号下挂各产品序号" /></el-form-item>
|
||||
<el-form-item label="产品序号"><el-input v-model="dialog.productSerial" placeholder="本产品在该工程下的序号" /></el-form-item>
|
||||
<el-form-item label="数量"><el-input-number v-model="dialog.quantity" :min="1" /></el-form-item>
|
||||
<el-form-item label="数量" required><el-input-number v-model="dialog.quantity" :min="1" /></el-form-item>
|
||||
<el-form-item label="完成日期" required>
|
||||
<el-date-picker v-model="dialog.dueDate" type="date" value-format="YYYY-MM-DD"
|
||||
placeholder="请选择完成日期(必填)" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工位组合">
|
||||
<el-form-item label="开始时间">
|
||||
<el-date-picker v-model="dialog.planStart" type="date" value-format="YYYY-MM-DD"
|
||||
placeholder="计划开始日期(选填)" style="width:100%" />
|
||||
</el-form-item>
|
||||
<el-form-item label="工位组合" required>
|
||||
<ProcessSeqPicker :model-value="dialog.processSeq" @update:model-value="dialog.processSeq = $event" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -133,6 +151,8 @@
|
||||
<el-dialog v-model="sched.show" :title="`自动排产 - ${sched.orderNo}`" width="640px">
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:14px"
|
||||
title="自动排产仅排「未开工数量 = 工单总数 − 已完工 − 在产未完工」,生成后状态为未启用(PENDING),需到「日排产」页面人工启用后生效。" />
|
||||
<el-alert type="info" :closable="false" style="margin-bottom:14px"
|
||||
:title="`项目开始:${fmtDateOnly(sched.planStart)} 结束:${fmtDateOnly(sched.dueDate)} 工单数量:${sched.quantity} 已完工:${sched.finishedNum}`" />
|
||||
<el-form :model="sched" label-width="100px">
|
||||
<el-form-item label="排产模式">
|
||||
<el-radio-group v-model="sched.mode">
|
||||
@@ -159,6 +179,9 @@
|
||||
<div>共 {{ previewDates.length }} 天:{{ previewDates.join('、') || '-' }}</div>
|
||||
<div v-if="sched.mode === 'AVG' && previewDates.length">平均分摊到 {{ previewDates.length }} 天(余数补前几天)</div>
|
||||
<div v-if="sched.mode === 'FIXED' && previewDates.length">每日 {{ sched.dailyQty }} 件 × {{ previewDates.length }} 天(超未开工数量将被拒)</div>
|
||||
<div v-if="sched.mode === 'FIXED' && previewDates.length" :class="{ 'sched-warn': previewTotal > sched.quantity }">
|
||||
本次计划总量 {{ previewTotal }},工单数量 {{ sched.quantity }}<span v-if="previewTotal > sched.quantity">(超出工单数量,保存将被拒绝)</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -214,7 +237,7 @@ function seqToCodes(s) {
|
||||
const out = []
|
||||
String(s).split(',').forEach((p) => {
|
||||
const n = Number(p)
|
||||
if (n >= 1 && !out.includes(n)) out.push(n)
|
||||
if (n >= 0 && !out.includes(n)) out.push(n)
|
||||
})
|
||||
return out.sort((a, b) => a - b)
|
||||
}
|
||||
@@ -232,7 +255,7 @@ const query = reactive({ orderNo: '', productCode: '', productName: '', contract
|
||||
const list = ref([])
|
||||
const productTypes = ref([])
|
||||
|
||||
const dialog = reactive({ show: false, id: 0, workOrderNo: '', productTypeId: 0, productCode: '', productName: '', contractNo: '', projectNo: '', productSerial: '', quantity: 1, dueDate: '', processSeq: '', status: 'CREATED' })
|
||||
const dialog = reactive({ show: false, id: 0, workOrderNo: '', productTypeId: 0, productCode: '', productName: '', contractNo: '', projectNo: '', productSerial: '', quantity: 1, dueDate: '', planStart: '', processSeq: '', status: 'CREATED' })
|
||||
const seq = reactive({ show: false, id: 0, orderNo: '', productCode: '', productName: '', processSeq: '' })
|
||||
const stationNos = ref([])
|
||||
|
||||
@@ -249,15 +272,32 @@ async function load() {
|
||||
async function loadProducts() {
|
||||
productTypes.value = (await request.get('/product-types')) || []
|
||||
}
|
||||
function genOrderNo() {
|
||||
const d = new Date()
|
||||
const ymd = `${d.getFullYear()}${String(d.getMonth() + 1).padStart(2, '0')}${String(d.getDate()).padStart(2, '0')}`
|
||||
return `WO-${ymd}-001`
|
||||
}
|
||||
// Q1=A:选产品型号后自动带出产品编号/名称(只读,避免手填错位)
|
||||
function onProductTypeChange(id) {
|
||||
const p = productTypes.value.find((x) => x.id === id)
|
||||
if (p) {
|
||||
dialog.productCode = p.code || ''
|
||||
dialog.productName = p.name || ''
|
||||
} else {
|
||||
dialog.productCode = ''
|
||||
dialog.productName = ''
|
||||
}
|
||||
}
|
||||
function openCreate() {
|
||||
Object.assign(dialog, { show: true, id: 0, workOrderNo: `WO${Date.now()}`, productTypeId: 0, productCode: '', productName: '', contractNo: '', projectNo: '', productSerial: '', quantity: 1, dueDate: '', processSeq: '', status: 'CREATED' })
|
||||
Object.assign(dialog, { show: true, id: 0, workOrderNo: genOrderNo(), productTypeId: 0, productCode: '', productName: '', contractNo: '', projectNo: '', productSerial: '', quantity: 1, dueDate: '', planStart: '', processSeq: '', status: 'CREATED' })
|
||||
}
|
||||
function openEdit(row) {
|
||||
Object.assign(dialog, { show: true, id: row.id, workOrderNo: row.workOrderNo, productTypeId: row.productTypeId, productCode: row.productCode, productName: row.productName, contractNo: row.contractNo || '', projectNo: row.projectNo || '', productSerial: row.productSerial || '', quantity: row.quantity, dueDate: row.dueDate ? String(row.dueDate).slice(0, 10) : '', processSeq: row.processSeq || '', status: row.status })
|
||||
Object.assign(dialog, { show: true, id: row.id, workOrderNo: row.workOrderNo, productTypeId: row.productTypeId, productCode: row.productCode, productName: row.productName, contractNo: row.contractNo || '', projectNo: row.projectNo || '', productSerial: row.productSerial || '', quantity: row.quantity, dueDate: row.dueDate ? String(row.dueDate).slice(0, 10) : '', planStart: row.planStart ? String(row.planStart).slice(0, 10) : '', processSeq: row.processSeq || '', status: row.status })
|
||||
}
|
||||
async function save() {
|
||||
if (!dialog.dueDate) return ElMessage.warning('请选择完成日期(必填)')
|
||||
const body = { id: dialog.id, workOrderNo: dialog.workOrderNo, productTypeId: dialog.productTypeId, productCode: dialog.productCode, productName: dialog.productName, contractNo: dialog.contractNo, projectNo: dialog.projectNo, productSerial: dialog.productSerial, quantity: dialog.quantity, dueDate: dialog.dueDate, processSeq: dialog.processSeq }
|
||||
if (!dialog.processSeq || !dialog.processSeq.trim()) return ElMessage.warning('请至少选择一个工位(工位组合为必填项)')
|
||||
const body = { id: dialog.id, workOrderNo: dialog.workOrderNo, productTypeId: dialog.productTypeId, productCode: dialog.productCode, productName: dialog.productName, contractNo: dialog.contractNo, projectNo: dialog.projectNo, productSerial: dialog.productSerial, quantity: dialog.quantity, dueDate: dialog.dueDate, planStart: dialog.planStart, processSeq: dialog.processSeq }
|
||||
if (dialog.id) await request.put('/work-orders', body)
|
||||
else await request.post('/work-orders', body)
|
||||
ElMessage.success('保存成功')
|
||||
@@ -315,10 +355,20 @@ async function del(row) {
|
||||
load()
|
||||
}
|
||||
|
||||
// 操作列「更多」菜单分发:工位组合 / 编辑 / 删除
|
||||
function moreAction(row, cmd) {
|
||||
if (cmd === 'seq') openSeq(row)
|
||||
else if (cmd === 'edit') openEdit(row)
|
||||
else if (cmd === 'del') del(row)
|
||||
}
|
||||
|
||||
// ---- 自动排产(引导式,Item K)----
|
||||
const sched = reactive({ show: false, orderNo: '', mode: 'AVG', dailyQty: 1, segments: [{ range: null }] })
|
||||
const sched = reactive({ show: false, orderNo: '', mode: 'AVG', dailyQty: 1, segments: [{ range: null }], planStart: '', dueDate: '', quantity: 0, finishedNum: 0 })
|
||||
function openAutoSchedule(row) {
|
||||
Object.assign(sched, { show: true, orderNo: row.workOrderNo, mode: 'AVG', dailyQty: 1, segments: [{ range: null }] })
|
||||
Object.assign(sched, {
|
||||
show: true, orderNo: row.workOrderNo, mode: 'AVG', dailyQty: 1, segments: [{ range: null }],
|
||||
planStart: row.planStart || '', dueDate: row.dueDate || '', quantity: row.quantity || 0, finishedNum: row.finishedNum || 0
|
||||
})
|
||||
}
|
||||
function addSegment() { sched.segments.push({ range: null }) }
|
||||
function removeSegment(i) { sched.segments.splice(i, 1) }
|
||||
@@ -340,6 +390,12 @@ const previewDates = computed(() => {
|
||||
}
|
||||
return out.sort()
|
||||
})
|
||||
// 本次排产计划总量(仅用于前端提示);AVG 模式后端按未开工数量自动均摊,此处不预估精确值
|
||||
const previewTotal = computed(() => {
|
||||
if (!previewDates.length) return 0
|
||||
if (sched.mode === 'FIXED') return sched.dailyQty * previewDates.length
|
||||
return 0
|
||||
})
|
||||
async function submitAutoSchedule() {
|
||||
const segments = sched.segments
|
||||
.filter((s) => s.range && s.range.length === 2 && s.range[0] && s.range[1])
|
||||
|
||||
Reference in New Issue
Block a user