Files
bj_power/bj_power_mes/frontend/src/pages/Performance.vue
T
SunYF 05b0b8a909 feat: 完成客户第12条需求+多系统改造
1. 新增预警表添加工单号字段,支持按工单号检索预警
2. 添加工单过滤在制品/绩效报表/预警中心查询
3. 补全WMS台账字段口径与权限配置
4. 修复备料单与数量不符处理流程
5. 新增配送进度/库位联动/自动出库功能
6. 修复AGV配送状态更新逻辑
7. 优化退料与库存管理细节
2026-09-19 17:04:58 +08:00

260 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>