feat: 完成多模块迭代更新

- 新增过程巡检按步骤上报、数量不符上报、工位退料功能
- 增加工单工程编号三级归属字段
- 新增物料安全库存与缺货预警
- 新增附件管理、退库单管理模块
- 优化生产看板展示逻辑与前端页面文案
- 清理冗余的工艺路线相关代码与备份文件
This commit is contained in:
SunYF
2026-09-15 16:37:39 +08:00
parent 951f3dfecd
commit 4a5e2d7bac
494 changed files with 24702 additions and 139223 deletions
+17 -1
View File
@@ -63,4 +63,20 @@ export const ackLineMove = (data) => request.post('/api/line/move/ack', data)
export const occupyLinePoint = (data) => request.post('/api/line/occupy', data)
// 工位叫料:{stationNo, dock, orderNo, items:[{materialCode, materialName, unit, manageMode, qty}]}
export const callLineMaterial = (data) => request.post('/api/line/call-material', data)
export const callLineMaterial = (data) => request.post('/api/line/call-material', data)
// ---------- 工位终端巡检 / 应急 / 数量不符 / 退料(代理 MES 内部 API ----------
// 巡检提交:{category(CHECKIN/POINT/PROCESS/DONE/ALARM), stationNo, orderNo, sn, shift, result, items, photo, remark, processCode, stepId, stepName, measuredValue}
export const submitInspection = (data) => request.post('/api/inspection/submit', data)
// 巡检拍照上传(base64):{filename, data}
export const uploadInspectionPhoto = (data) => request.post('/api/inspection/upload', data)
// 应急呼叫:{stationNo, orderNo, remark}
export const emergencyCall = (data) => request.post('/api/emergency/call', data)
// 数量不符上报:{stationNo, orderNo, sn, materialCode, materialName, planQty, actualQty, cause}
export const qtyReport = (data) => request.post('/api/qty-report', data)
// 工位退料:{orderNo, stationNo, sn, materialCode, materialName, spec, qty, unit, reason}
export const returnMaterial = (data) => request.post('/api/return-material', data)
@@ -58,15 +58,6 @@ export const orderNos = computed(() => (Array.isArray(state.task?.orderNos) ? st
export const flowName = computed(() => state.task?.flow?.name || '-')
export const flowActive = computed(() => !!state.task?.flowActive)
// 本工位所属路线段(工单路线快照,段=一工艺流程图 + 一组并行工位)
export const currentSegment = computed(() => state.task?.segment || null)
export const segmentText = computed(() => {
const s = currentSegment.value
if (!s || !s.seq) return '未排路线段'
const st = Array.isArray(s.stations) ? s.stations.join(' / ') : '-'
return `${s.seq} 段 · 并行工位 ${st}`
})
// 本工位的产线点位台账(唯一事实源:本工位当前是否有工件)
export const myPoint = computed(() =>
state.points.find((p) => p.pointType === 'STATION' && String(p.pointNo) === String(state.stationNo))
@@ -15,10 +15,6 @@
<span class="lab">工件</span>
<b class="sn">{{ sn || '未上料' }}</b>
</div>
<div class="chip">
<span class="lab">路线段</span>
<b>{{ segmentText }}</b>
</div>
<div class="spacer"></div>
@@ -49,6 +45,9 @@
<el-tab-pane label="工艺文件" name="pdf">
<PdfPanel />
</el-tab-pane>
<el-tab-pane label="巡检 / 异常" name="inspect">
<InspectPanel />
</el-tab-pane>
</el-tabs>
<WorkloadDialog ref="wlRef" />
@@ -63,6 +62,7 @@ import WorkPanel from './panels/WorkPanel.vue'
import MaterialPanel from './panels/MaterialPanel.vue'
import MovePanel from './panels/MovePanel.vue'
import PdfPanel from './panels/PdfPanel.vue'
import InspectPanel from './panels/InspectPanel.vue'
import WorkloadDialog from './panels/WorkloadDialog.vue'
import { stationLogout, syncFlush } from '../api'
import { clearAuth } from '../utils/request'
@@ -77,7 +77,6 @@ import {
orderNo,
pendingTotal,
refreshAll,
segmentText,
sn,
state,
stationNo,
@@ -0,0 +1,290 @@
<template>
<div class="inspect">
<el-alert type="info" :closable="false" show-icon title="巡检与异常" description="签到 / 点检 / 过程巡检 / 完工确认 四类记录均上报 MES 巡检中心;应急呼叫与数量不符实时通知库管与班长。" />
<el-collapse v-model="active" class="blocks">
<!-- 1. 签到 -->
<el-collapse-item name="checkin" title="① 签到(班前)">
<el-form label-width="84px" size="large">
<el-form-item label="班次">
<el-radio-group v-model="checkin.shift">
<el-radio value="早">早班</el-radio>
<el-radio value="中">中班</el-radio>
<el-radio value="晚">晚班</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="现场拍照">
<PhotoField v-model="checkin.photo" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="checkin.remark" type="textarea" :rows="2" placeholder="到岗情况 / 设备初检" />
</el-form-item>
<el-button type="primary" size="large" @click="submitCheckin">提交签到</el-button>
</el-form>
</el-collapse-item>
<!-- 2. 工位点检 -->
<el-collapse-item name="point" title="② 工位点检(班中)">
<el-form label-width="84px" size="large">
<el-form-item label="点检项">
<el-checkbox-group v-model="point.items">
<el-checkbox v-for="it in pointOptions" :key="it" :value="it">{{ it }}</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="现场拍照">
<PhotoField v-model="point.photo" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="point.remark" type="textarea" :rows="2" placeholder="异常项说明" />
</el-form-item>
<el-button type="primary" size="large" @click="submitPoint">提交点检</el-button>
</el-form>
</el-collapse-item>
<!-- 3. 过程巡检按步骤 -->
<el-collapse-item name="process" title="③ 过程巡检(按工艺步骤)">
<template v-if="steps.length">
<el-table :data="steps" size="large" border>
<el-table-column prop="name" label="步骤" min-width="120" />
<el-table-column label="实测值" min-width="140">
<template #default="{ row }">
<el-input v-model="row.measured" placeholder="实测值" />
</template>
</el-table-column>
<el-table-column label="结果" min-width="120">
<template #default="{ row }">
<el-radio-group v-model="row.result">
<el-radio value="OK">合格</el-radio>
<el-radio value="NG">不合格</el-radio>
</el-radio-group>
</template>
</el-table-column>
<el-table-column label="操作" min-width="90">
<template #default="{ row }">
<el-button size="small" type="primary" @click="submitProcessStep(row)">上报</el-button>
</template>
</el-table-column>
</el-table>
</template>
<el-empty v-else description="当前无进行中工单或工艺步骤" />
</el-collapse-item>
<!-- 4. 完工确认 -->
<el-collapse-item name="done" title="④ 完工确认(扫 SN">
<el-form label-width="84px" size="large">
<el-form-item label="工件 SN">
<el-input v-model="done.sn" placeholder="扫描或输入工件 SN" />
</el-form-item>
<el-form-item label="现场拍照">
<PhotoField v-model="done.photo" />
</el-form-item>
<el-form-item label="备注">
<el-input v-model="done.remark" type="textarea" :rows="2" />
</el-form-item>
<el-alert v-if="!canReport" type="warning" :closable="false" title="本工位步骤未全部完成 / 物料未齐套,暂不能完工确认" />
<el-button type="success" size="large" :disabled="!canReport" @click="submitDone">提交完工确认</el-button>
</el-form>
</el-collapse-item>
<!-- 5. 应急呼叫 -->
<el-collapse-item name="emergency" title="⑤ 应急呼叫">
<el-form label-width="84px" size="large">
<el-form-item label="说明">
<el-input v-model="emergency.remark" type="textarea" :rows="2" placeholder="设备故障 / 缺料 / 安全等" />
</el-form-item>
<el-button type="danger" size="large" @click="submitEmergency">发送应急呼叫</el-button>
</el-form>
</el-collapse-item>
<!-- 6. 数量不符上报 -->
<el-collapse-item name="qty" title="⑥ 数量不符上报">
<el-form label-width="92px" size="large">
<el-form-item label="物料编码">
<el-input v-model="qty.materialCode" />
</el-form-item>
<el-form-item label="物料名称">
<el-input v-model="qty.materialName" />
</el-form-item>
<el-form-item label="应发数量">
<el-input-number v-model="qty.planQty" :min="0" />
</el-form-item>
<el-form-item label="实收数量">
<el-input-number v-model="qty.actualQty" :min="0" />
</el-form-item>
<el-form-item label="差异">
<el-tag :type="diffQty === 0 ? 'info' : 'danger'">{{ diffQty }}</el-tag>
</el-form-item>
<el-form-item label="原因">
<el-input v-model="qty.cause" type="textarea" :rows="2" />
</el-form-item>
<el-button type="warning" size="large" :disabled="!qty.materialCode || diffQty === 0" @click="submitQty">上报数量不符</el-button>
</el-form>
</el-collapse-item>
<!-- 7. 工位退料 -->
<el-collapse-item name="return" title="⑦ 工位退料(回库房)">
<el-form label-width="92px" size="large">
<el-form-item label="物料编码">
<el-input v-model="ret.materialCode" />
</el-form-item>
<el-form-item label="物料名称">
<el-input v-model="ret.materialName" />
</el-form-item>
<el-form-item label="规格">
<el-input v-model="ret.spec" />
</el-form-item>
<el-form-item label="退库数量">
<el-input-number v-model="ret.qty" :min="1" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="ret.unit" />
</el-form-item>
<el-form-item label="工件 SN">
<el-input v-model="ret.sn" placeholder="可空" />
</el-form-item>
<el-form-item label="原因">
<el-input v-model="ret.reason" type="textarea" :rows="2" />
</el-form-item>
<el-button type="primary" size="large" :disabled="!ret.materialCode || ret.qty <= 0" @click="submitReturn">提交退料待库管收货</el-button>
</el-form>
</el-collapse-item>
</el-collapse>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import PhotoField from './PhotoField.vue'
import { emergencyCall, qtyReport, returnMaterial, submitInspection } from '../../api'
import { orderNo, sn as snState, stationNo, taskSteps, canReport } from '../../store/station'
const active = ref(['checkin', 'point', 'process', 'done'])
const checkin = ref({ shift: '早', photo: '', remark: '' })
const point = ref({ items: [], photo: '', remark: '' })
const done = ref({ sn: '', photo: '', remark: '' })
const emergency = ref({ remark: '' })
const qty = ref({ materialCode: '', materialName: '', planQty: 0, actualQty: 0, cause: '' })
const ret = ref({ materialCode: '', materialName: '', spec: '', qty: 1, unit: '', sn: '', reason: '' })
const pointOptions = ['设备外观', '气源/电源', '工装夹具', '量具校准', '安全防护']
const steps = computed(() =>
(Array.isArray(taskSteps.value) ? taskSteps.value : []).map((s) => ({
id: s.id,
name: s.name,
measured: '',
result: 'OK'
}))
)
const diffQty = computed(() => qty.value.actualQty - qty.value.planQty)
function base(category, extra = {}) {
return {
category,
stationNo: stationNo.value,
orderNo: orderNo.value,
sn: snState.value,
...extra
}
}
async function submitCheckin() {
try {
await submitInspection(base('CHECKIN', { shift: checkin.value.shift, photo: checkin.value.photo, remark: checkin.value.remark }))
ElMessage.success('签到已提交')
checkin.value = { shift: '早', photo: '', remark: '' }
} catch (e) { /* 提示由拦截器统一 */ }
}
async function submitPoint() {
try {
await submitInspection(base('POINT', { items: point.value.items, photo: point.value.photo, remark: point.value.remark }))
ElMessage.success('点检已提交')
point.value = { items: [], photo: '', remark: '' }
} catch (e) {}
}
async function submitProcessStep(row) {
try {
await submitInspection(base('PROCESS', {
processCode: stationNo.value,
stepId: row.id,
stepName: row.name,
measuredValue: String(row.measured ?? ''),
result: row.result
}))
ElMessage.success(`步骤「${row.name}」已上报`)
} catch (e) {}
}
async function submitDone() {
if (!canReport.value) {
ElMessage.warning('本工位步骤未全部完成或物料未齐套,不能完工确认')
return
}
try {
await submitInspection(base('DONE', { sn: done.value.sn || snState.value, photo: done.value.photo, remark: done.value.remark }))
ElMessage.success('完工确认已提交')
done.value = { sn: '', photo: '', remark: '' }
} catch (e) {}
}
async function submitEmergency() {
if (!emergency.value.remark) {
ElMessage.warning('请填写应急说明')
return
}
try {
await emergencyCall({ stationNo: stationNo.value, orderNo: orderNo.value, remark: emergency.value.remark })
ElMessage.success('应急呼叫已发送')
emergency.value.remark = ''
} catch (e) {}
}
async function submitQty() {
if (diffQty.value === 0) {
ElMessage.warning('应发与实收一致,无需上报')
return
}
try {
await qtyReport({
stationNo: stationNo.value,
orderNo: orderNo.value,
sn: snState.value,
materialCode: qty.value.materialCode,
materialName: qty.value.materialName,
planQty: qty.value.planQty,
actualQty: qty.value.actualQty,
cause: qty.value.cause
})
ElMessage.success('数量不符已上报')
qty.value = { materialCode: '', materialName: '', planQty: 0, actualQty: 0, cause: '' }
} catch (e) {}
}
async function submitReturn() {
try {
await returnMaterial({
orderNo: orderNo.value,
stationNo: stationNo.value,
sn: ret.value.sn,
materialCode: ret.value.materialCode,
materialName: ret.value.materialName,
spec: ret.value.spec,
qty: ret.value.qty,
unit: ret.value.unit,
reason: ret.value.reason
})
ElMessage.success('退料已提交,待库管确认收货')
ret.value = { materialCode: '', materialName: '', spec: '', qty: 1, unit: '', sn: '', reason: '' }
} catch (e) {}
}
</script>
<style scoped>
.inspect { padding: 4px; }
.blocks { margin-top: 12px; }
</style>
@@ -0,0 +1,67 @@
<template>
<div class="photo-field">
<input
ref="fileRef"
type="file"
accept="image/*"
capture="environment"
style="display: none"
@change="onPick"
/>
<el-button size="default" @click="fileRef?.click()">拍照 / 选图</el-button>
<el-button v-if="model" size="default" text type="danger" @click="clear">移除</el-button>
<div v-if="preview" class="prev">
<img :src="preview" alt="预览" />
</div>
<span v-if="uploading" class="tip">上传中</span>
</div>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { uploadInspectionPhoto } from '../../api'
const props = defineProps({ modelValue: { type: String, default: '' } })
const emit = defineEmits(['update:modelValue'])
const fileRef = ref()
const preview = ref('')
const uploading = ref(false)
function onPick(e) {
const file = e.target.files?.[0]
if (!file) return
const reader = new FileReader()
reader.onload = async () => {
const dataUrl = reader.result
preview.value = dataUrl
uploading.value = true
try {
const res = await uploadInspectionPhoto({ filename: file.name, data: dataUrl })
emit('update:modelValue', res?.filename || '')
ElMessage.success('照片已上传')
} catch {
ElMessage.error('照片上传失败')
} finally {
uploading.value = false
}
}
reader.readAsDataURL(file)
e.target.value = ''
}
function clear() {
modelClear()
}
function modelClear() {
emit('update:modelValue', '')
preview.value = ''
}
</script>
<style scoped>
.photo-field { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.prev img { width: 96px; height: 96px; object-fit: cover; border-radius: 6px; border: 1px solid #dcdfe6; }
.tip { color: #909399; }
</style>
@@ -22,7 +22,6 @@
<el-tag :type="myPointSn ? 'success' : 'info'" effect="dark" size="large">
{{ myPointSn ? '在制 ' + myPointSn : '空闲' }}
</el-tag>
<span class="seg-text">路线段{{ segmentText }}</span>
</div>
</section>
@@ -139,7 +138,6 @@ import {
orderNos,
orderNo,
screwCount,
segmentText,
setCurrentSn,
sn,
state,
@@ -281,7 +279,14 @@ function buildStepsPayload() {
for (const s of manualSteps.value) {
const raw = stepValues.value[s.id]
const val = raw === null || raw === undefined || raw === '' ? null : Number(raw)
payload.push({ stepId: s.id, name: s.name, value: val, text: `${s.name}: ${val === null ? '未录入' : val}` })
// checkedneedCheck 步骤的「检测确认」结果,随报工一并上报,MES 服务端会强校验,防止绕过前端
payload.push({
stepId: s.id,
name: s.name,
value: val,
text: `${s.name}: ${val === null ? '未录入' : val}`,
checked: !!stepChecks.value[s.id]
})
}
for (const s of autoSteps.value) {
if (s.isTorque) {
@@ -401,9 +406,6 @@ defineExpose({ focusScan })
margin-top: 10px;
color: #606266;
}
.seg-text {
margin-left: 12px;
}
.task-grid .kv {
display: flex;
align-items: center;