feat&refactor: 完成多模块功能迭代与配置优化
本次提交覆盖多个业务模块的功能完善与体验优化:
1. **鉴权与配置调整**:
- 统一JWT滑动续签逻辑,简化Token存储,移除RefreshToken相关冗余代码
- 调整多项目配置文件中JWT过期时间为3600秒,统一会话闲置窗口
- 工位配置放开1~12限制,改为仅校验大于0
2. **术语统一替换**:全链路将"精密件"替换为"电气件",修正物料管理描述
3. **功能新增**:
- 新增工位类型、工艺路线与产线点位台账模块
- 添加工艺PDF预览面板、工位终端代理转发接口
- 新增操作日志按操作人列表筛选、工位登出日志记录
- 新增PLC移料指令与产线点位状态管理
4. **业务流程优化**:
- 调整BOM物料删除校验逻辑,优化工单备料计算
- 补充物料图号、检测单号等追溯字段
- 完善工艺流程图与工位绑定关系说明
- 优化前端页面文案与交互细节
5. **代码规范与维护**:
- 新增通用工具函数与前端静态资源
- 整理路由权限与中间件逻辑
- 修复部分接口与配置的不兼容问题
This commit is contained in:
@@ -52,7 +52,7 @@ const password = ref('')
|
||||
const fixedStation = ref(0)
|
||||
const loading = ref(false)
|
||||
|
||||
// 工位由服务端配置文件固定(1~12),一个工位一个终端,终端不允许修改工位
|
||||
// 工位由服务端配置文件固定,一个工位一个终端,终端不允许修改工位
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const cfg = await getConfig()
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="mat-panel">
|
||||
<!-- ① 装配物料绑定(电气件必须扫码SN / 结构件扫批次) -->
|
||||
<section class="card">
|
||||
<div class="card-title">
|
||||
装配物料绑定
|
||||
<el-tag v-if="bindPanel && bindPanel.enabled && bindPanel.complete" type="success" size="large" effect="dark">
|
||||
已绑齐 ✓
|
||||
</el-tag>
|
||||
<el-tag v-else-if="bindPanel && bindPanel.enabled" type="danger" size="large" effect="dark">
|
||||
未绑齐({{ bindPanel.missing || '缺料' }})
|
||||
</el-tag>
|
||||
<el-tag v-else type="info" size="large">本工位未配置需绑物料</el-tag>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!sn" description="请先在「作业」页扫描工件SN" :image-size="70" />
|
||||
<el-empty v-else-if="!required.length" description="该工件在本工位无需绑定物料" :image-size="70" />
|
||||
|
||||
<div v-for="r in required" :key="r.materialCode" class="bind-item">
|
||||
<div class="bind-meta">
|
||||
<span class="bind-name">{{ r.materialName || r.materialCode }}</span>
|
||||
<span class="bind-code">{{ r.materialCode }}</span>
|
||||
<el-tag size="small" :type="r.manageMode === '2' ? 'primary' : 'warning'">
|
||||
{{ r.manageMode === '2' ? '电气件·扫码SN' : '结构件·批次' }}
|
||||
</el-tag>
|
||||
<span class="bind-count" :class="r.complete ? 'ok' : 'ng'">{{ r.boundCount }}/{{ r.need }}</span>
|
||||
</div>
|
||||
<div class="bind-bound">
|
||||
<el-tag
|
||||
v-for="b in r.bound || []"
|
||||
:key="b"
|
||||
size="small"
|
||||
closable
|
||||
@close="removeBind(r, b)"
|
||||
>{{ b }}</el-tag>
|
||||
<span v-if="!(r.bound || []).length" class="none">未绑定</span>
|
||||
</div>
|
||||
<div class="bind-input-row">
|
||||
<el-input
|
||||
v-model.trim="bindInputs[r.materialCode]"
|
||||
size="large"
|
||||
:placeholder="r.manageMode === '2' ? '扫描电气件SN(必扫)' : '扫描/输入批次号'"
|
||||
clearable
|
||||
@keyup.enter="doBind(r)"
|
||||
/>
|
||||
<el-button type="primary" size="large" :loading="binding" @click="doBind(r)">绑定</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ② 叫料(选本工位接驳台,AGV 送达该接驳台) -->
|
||||
<section class="card">
|
||||
<div class="card-title">叫料(送到本工位接驳台)</div>
|
||||
<div class="call-row">
|
||||
<span class="lab">接驳台</span>
|
||||
<el-select v-model="dock" size="large" class="dock-el" placeholder="选择接驳台">
|
||||
<el-option v-for="d in myDocks" :key="d.pointNo" :label="d.label || d.pointNo" :value="d.pointNo" />
|
||||
</el-select>
|
||||
<el-button size="large" @click="fillFromBind">按缺料填充</el-button>
|
||||
<el-button size="large" @click="addRow">添加一行</el-button>
|
||||
</div>
|
||||
|
||||
<el-table :data="items" border size="large" class="call-table">
|
||||
<el-table-column label="物料编码" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model.trim="row.materialCode" size="large" placeholder="物料编码" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="物料名称" min-width="150">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model.trim="row.materialName" size="large" placeholder="物料名称" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="单位" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-input v-model.trim="row.unit" size="large" placeholder="个/件" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="140">
|
||||
<template #default="{ row }">
|
||||
<el-input-number v-model="row.qty" size="large" :min="0" :precision="2" controls-position="right" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="90" align="center">
|
||||
<template #default="{ $index }">
|
||||
<el-button type="danger" size="large" @click="items.splice($index, 1)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-empty v-if="!items.length" description="暂无叫料明细,可点「按缺料填充」或「添加一行」" :image-size="60" />
|
||||
|
||||
<div class="call-foot">
|
||||
<el-button type="primary" size="large" class="big-btn" :loading="calling" @click="doCall">提交叫料</el-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { bindRecord, bindRemove, callLineMaterial } from '../../api'
|
||||
import {
|
||||
bindInputs,
|
||||
loadBind,
|
||||
myDocks,
|
||||
operatorName,
|
||||
orderNo,
|
||||
sn,
|
||||
state,
|
||||
stationNo
|
||||
} from '../../store/station'
|
||||
|
||||
const bindPanel = computed(() => state.bindPanel)
|
||||
const required = computed(() => (bindPanel.value?.required || []))
|
||||
const binding = ref(false)
|
||||
const calling = ref(false)
|
||||
const dock = ref('')
|
||||
const items = ref([])
|
||||
|
||||
function addRow() {
|
||||
items.value.push({ materialCode: '', materialName: '', unit: '个', manageMode: '1', qty: 1 })
|
||||
}
|
||||
|
||||
// 按绑定面板的缺料情况一键填充(数量 = 需求 - 已绑)
|
||||
function fillFromBind() {
|
||||
const list = required.value
|
||||
.filter((r) => !r.complete)
|
||||
.map((r) => ({
|
||||
materialCode: r.materialCode,
|
||||
materialName: r.materialName || '',
|
||||
unit: r.unit || '个',
|
||||
manageMode: r.manageMode || '1',
|
||||
qty: Math.max(Number(r.need || 0) - Number(r.boundCount || 0), 1)
|
||||
}))
|
||||
if (!list.length) {
|
||||
ElMessage.warning('当前没有缺料')
|
||||
return
|
||||
}
|
||||
items.value = list
|
||||
}
|
||||
|
||||
async function doBind(r) {
|
||||
const v = (bindInputs.value[r.materialCode] || '').trim()
|
||||
if (!v) {
|
||||
ElMessage.warning('请先扫描/输入' + (r.manageMode === '2' ? '电气件SN' : '批次号'))
|
||||
return
|
||||
}
|
||||
if (!sn.value) {
|
||||
ElMessage.warning('请先扫描工件SN')
|
||||
return
|
||||
}
|
||||
binding.value = true
|
||||
try {
|
||||
await bindRecord({
|
||||
sn: sn.value,
|
||||
orderNo: orderNo.value || '',
|
||||
processCode: Number(stationNo.value),
|
||||
stationNo: stationNo.value,
|
||||
items: [{ materialCode: r.materialCode, bindValue: v }]
|
||||
})
|
||||
ElMessage.success('绑定成功:' + (r.materialName || r.materialCode))
|
||||
bindInputs.value[r.materialCode] = ''
|
||||
loadBind()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
binding.value = false
|
||||
}
|
||||
|
||||
async function removeBind(r, b) {
|
||||
try {
|
||||
await bindRemove({
|
||||
sn: sn.value,
|
||||
processCode: Number(stationNo.value),
|
||||
materialCode: r.materialCode,
|
||||
bindValue: b
|
||||
})
|
||||
ElMessage.success('已撤销')
|
||||
loadBind()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
async function doCall() {
|
||||
if (!dock.value) {
|
||||
ElMessage.warning('请选择接驳台')
|
||||
return
|
||||
}
|
||||
const list = items.value.filter((i) => i.materialCode && Number(i.qty) > 0)
|
||||
if (!list.length) {
|
||||
ElMessage.warning('请添加有效的叫料明细(物料编码 + 数量)')
|
||||
return
|
||||
}
|
||||
calling.value = true
|
||||
try {
|
||||
const res = await callLineMaterial({
|
||||
stationNo: stationNo.value,
|
||||
dock: dock.value,
|
||||
orderNo: orderNo.value || '',
|
||||
operator: operatorName(),
|
||||
items: list
|
||||
})
|
||||
ElMessage.success('叫料已提交,共 ' + (res?.count ?? list.length) + ' 项')
|
||||
items.value = []
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
calling.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mat-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 1px 6px rgba(0, 21, 41, 0.08);
|
||||
}
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.bind-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.bind-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.bind-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.bind-code {
|
||||
color: #909399;
|
||||
}
|
||||
.bind-count.ok { color: #67c23a; font-weight: 700; }
|
||||
.bind-count.ng { color: #f56c6c; font-weight: 700; }
|
||||
.bind-bound {
|
||||
margin: 6px 0;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.none {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.bind-input-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.call-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.call-row .lab {
|
||||
color: #909399;
|
||||
}
|
||||
.dock-el {
|
||||
width: 260px;
|
||||
}
|
||||
.call-table {
|
||||
width: 100%;
|
||||
}
|
||||
.call-foot {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.big-btn {
|
||||
min-width: 160px;
|
||||
height: 48px;
|
||||
font-size: 17px;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.mat-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div class="move-panel">
|
||||
<!-- ① 产线点位图(唯一事实源:每点位当前工件) -->
|
||||
<section class="card">
|
||||
<div class="card-title">
|
||||
产线点位(点击选择起点 → 再点终点 → 下发移料)
|
||||
<el-button size="large" @click="clearSel">清空选择</el-button>
|
||||
<el-button size="large" @click="loadPoints">刷新</el-button>
|
||||
</div>
|
||||
|
||||
<div class="belt">
|
||||
<div
|
||||
v-for="p in beltPoints"
|
||||
:key="p.pointNo"
|
||||
class="pt"
|
||||
:class="[typeClass(p), { sel: selected === p.pointNo, mine: String(p.stationNo) === String(stationNo) }]"
|
||||
@click="onPick(p)"
|
||||
>
|
||||
<div class="pt-name">{{ p.pointNo }}</div>
|
||||
<div class="pt-sn">{{ p.occupied ? p.currentSn : '空' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dock-line" v-if="myDocks.length">
|
||||
<span class="lab">本工位接驳台(AGV 点位)</span>
|
||||
<div v-for="d in myDocks" :key="d.pointNo" class="pt dock">
|
||||
<div class="pt-name">{{ d.pointNo }}</div>
|
||||
<div class="pt-sn">物料</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ops">
|
||||
<div class="sel-text">
|
||||
起点:<b>{{ fromText }}</b> | 终点:<b>{{ toText }}</b>
|
||||
</div>
|
||||
<el-button type="primary" size="large" :disabled="!canIssue" @click="onIssue">下发移料</el-button>
|
||||
<el-button size="large" :disabled="!selected" @click="onIn">上料登记</el-button>
|
||||
<el-button type="danger" size="large" :disabled="!selected" @click="onOut">下料释放</el-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ② 移料指令 -->
|
||||
<section class="card">
|
||||
<div class="card-title">移料指令(最近 20 条)</div>
|
||||
<el-table :data="moves" border size="large" height="420">
|
||||
<el-table-column prop="cmdNo" label="指令号" min-width="150" />
|
||||
<el-table-column prop="sn" label="工件SN" min-width="140" />
|
||||
<el-table-column label="起点 → 终点" min-width="140">
|
||||
<template #default="{ row }">{{ row.fromPoint }} → {{ row.toPoint }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusType(row.status)" effect="dark">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" width="110" />
|
||||
<el-table-column label="操作" width="200" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'ISSUED'"
|
||||
size="large"
|
||||
@click="onAck(row)"
|
||||
>应答</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'ISSUED' || row.status === 'ACK'"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="onArrive(row)"
|
||||
>确认到位</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ackLineMove, arriveLineMove, issueLineMove, occupyLinePoint } from '../../api'
|
||||
import {
|
||||
beltPoints,
|
||||
loadMoves,
|
||||
loadPoints,
|
||||
myDocks,
|
||||
orderNo,
|
||||
pointByNo,
|
||||
stationNo,
|
||||
state
|
||||
} from '../../store/station'
|
||||
|
||||
const moves = computed(() => state.moves)
|
||||
const selected = ref('')
|
||||
const from = ref('')
|
||||
const to = ref('')
|
||||
|
||||
const fromText = computed(() => from.value || '未选择')
|
||||
const toText = computed(() => to.value || '未选择')
|
||||
const canIssue = computed(() => {
|
||||
if (!from.value || !to.value || from.value === to.value) return false
|
||||
return !!pointByNo(from.value)?.occupied
|
||||
})
|
||||
|
||||
function clearSel() {
|
||||
selected.value = ''
|
||||
from.value = ''
|
||||
to.value = ''
|
||||
}
|
||||
|
||||
function typeClass(p) {
|
||||
if (p.pointType === 'STATION') return 't-station'
|
||||
if (p.pointType === 'CACHE') return 't-cache'
|
||||
if (p.pointType === 'FEED') return 't-feed'
|
||||
return 't-end'
|
||||
}
|
||||
|
||||
function onPick(p) {
|
||||
selected.value = p.pointNo
|
||||
if (!from.value) {
|
||||
// 起点必须是占用点位
|
||||
if (!p.occupied) {
|
||||
ElMessage.warning(p.pointNo + ' 当前无工件,不能作为起点')
|
||||
return
|
||||
}
|
||||
from.value = p.pointNo
|
||||
return
|
||||
}
|
||||
if (p.pointNo === from.value) {
|
||||
to.value = ''
|
||||
return
|
||||
}
|
||||
if (p.occupied) {
|
||||
ElMessage.warning(p.pointNo + ' 已被占用,不能作为终点')
|
||||
return
|
||||
}
|
||||
to.value = p.pointNo
|
||||
}
|
||||
|
||||
async function onIssue() {
|
||||
const src = pointByNo(from.value)
|
||||
if (!src) return
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认把「${src.currentSn}」从 ${from.value} 移到 ${to.value}?`,
|
||||
'移料确认',
|
||||
{ confirmButtonText: '确认下发', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await issueLineMove({
|
||||
sn: src.currentSn,
|
||||
fromPoint: from.value,
|
||||
toPoint: to.value,
|
||||
orderNo: orderNo.value || src.orderNo || ''
|
||||
})
|
||||
ElMessage.success('移料指令已下发')
|
||||
clearSel()
|
||||
loadPoints()
|
||||
loadMoves()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
async function onIn() {
|
||||
const p = pointByNo(selected.value)
|
||||
if (!p) return
|
||||
if (p.occupied) {
|
||||
ElMessage.warning(p.pointNo + ' 已被 ' + p.currentSn + ' 占用')
|
||||
return
|
||||
}
|
||||
let sn = ''
|
||||
try {
|
||||
const r = await ElMessageBox.prompt('输入要登记到 ' + p.pointNo + ' 的工件SN', '上料登记', {
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
inputPlaceholder: '工件SN'
|
||||
})
|
||||
sn = (r.value || '').trim()
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
if (!sn) return
|
||||
try {
|
||||
await occupyLinePoint({ pointNo: p.pointNo, sn, orderNo: orderNo.value || '', action: 'IN' })
|
||||
ElMessage.success('已登记上料')
|
||||
loadPoints()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
async function onOut() {
|
||||
const p = pointByNo(selected.value)
|
||||
if (!p || !p.occupied) {
|
||||
ElMessage.warning('该点位当前无工件')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认把「${p.currentSn}」从 ${p.pointNo} 下料释放?`, '下料确认', {
|
||||
confirmButtonText: '确认下料',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await occupyLinePoint({ pointNo: p.pointNo, sn: p.currentSn, orderNo: '', action: 'OUT' })
|
||||
ElMessage.success('已释放点位')
|
||||
clearSel()
|
||||
loadPoints()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
async function onAck(row) {
|
||||
try {
|
||||
await ackLineMove({ cmdNo: row.cmdNo })
|
||||
ElMessage.success('已应答')
|
||||
loadMoves()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
async function onArrive(row) {
|
||||
try {
|
||||
await arriveLineMove({ cmdNo: row.cmdNo })
|
||||
ElMessage.success('到位已记账')
|
||||
loadMoves()
|
||||
loadPoints()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
function statusText(s) {
|
||||
return { ISSUED: '已下发', ACK: '已应答', ARRIVED: '已到位', CLOSED: '已闭环', FAILED: '失败' }[s] || s
|
||||
}
|
||||
function statusType(s) {
|
||||
return { ISSUED: 'warning', ACK: 'primary', ARRIVED: 'success', CLOSED: 'info', FAILED: 'danger' }[s] || 'info'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.move-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 1px 6px rgba(0, 21, 41, 0.08);
|
||||
}
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.belt {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
.pt {
|
||||
min-width: 78px;
|
||||
border: 2px solid #dcdfe6;
|
||||
border-radius: 8px;
|
||||
padding: 8px 6px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: #fafafa;
|
||||
}
|
||||
.pt-name {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
}
|
||||
.pt-sn {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
margin-top: 4px;
|
||||
word-break: break-all;
|
||||
}
|
||||
.t-station { background: #e8f3ff; border-color: #79bbff; }
|
||||
.t-cache { background: #f4f4f5; border-color: #d3dce6; }
|
||||
.t-end { background: #f3e8ff; border-color: #b488f0; }
|
||||
.t-feed { background: #fff7e6; border-color: #ffc069; }
|
||||
.pt.mine { box-shadow: 0 0 0 3px rgba(31, 111, 235, 0.25); }
|
||||
.pt.sel { border-color: #f56c6c; background: #fef0f0; }
|
||||
.dock-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dock-line .lab {
|
||||
color: #909399;
|
||||
font-size: 14px;
|
||||
}
|
||||
.pt.dock {
|
||||
background: #eafaf1;
|
||||
border-color: #67c23a;
|
||||
min-width: 92px;
|
||||
cursor: default;
|
||||
}
|
||||
.ops {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sel-text {
|
||||
font-size: 15px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.belt {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="pdf-panel">
|
||||
<section class="card">
|
||||
<div class="card-title">
|
||||
工艺文件
|
||||
<el-tag v-if="pdfFile" type="success" size="large" effect="dark">{{ flowName }}</el-tag>
|
||||
<el-button type="primary" size="large" :disabled="!pdfFile" @click="openPdf">放大查看</el-button>
|
||||
</div>
|
||||
<el-empty v-if="!pdfFile" description="本工位当前流程未上传工艺文件" :image-size="90" />
|
||||
<iframe v-else :src="pdfUrl" class="pdf-frame" title="工艺文件"></iframe>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { pdfOpenUrl } from '../../api'
|
||||
import { flowName, state } from '../../store/station'
|
||||
|
||||
const pdfFile = computed(() => {
|
||||
const f = state.task?.flow?.pdfFile
|
||||
return typeof f === 'string' && f.trim() !== '' ? f.trim() : ''
|
||||
})
|
||||
const pdfUrl = computed(() => (pdfFile.value ? pdfOpenUrl(pdfFile.value) : ''))
|
||||
|
||||
function openPdf() {
|
||||
if (pdfUrl.value) window.open(pdfUrl.value, '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 1px 6px rgba(0, 21, 41, 0.08);
|
||||
}
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.pdf-frame {
|
||||
width: 100%;
|
||||
height: calc(100vh - 260px);
|
||||
min-height: 420px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,503 @@
|
||||
<template>
|
||||
<div class="work-panel">
|
||||
<!-- ① 工件区:扫码上料 / 取料 -->
|
||||
<section class="card sn-card">
|
||||
<div class="card-title">当前工件</div>
|
||||
<div class="sn-big" :class="{ empty: !sn }">{{ sn || '未上料' }}</div>
|
||||
<div class="sn-row">
|
||||
<el-input
|
||||
ref="scanRef"
|
||||
v-model.trim="scanInput"
|
||||
size="large"
|
||||
class="scan-input"
|
||||
placeholder="扫描或输入工件SN"
|
||||
clearable
|
||||
@keyup.enter="onScan"
|
||||
/>
|
||||
<el-button type="primary" size="large" :loading="taking" @click="onTakeIn">上料 / 取料到本工位</el-button>
|
||||
<el-button size="large" :disabled="!sn" @click="onMoveOut">移出本工位</el-button>
|
||||
</div>
|
||||
<div class="point-line">
|
||||
本工位点位:<b>{{ myPointLabel }}</b> · 状态:
|
||||
<el-tag :type="myPointSn ? 'success' : 'info'" effect="dark" size="large">
|
||||
{{ myPointSn ? '在制 ' + myPointSn : '空闲' }}
|
||||
</el-tag>
|
||||
<span class="seg-text">路线段:{{ segmentText }}</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ② 任务区(工单 + 流程) -->
|
||||
<section class="card task-card">
|
||||
<div class="card-title">当前任务</div>
|
||||
<div class="task-grid">
|
||||
<div class="kv">
|
||||
<span>工艺流程</span>
|
||||
<b>{{ flowName }}</b>
|
||||
<el-tag :type="flowActive ? 'success' : 'info'" size="large" effect="dark">
|
||||
{{ flowActive ? '启用' : '停用' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="kv">
|
||||
<span>工单号</span>
|
||||
<el-select v-model="orderNo" size="large" class="order-el" placeholder="选择工单号">
|
||||
<el-option v-for="(o, i) in orderNos" :key="i" :label="o" :value="o" />
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ③ 步骤采集 -->
|
||||
<section class="card steps-card">
|
||||
<div class="steps-head">
|
||||
<span class="card-title">步骤采集</span>
|
||||
<el-tag size="large" effect="dark" :type="stepSummaryType">{{ stepSummaryText }}</el-tag>
|
||||
</div>
|
||||
|
||||
<el-empty v-if="!manualSteps.length && !autoSteps.length" description="本工位暂无工艺步骤" :image-size="80" />
|
||||
|
||||
<div v-for="s in manualSteps" :key="s.id" class="step-item">
|
||||
<div class="step-head">
|
||||
<span class="step-name">{{ s.name || '步骤 ' + s.seq }}</span>
|
||||
<el-tag size="large" :type="stepDisplay(s).type" effect="dark">{{ stepDisplay(s).text }}</el-tag>
|
||||
</div>
|
||||
<div class="step-input">
|
||||
<el-input-number
|
||||
v-model="stepValues[s.id]"
|
||||
size="large"
|
||||
:controls-position="'right'"
|
||||
:precision="2"
|
||||
class="num-input"
|
||||
/>
|
||||
<span class="step-remark">{{ s.remark }}</span>
|
||||
</div>
|
||||
<div v-if="s.criteria && s.criteria.length" class="criteria">
|
||||
<div v-for="c in s.criteria" :key="c.id" class="criterion" :class="criterionClass(c, stepValues[s.id])">
|
||||
<span class="c-name">{{ c.name }}</span>
|
||||
<span class="c-target">{{ criterionText(c) }}</span>
|
||||
<span class="c-result">{{ criterionMark(c, stepValues[s.id]) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="s.needCheck" class="check-row">
|
||||
<el-checkbox v-model="stepChecks[s.id]" size="large" border>检测确认(合格)</el-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-for="s in autoSteps" :key="s.id" class="step-item auto">
|
||||
<div class="step-head">
|
||||
<span class="step-name">{{ s.name || '自动采集' }}</span>
|
||||
<el-tag size="large" :type="autoTorqueState.ok ? 'success' : 'warning'" effect="dark">
|
||||
{{ autoTorqueState.text }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="step-input">
|
||||
<span class="tight-progress">
|
||||
拧紧 {{ autoTorqueOk }} / {{ tightRows.length }}
|
||||
<template v-if="screwCount"> (本工位需拧 {{ screwCount }} 颗)</template>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ④ 底部操作 -->
|
||||
<footer class="action-bar">
|
||||
<el-button type="success" size="large" class="big-btn" :disabled="!canReport" @click="onProcessDone">
|
||||
报工完成
|
||||
</el-button>
|
||||
<el-button type="warning" size="large" class="big-btn" :disabled="!sn" @click="onTempStore">暂存退库</el-button>
|
||||
<div class="block-tip" v-if="blockTip">{{ blockTip }}</div>
|
||||
</footer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { arriveLineMove, reportProcessDone, reportTempStore } from '../../api'
|
||||
import {
|
||||
autoSteps,
|
||||
autoTorqueOk,
|
||||
autoTorqueState,
|
||||
bindBlocking,
|
||||
bringSnToStation,
|
||||
canReport,
|
||||
criterionOk,
|
||||
flowActive,
|
||||
flowName,
|
||||
hasNg,
|
||||
loadBind,
|
||||
loadMoves,
|
||||
loadPoints,
|
||||
loadStats,
|
||||
loadTask,
|
||||
manualSteps,
|
||||
missingChecks,
|
||||
missingSteps,
|
||||
moveOut,
|
||||
myPoint,
|
||||
myPointSn,
|
||||
operatorName,
|
||||
orderNos,
|
||||
orderNo,
|
||||
screwCount,
|
||||
segmentText,
|
||||
setCurrentSn,
|
||||
sn,
|
||||
state,
|
||||
stepChecks,
|
||||
stepDisplay,
|
||||
stepValues,
|
||||
stationNo,
|
||||
suggestOutPoint,
|
||||
tightRows
|
||||
} from '../../store/station'
|
||||
|
||||
const scanRef = ref()
|
||||
const scanInput = ref('')
|
||||
const taking = ref(false)
|
||||
|
||||
const myPointLabel = computed(() => (myPoint.value ? myPoint.value.label || myPoint.value.pointNo : '工位' + stationNo.value))
|
||||
|
||||
const stepSummaryText = computed(() => {
|
||||
const total = manualSteps.value.length
|
||||
const done = total - missingSteps.value.length
|
||||
return `步骤 ${done}/${total}`
|
||||
})
|
||||
const stepSummaryType = computed(() => (missingSteps.value.length ? 'warning' : 'success'))
|
||||
|
||||
const blockTip = computed(() => {
|
||||
if (!sn.value) return '请先扫描工件 SN'
|
||||
if (!orderNo.value) return '请选择工单号'
|
||||
if (bindBlocking.value) return '装配物料未绑齐:' + (state.bindPanel?.missing || '缺料')
|
||||
if (missingSteps.value.length) return '还有步骤未录入:' + (missingSteps.value[0].name || '未命名步骤')
|
||||
if (missingChecks.value.length) return '还有步骤未做检测确认:' + (missingChecks.value[0].name || '未命名步骤')
|
||||
if (autoSteps.value.some((s) => s.isTorque) && !autoTorqueState.value.ok) return '拧紧步骤尚未完成'
|
||||
return ''
|
||||
})
|
||||
|
||||
function focusScan() {
|
||||
scanRef.value?.focus?.()
|
||||
}
|
||||
|
||||
// 扫码后:若 SN 已在本工位直接开干;否则提示去"上料/取料"
|
||||
async function onScan() {
|
||||
const v = scanInput.value.trim()
|
||||
if (!v) return
|
||||
scanInput.value = ''
|
||||
const onPoint = state.points.find((p) => p.currentSn === v && p.occupied)
|
||||
if (onPoint && String(onPoint.pointNo) === String(stationNo.value)) {
|
||||
setCurrentSn(v)
|
||||
ElMessage.success('已装载工件 ' + v)
|
||||
return
|
||||
}
|
||||
if (onPoint) {
|
||||
setCurrentSn(v)
|
||||
ElMessage.warning('该工件在 ' + (onPoint.label || onPoint.pointNo) + ',请点「上料 / 取料到本工位」调入')
|
||||
return
|
||||
}
|
||||
setCurrentSn(v)
|
||||
ElMessage.warning('产线台账无该工件,请点「上料 / 取料到本工位」登记到本工位')
|
||||
}
|
||||
|
||||
async function onTakeIn() {
|
||||
const v = (scanInput.value || '').trim() || sn.value
|
||||
if (!v) {
|
||||
ElMessage.warning('请先扫描或输入工件SN')
|
||||
focusScan()
|
||||
return
|
||||
}
|
||||
taking.value = true
|
||||
try {
|
||||
const r = await bringSnToStation(v)
|
||||
await loadPoints()
|
||||
if (r.mode === 'MOVE' && r.cmdNo) {
|
||||
// PLC 联调前由人工确认到位;联调后由 PLC 到位信号自动闭环
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`已下发移料指令:${r.fromPoint} → 工位${stationNo.value}。工件到位后点「确认到位」记账。`,
|
||||
'移料指令已下发',
|
||||
{ confirmButtonText: '确认到位', cancelButtonText: '稍后', type: 'info' }
|
||||
)
|
||||
await arriveLineMove({ cmdNo: r.cmdNo })
|
||||
await loadPoints()
|
||||
await loadMoves()
|
||||
} catch { /* 稍后:指令留在列表,可在移料页确认 */ }
|
||||
}
|
||||
setCurrentSn(v)
|
||||
scanInput.value = ''
|
||||
ElMessage.success(r.mode === 'IN' ? '已登记上料到本工位' : r.mode === 'HERE' ? '工件已在本工位' : '工件已调入本工位')
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
taking.value = false
|
||||
focusScan()
|
||||
}
|
||||
|
||||
async function onMoveOut() {
|
||||
if (!sn.value) return
|
||||
const target = suggestOutPoint.value
|
||||
if (!target) {
|
||||
ElMessage.warning('下游无可用点位')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认把「${sn.value}」移出本工位到 ${target.label || target.pointNo}?`,
|
||||
'移出确认',
|
||||
{ confirmButtonText: '确认移出', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const cmdNo = await moveOut(sn.value, target.pointNo)
|
||||
try {
|
||||
await arriveLineMove({ cmdNo })
|
||||
} catch { /* 指令保留在列表 */ }
|
||||
await loadPoints()
|
||||
await loadMoves()
|
||||
setCurrentSn('')
|
||||
ElMessage.success('已移出到 ' + (target.label || target.pointNo))
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
const OP_TEXT = { GE: '≥', LE: '≤', GT: '>', LT: '<', EQUAL: '=' }
|
||||
function criterionText(c) {
|
||||
if (c?.logic === 'RANGE') {
|
||||
const min = Number(c.min)
|
||||
const max = Number(c.max)
|
||||
return `${isNaN(min) ? '-' : min} ~ ${isNaN(max) ? '-' : max} ${c.unit || ''}`
|
||||
}
|
||||
return `${OP_TEXT[c?.logic] || c?.logic || '='} ${c.target} ${c.unit || ''}`
|
||||
}
|
||||
function criterionClass(c, v) {
|
||||
if (v === null || v === undefined || v === '') return 'c-pending'
|
||||
return criterionOk(c, v) ? 'c-ok' : 'c-ng'
|
||||
}
|
||||
function criterionMark(c, v) {
|
||||
if (v === null || v === undefined || v === '') return '·'
|
||||
return criterionOk(c, v) ? '✓' : '✗'
|
||||
}
|
||||
|
||||
function buildStepsPayload() {
|
||||
const payload = []
|
||||
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}` })
|
||||
}
|
||||
for (const s of autoSteps.value) {
|
||||
if (s.isTorque) {
|
||||
payload.push({
|
||||
stepId: s.id,
|
||||
name: s.name,
|
||||
value: autoTorqueOk.value,
|
||||
text: `拧紧 ${autoTorqueOk.value}/${tightRows.value.length} OK`
|
||||
})
|
||||
}
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
async function onProcessDone() {
|
||||
if (!canReport.value) {
|
||||
ElMessage.warning(blockTip.value || '当前不满足报工条件')
|
||||
return
|
||||
}
|
||||
const allOk = !hasNg.value
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认报工?工件 ${sn.value} · 工单 ${orderNo.value} · 工位 ${stationNo.value}`,
|
||||
'报工确认',
|
||||
{ confirmButtonText: '确认报工', cancelButtonText: '取消', type: 'warning' }
|
||||
)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await reportProcessDone({
|
||||
sn: sn.value,
|
||||
orderNo: orderNo.value,
|
||||
processCode: Number(stationNo.value),
|
||||
stationNo: stationNo.value,
|
||||
steps: buildStepsPayload(),
|
||||
operator: operatorName()
|
||||
})
|
||||
ElMessage.success(data?.queued ? 'MES 暂不可达,已离线缓存,稍后自动重传' : '报工成功')
|
||||
setCurrentSn('')
|
||||
loadTask()
|
||||
loadStats()
|
||||
loadPoints()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
async function onTempStore() {
|
||||
if (!sn.value) {
|
||||
ElMessage.warning('请先扫描工件 SN')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(`确认将工件「${sn.value}」暂存退库?`, '暂存退库', {
|
||||
confirmButtonText: '确认暂存',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await reportTempStore({
|
||||
sn: sn.value,
|
||||
orderNo: orderNo.value || '',
|
||||
stationNo: stationNo.value,
|
||||
processCode: Number(stationNo.value),
|
||||
operator: operatorName()
|
||||
})
|
||||
ElMessage.success(data?.queued ? 'MES 暂不可达,已离线缓存' : '暂存退库已上报')
|
||||
setCurrentSn('')
|
||||
loadTask()
|
||||
loadStats()
|
||||
loadPoints()
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
defineExpose({ focusScan })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.work-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(320px, 1fr) minmax(360px, 1.3fr) minmax(360px, 1.3fr);
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
box-shadow: 0 1px 6px rgba(0, 21, 41, 0.08);
|
||||
}
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sn-card .sn-big {
|
||||
font-size: 34px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
color: #1f6feb;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.sn-big.empty {
|
||||
color: #c0c4cc;
|
||||
}
|
||||
.sn-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.sn-row .scan-input {
|
||||
flex: 1 1 220px;
|
||||
}
|
||||
.point-line {
|
||||
margin-top: 10px;
|
||||
color: #606266;
|
||||
}
|
||||
.seg-text {
|
||||
margin-left: 12px;
|
||||
}
|
||||
.task-grid .kv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.task-grid .kv span {
|
||||
color: #909399;
|
||||
min-width: 72px;
|
||||
}
|
||||
.order-el {
|
||||
flex: 1;
|
||||
}
|
||||
.steps-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.step-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.step-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.step-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.step-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.step-remark {
|
||||
color: #909399;
|
||||
}
|
||||
.num-input {
|
||||
width: 190px;
|
||||
}
|
||||
.criteria {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.criterion {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
padding: 2px 0;
|
||||
}
|
||||
.c-pending { color: #909399; }
|
||||
.c-ok { color: #67c23a; }
|
||||
.c-ng { color: #f56c6c; }
|
||||
.check-row {
|
||||
margin-top: 8px;
|
||||
}
|
||||
.tight-progress {
|
||||
font-size: 16px;
|
||||
}
|
||||
.action-bar {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 12px 16px;
|
||||
box-shadow: 0 1px 6px rgba(0, 21, 41, 0.08);
|
||||
}
|
||||
.big-btn {
|
||||
min-width: 160px;
|
||||
height: 52px;
|
||||
font-size: 18px;
|
||||
}
|
||||
.block-tip {
|
||||
color: #e6a23c;
|
||||
font-size: 15px;
|
||||
}
|
||||
@media (max-width: 1280px) {
|
||||
.work-panel {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.steps-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.work-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<el-dialog v-model="visible" title="我的工作量" width="80%" top="6vh" destroy-on-close>
|
||||
<div class="wl-filter">
|
||||
<span class="lab">工位</span>
|
||||
<el-select v-model="wlStation" size="large" class="sel" placeholder="全部工位">
|
||||
<el-option label="全部工位" :value="0" />
|
||||
<el-option v-for="s in stationOptions" :key="s" :label="'工位 ' + s" :value="s" />
|
||||
</el-select>
|
||||
<span class="lab">日期</span>
|
||||
<el-date-picker
|
||||
v-model="wlRange"
|
||||
type="daterange"
|
||||
size="large"
|
||||
value-format="YYYY-MM-DD"
|
||||
start-placeholder="开始"
|
||||
end-placeholder="结束"
|
||||
/>
|
||||
<el-button type="primary" size="large" @click="load">查询</el-button>
|
||||
</div>
|
||||
|
||||
<div class="wl-summary">
|
||||
<div class="sum-item">完成 <b>{{ summary.doneCount }}</b></div>
|
||||
<div class="sum-item ok">合格 <b>{{ summary.okCount }}</b></div>
|
||||
<div class="sum-item ng">不合格 <b>{{ summary.ngCount }}</b></div>
|
||||
<div class="sum-item">合格率 <b>{{ summary.rate }}</b></div>
|
||||
</div>
|
||||
|
||||
<el-table :data="rows" border size="large" height="420">
|
||||
<el-table-column prop="date" label="日期" width="130" />
|
||||
<el-table-column prop="stationNo" label="工位" width="90" />
|
||||
<el-table-column prop="processName" label="工序" min-width="160" />
|
||||
<el-table-column prop="operator" label="操作人" width="120" />
|
||||
<el-table-column prop="doneCount" label="完成" width="90" />
|
||||
<el-table-column prop="okCount" label="合格" width="90" />
|
||||
<el-table-column prop="ngCount" label="不合格" width="100" />
|
||||
</el-table>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { getWorkload } from '../../api'
|
||||
import { loadStationNos } from '../../utils/stations'
|
||||
import { getUser } from '../../utils/request'
|
||||
import { stationNo as curStation } from '../../store/station'
|
||||
|
||||
const user = getUser()
|
||||
const visible = ref(false)
|
||||
const wlStation = ref(0)
|
||||
const wlRange = ref(null)
|
||||
const stationOptions = ref([])
|
||||
const rows = ref([])
|
||||
const summary = ref({ doneCount: 0, okCount: 0, ngCount: 0, rate: '-' })
|
||||
|
||||
async function open() {
|
||||
wlStation.value = curStation.value || 0
|
||||
stationOptions.value = await loadStationNos().catch(() => [])
|
||||
visible.value = true
|
||||
load()
|
||||
}
|
||||
|
||||
async function load() {
|
||||
const [from, to] = wlRange.value || []
|
||||
try {
|
||||
const data = await getWorkload({
|
||||
operator: user.name || user.username || '',
|
||||
stationNo: wlStation.value || 0,
|
||||
from: from || '',
|
||||
to: to || ''
|
||||
})
|
||||
const list = (data && Array.isArray(data.rows) ? data.rows : []) || []
|
||||
rows.value = list
|
||||
let doneCount = 0, okCount = 0, ngCount = 0
|
||||
list.forEach((r) => {
|
||||
doneCount += Number(r.doneCount) || 0
|
||||
okCount += Number(r.okCount) || 0
|
||||
ngCount += Number(r.ngCount) || 0
|
||||
})
|
||||
summary.value = {
|
||||
doneCount,
|
||||
okCount,
|
||||
ngCount,
|
||||
rate: doneCount ? ((okCount / doneCount) * 100).toFixed(1) + '%' : '-'
|
||||
}
|
||||
} catch (e) { /* 统一错误提示 */ }
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.wl-filter {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.lab {
|
||||
color: #909399;
|
||||
}
|
||||
.sel {
|
||||
width: 180px;
|
||||
}
|
||||
.wl-summary {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.sum-item.ok b { color: #67c23a; }
|
||||
.sum-item.ng b { color: #f56c6c; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user