1. 新增预警表添加工单号字段,支持按工单号检索预警 2. 添加工单过滤在制品/绩效报表/预警中心查询 3. 补全WMS台账字段口径与权限配置 4. 修复备料单与数量不符处理流程 5. 新增配送进度/库位联动/自动出库功能 6. 修复AGV配送状态更新逻辑 7. 优化退料与库存管理细节
260 lines
11 KiB
Vue
260 lines
11 KiB
Vue
<template>
|
||
<el-card>
|
||
<el-form inline>
|
||
<el-form-item label="操作人">
|
||
<el-input v-model="query.operator" placeholder="输入操作人" clearable style="width:180px" @keyup.enter="load" />
|
||
</el-form-item>
|
||
<el-form-item label="工位">
|
||
<el-select v-model="query.stationNo" clearable placeholder="全部工位" style="width:130px">
|
||
<el-option v-for="i in stationNos" :key="i" :label="`工位${i}`" :value="String(i)" />
|
||
</el-select>
|
||
</el-form-item>
|
||
<el-form-item label="工单号">
|
||
<el-input v-model="query.orderNo" placeholder="工单号(模糊)" clearable style="width:180px" @keyup.enter="load" />
|
||
</el-form-item>
|
||
<el-form-item label="日期范围">
|
||
<el-date-picker
|
||
v-model="query.range"
|
||
type="daterange"
|
||
value-format="YYYY-MM-DD"
|
||
start-placeholder="开始日期"
|
||
end-placeholder="结束日期"
|
||
style="width:260px"
|
||
@change="load" />
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="primary" @click="load">查询</el-button>
|
||
</el-form-item>
|
||
<el-form-item>
|
||
<el-button type="success" @click="exportXlsx">导出 xlsx(三 sheet)</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<!-- 汇总卡(按人聚合是全量推算的分页首页,统计卡随查询刷新) -->
|
||
<el-row :gutter="16" style="margin-bottom:14px">
|
||
<el-col :span="6">
|
||
<el-card shadow="never" class="stat-card">
|
||
<div class="stat-label">总完成数</div>
|
||
<div class="stat-value">{{ summary.doneCount }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card shadow="never" class="stat-card">
|
||
<div class="stat-label">总合格数</div>
|
||
<div class="stat-value ok">{{ summary.okCount }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card shadow="never" class="stat-card">
|
||
<div class="stat-label">总不合格数</div>
|
||
<div class="stat-value ng">{{ summary.ngCount }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
<el-col :span="6">
|
||
<el-card shadow="never" class="stat-card">
|
||
<div class="stat-label">总合格率</div>
|
||
<div class="stat-value">{{ summary.rate }}</div>
|
||
</el-card>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<el-tabs v-model="activeTab">
|
||
<!-- 按人 -->
|
||
<el-tab-pane label="按人" name="byOperator">
|
||
<el-table :data="views.byOperator.list" border size="small">
|
||
<el-table-column prop="operator" label="操作人" min-width="130" />
|
||
<el-table-column prop="doneCount" label="完成数" width="110" align="right" />
|
||
<el-table-column prop="okCount" label="合格数" width="110" align="right" />
|
||
<el-table-column prop="ngCount" label="不合格数" width="110" align="right" />
|
||
<el-table-column label="合格率" width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="rateOf(row) >= 100 ? 'success' : (rateOf(row) >= 0 ? 'warning' : 'info')" effect="plain">
|
||
{{ rateText(row) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="平均作业时长" width="120" align="right">
|
||
<template #default="{ row }">{{ fmtDur(row.avgDurationSec) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="效率(件/时)" width="110" align="right">
|
||
<template #default="{ row }">{{ fmtEff(row.efficiency) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-pagination style="margin-top:12px;justify-content:flex-end" background layout="total, prev, pager, next"
|
||
:total="views.byOperator.total" :page-size="pageSize" v-model:current-page="views.byOperator.page"
|
||
@current-change="load" />
|
||
</el-tab-pane>
|
||
|
||
<!-- 按工位 -->
|
||
<el-tab-pane label="按工位" name="byStation">
|
||
<el-table :data="views.byStation.list" border size="small">
|
||
<el-table-column prop="stationNo" label="工位" width="90" align="center" />
|
||
<el-table-column prop="doneCount" label="完成数" width="110" align="right" />
|
||
<el-table-column prop="okCount" label="合格数" width="110" align="right" />
|
||
<el-table-column prop="ngCount" label="不合格数" width="110" align="right" />
|
||
<el-table-column label="合格率" width="110" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="rateOf(row) >= 100 ? 'success' : (rateOf(row) >= 0 ? 'warning' : 'info')" effect="plain">
|
||
{{ rateText(row) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="平均作业时长" width="120" align="right">
|
||
<template #default="{ row }">{{ fmtDur(row.avgDurationSec) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="效率(件/时)" width="110" align="right">
|
||
<template #default="{ row }">{{ fmtEff(row.efficiency) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-pagination style="margin-top:12px;justify-content:flex-end" background layout="total, prev, pager, next"
|
||
:total="views.byStation.total" :page-size="pageSize" v-model:current-page="views.byStation.page"
|
||
@current-change="load" />
|
||
</el-tab-pane>
|
||
|
||
<!-- 明细 -->
|
||
<el-tab-pane label="明细" name="detail">
|
||
<el-table :data="views.detail.list" border size="small">
|
||
<el-table-column prop="operator" label="操作人" min-width="110" />
|
||
<el-table-column prop="date" label="日期" width="110" />
|
||
<el-table-column prop="stationNo" label="工位" width="80" align="center" />
|
||
<el-table-column prop="processName" label="工序名" min-width="130" show-overflow-tooltip />
|
||
<el-table-column prop="doneCount" label="完成数" width="90" align="right" />
|
||
<el-table-column prop="okCount" label="合格数" width="90" align="right" />
|
||
<el-table-column prop="ngCount" label="不合格数" width="90" align="right" />
|
||
<el-table-column label="合格率" width="100" align="center">
|
||
<template #default="{ row }">
|
||
<el-tag :type="rateOf(row) >= 100 ? 'success' : (rateOf(row) >= 0 ? 'warning' : 'info')" effect="plain">
|
||
{{ rateText(row) }}
|
||
</el-tag>
|
||
</template>
|
||
</el-table-column>
|
||
<el-table-column label="平均作业时长" width="120" align="right">
|
||
<template #default="{ row }">{{ fmtDur(row.avgDurationSec) }}</template>
|
||
</el-table-column>
|
||
<el-table-column label="效率(件/时)" width="110" align="right">
|
||
<template #default="{ row }">{{ fmtEff(row.efficiency) }}</template>
|
||
</el-table-column>
|
||
</el-table>
|
||
<el-pagination style="margin-top:12px;justify-content:flex-end" background layout="total, prev, pager, next"
|
||
:total="views.detail.total" :page-size="pageSize" v-model:current-page="views.detail.page"
|
||
@current-change="load" />
|
||
</el-tab-pane>
|
||
</el-tabs>
|
||
</el-card>
|
||
<PageHelp :data="helpPerformance" page="Performance" />
|
||
</template>
|
||
|
||
<script setup>
|
||
import { ref, reactive, computed, onMounted } from 'vue'
|
||
import { ElMessage } from 'element-plus'
|
||
import PageHelp from '../components/PageHelp.vue'
|
||
import { helpPerformance } from '../help'
|
||
import { loadStationNos } from '../utils/stations'
|
||
import request from '../utils/request'
|
||
|
||
// 后端三视图各自真分页(默认20):byOperator/byStation/detail 各带 {total, list}
|
||
const activeTab = ref('byOperator')
|
||
const pageSize = ref(20)
|
||
const stationNos = ref([])
|
||
const query = reactive({ operator: '', stationNo: '', orderNo: '', range: null })
|
||
const views = reactive({
|
||
byOperator: { total: 0, page: 1, list: [] },
|
||
byStation: { total: 0, page: 1, list: [] },
|
||
detail: { total: 0, page: 1, list: [] }
|
||
})
|
||
|
||
const summary = computed(() => {
|
||
let doneCount = 0, okCount = 0, ngCount = 0
|
||
views.byOperator.list.forEach((r) => {
|
||
doneCount += Number(r.doneCount) || 0
|
||
okCount += Number(r.okCount) || 0
|
||
ngCount += Number(r.ngCount) || 0
|
||
})
|
||
return {
|
||
doneCount, okCount, ngCount,
|
||
rate: doneCount ? ((okCount / doneCount) * 100).toFixed(1) + '%' : '-'
|
||
}
|
||
})
|
||
|
||
function rateOf(row) {
|
||
const done = Number(row.doneCount) || 0
|
||
const ok = Number(row.okCount) || 0
|
||
return done ? (ok / done) * 100 : -1
|
||
}
|
||
function rateText(row) {
|
||
const r = rateOf(row)
|
||
return r < 0 ? '-' : r.toFixed(1) + '%'
|
||
}
|
||
// 作业时长(秒)→ 人性化显示;效率(件/小时)保留一位小数
|
||
function fmtDur(sec) {
|
||
const s = Number(sec) || 0
|
||
if (s <= 0) return '-'
|
||
if (s < 60) return s + '秒'
|
||
const m = Math.floor(s / 60)
|
||
const rs = s % 60
|
||
if (m < 60) return rs ? `${m}分${rs}秒` : `${m}分`
|
||
const h = Math.floor(m / 60)
|
||
const rm = m % 60
|
||
return rm ? `${h}时${rm}分` : `${h}时`
|
||
}
|
||
function fmtEff(v) {
|
||
const n = Number(v) || 0
|
||
return n > 0 ? n.toFixed(1) : '-'
|
||
}
|
||
|
||
function defaultRange() {
|
||
const d = new Date()
|
||
const p = (n) => String(n).padStart(2, '0')
|
||
const end = `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
|
||
const s = new Date(d.getTime() - 90 * 24 * 3600 * 1000)
|
||
return [`${s.getFullYear()}-${p(s.getMonth() + 1)}-${p(s.getDate())}`, end]
|
||
}
|
||
if (!query.range) query.range = defaultRange()
|
||
|
||
async function load() {
|
||
const [from, to] = query.range || []
|
||
const params = {
|
||
operator: query.operator || '',
|
||
stationNo: query.stationNo || '',
|
||
orderNo: query.orderNo.trim(),
|
||
from: from || '',
|
||
to: to || '',
|
||
opPage: views.byOperator.page, opPageSize: pageSize.value,
|
||
stPage: views.byStation.page, stPageSize: pageSize.value,
|
||
detPage: views.detail.page, detPageSize: pageSize.value
|
||
}
|
||
const data = (await request.get('/performance', { params })) || {}
|
||
for (const k of ['byOperator', 'byStation', 'detail']) {
|
||
const v = data[k] || {}
|
||
views[k].total = v.total || 0
|
||
views[k].list = v.list || []
|
||
}
|
||
}
|
||
|
||
// 导出:后端一个 xlsx 三个 sheet(按人/按工位/明细),执行统一导出时间硬规则
|
||
async function exportXlsx() {
|
||
const [from, to] = query.range || []
|
||
const params = {
|
||
operator: query.operator || '', stationNo: query.stationNo || '',
|
||
orderNo: query.orderNo.trim(),
|
||
startDate: from || '', endDate: to || ''
|
||
}
|
||
const blob = await request.get('/performance/export', { params, responseType: 'blob' })
|
||
const a = document.createElement('a')
|
||
a.href = URL.createObjectURL(blob)
|
||
a.download = '绩效报表.xlsx'
|
||
a.click()
|
||
URL.revokeObjectURL(a.href)
|
||
}
|
||
|
||
onMounted(async () => { await load(); stationNos.value = await loadStationNos() })
|
||
</script>
|
||
|
||
<style scoped>
|
||
.stat-card { text-align: center; }
|
||
.stat-label { color: #909399; font-size: 13px; margin-bottom: 6px; }
|
||
.stat-value { font-size: 26px; font-weight: 700; color: #303133; }
|
||
.stat-value.ok { color: #67c23a; }
|
||
.stat-value.ng { color: #f56c6c; }
|
||
</style>
|