chore: 批量完成项目多模块迭代优化
1. 废弃半成品库存表并统一库存主表 2. 修复列表排序与搜索大小写不敏感问题 3. 新增用户最近登录时间、工单BOM名称字段 4. 完善角色管理、工位选择器功能 5. 增加拧紧数据补录、成品自动回流WMS能力 6. 统一导出时间范围校验规则 7. 丰富物料/备料单筛选条件 8. 调整菜单与权限命名适配产品型号业务
This commit is contained in:
@@ -4,6 +4,11 @@
|
||||
<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 12" :key="i" :label="`工位${i}`" :value="String(i)" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="日期范围">
|
||||
<el-date-picker
|
||||
v-model="query.range"
|
||||
@@ -18,11 +23,11 @@
|
||||
<el-button type="primary" @click="load">查询</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="success" @click="exportCsv">导出 CSV</el-button>
|
||||
<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">
|
||||
@@ -53,7 +58,7 @@
|
||||
<el-tabs v-model="activeTab">
|
||||
<!-- 按人 -->
|
||||
<el-tab-pane label="按人" name="byOperator">
|
||||
<el-table :data="rowsByOperator" border size="small">
|
||||
<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" />
|
||||
@@ -66,13 +71,15 @@
|
||||
</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="rowsByStation" border size="small">
|
||||
<el-table-column prop="station" label="工位" width="90" align="center" />
|
||||
<el-table-column prop="stationName" label="工位名" min-width="140" show-overflow-tooltip />
|
||||
<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="NG数" width="110" align="right" />
|
||||
@@ -84,15 +91,18 @@
|
||||
</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="rows" border size="small">
|
||||
<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="processCode" label="工位" width="80" align="center" />
|
||||
<el-table-column prop="processName" label="工位名" min-width="130" show-overflow-tooltip />
|
||||
<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="NG数" width="90" align="right" />
|
||||
@@ -104,6 +114,9 @@
|
||||
</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>
|
||||
@@ -117,13 +130,19 @@ import PageHelp from '../components/PageHelp.vue'
|
||||
import { helpPerformance } from '../help'
|
||||
import request from '../utils/request'
|
||||
|
||||
// 后端三视图各自真分页(默认20):byOperator/byStation/detail 各带 {total, list}
|
||||
const activeTab = ref('byOperator')
|
||||
const query = reactive({ operator: '', range: null })
|
||||
const rows = ref([])
|
||||
const pageSize = ref(20)
|
||||
const query = reactive({ operator: '', stationNo: '', 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
|
||||
rows.value.forEach((r) => {
|
||||
views.byOperator.list.forEach((r) => {
|
||||
doneCount += Number(r.doneCount) || 0
|
||||
okCount += Number(r.okCount) || 0
|
||||
ngCount += Number(r.ngCount) || 0
|
||||
@@ -134,34 +153,6 @@ const summary = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 按人聚合:同一操作人的完成/合格/NG 累加
|
||||
const rowsByOperator = computed(() => {
|
||||
const m = new Map()
|
||||
for (const r of rows.value) {
|
||||
const k = r.operator
|
||||
if (!m.has(k)) m.set(k, { operator: k, doneCount: 0, okCount: 0, ngCount: 0 })
|
||||
const o = m.get(k)
|
||||
o.doneCount += Number(r.doneCount) || 0
|
||||
o.okCount += Number(r.okCount) || 0
|
||||
o.ngCount += Number(r.ngCount) || 0
|
||||
}
|
||||
return [...m.values()]
|
||||
})
|
||||
|
||||
// 按工位聚合:同一工位(工序编号)的完成/合格/NG 累加
|
||||
const rowsByStation = computed(() => {
|
||||
const m = new Map()
|
||||
for (const r of rows.value) {
|
||||
const k = r.processCode
|
||||
if (!m.has(k)) m.set(k, { station: r.processCode, stationName: r.processName, doneCount: 0, okCount: 0, ngCount: 0 })
|
||||
const o = m.get(k)
|
||||
o.doneCount += Number(r.doneCount) || 0
|
||||
o.okCount += Number(r.okCount) || 0
|
||||
o.ngCount += Number(r.ngCount) || 0
|
||||
}
|
||||
return [...m.values()]
|
||||
})
|
||||
|
||||
function rateOf(row) {
|
||||
const done = Number(row.doneCount) || 0
|
||||
const ok = Number(row.okCount) || 0
|
||||
@@ -172,40 +163,45 @@ function rateText(row) {
|
||||
return r < 0 ? '-' : r.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 || '',
|
||||
from: from || '',
|
||||
to: to || ''
|
||||
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 })) || {}
|
||||
rows.value = data.rows || []
|
||||
for (const k of ['byOperator', 'byStation', 'detail']) {
|
||||
const v = data[k] || {}
|
||||
views[k].total = v.total || 0
|
||||
views[k].list = v.list || []
|
||||
}
|
||||
}
|
||||
|
||||
// 导出 CSV(块6):按当前激活的 tab 视图生成,含 BOM 以便 Excel 打开中文不乱码
|
||||
function exportCsv() {
|
||||
let header, lines
|
||||
if (activeTab.value === 'byOperator') {
|
||||
header = ['操作人', '完成数', '合格数', 'NG数', '合格率']
|
||||
lines = rowsByOperator.value.map((r) => [r.operator, r.doneCount, r.okCount, r.ngCount, rateText(r)])
|
||||
} else if (activeTab.value === 'byStation') {
|
||||
header = ['工位', '工位名', '完成数', '合格数', 'NG数', '合格率']
|
||||
lines = rowsByStation.value.map((r) => [r.station, r.stationName, r.doneCount, r.okCount, r.ngCount, rateText(r)])
|
||||
} else {
|
||||
header = ['操作人', '日期', '工位', '工位名', '完成数', '合格数', 'NG数', '合格率']
|
||||
lines = rows.value.map((r) => {
|
||||
const rate = Number(r.doneCount) ? ((Number(r.okCount) / Number(r.doneCount)) * 100).toFixed(1) + '%' : '-'
|
||||
return [r.operator, r.date, r.processCode, r.processName, r.doneCount, r.okCount, r.ngCount, rate]
|
||||
})
|
||||
// 导出:后端一个 xlsx 三个 sheet(按人/按工位/明细),执行统一导出时间硬规则
|
||||
async function exportXlsx() {
|
||||
const [from, to] = query.range || []
|
||||
const params = {
|
||||
operator: query.operator || '', stationNo: query.stationNo || '',
|
||||
startDate: from || '', endDate: to || ''
|
||||
}
|
||||
if (!lines.length) return ElMessage.warning('当前无数据可导出')
|
||||
const esc = (v) => `"${String(v ?? '').replace(/"/g, '""')}"`
|
||||
const csv = '\ufeff' + [header, ...lines].map((row) => row.map(esc).join(',')).join('\r\n')
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' })
|
||||
const blob = await request.get('/performance/export', { params, responseType: 'blob' })
|
||||
const a = document.createElement('a')
|
||||
a.href = URL.createObjectURL(blob)
|
||||
a.download = `绩效报表_${activeTab.value}_${new Date().toISOString().slice(0, 10)}.csv`
|
||||
a.download = '绩效报表.xlsx'
|
||||
a.click()
|
||||
URL.revokeObjectURL(a.href)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user