feat: 完成多模块迭代更新
- 新增过程巡检按步骤上报、数量不符上报、工位退料功能 - 增加工单工程编号三级归属字段 - 新增物料安全库存与缺货预警 - 新增附件管理、退库单管理模块 - 优化生产看板展示逻辑与前端页面文案 - 清理冗余的工艺路线相关代码与备份文件
This commit is contained in:
@@ -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}` })
|
||||
// checked:needCheck 步骤的「检测确认」结果,随报工一并上报,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;
|
||||
|
||||
Reference in New Issue
Block a user