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

190 lines
9.6 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-alert type="info" :closable="false" style="margin-bottom:12px"
title="数量不符处理:工位终端接料时发现「实收 ≠ 应发」后上报,差异单汇总到本页。仓管/班组长按差异方向处理——少发走「补发」(自动生成补料单并叫 AGV)、多发走「退库」(生成退库单,WMS 确认后库存加回)、少量差异可「调整关闭」(不补不退,必须填说明)。" />
<el-row :gutter="12" style="margin-bottom:12px">
<el-col :span="6">
<el-card shadow="never" class="stat"><div class="stat-num warn">{{ stats.pending }}</div><div class="stat-label">待处理</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat"><div class="stat-num ok">{{ stats.replenished }}</div><div class="stat-label">已补发</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat"><div class="stat-num info">{{ stats.returned }}</div><div class="stat-label">已退库</div></el-card>
</el-col>
<el-col :span="6">
<el-card shadow="never" class="stat"><div class="stat-num">{{ stats.closed }}</div><div class="stat-label">已关闭</div></el-card>
</el-col>
</el-row>
<el-form inline>
<el-form-item label="状态">
<el-select v-model="query.status" clearable placeholder="全部" style="width:130px">
<el-option label="待处理" value="PENDING" />
<el-option label="已补发" value="REPLENISHED" />
<el-option label="已退库" value="RETURNED" />
<el-option label="已关闭" value="CLOSED" />
</el-select>
</el-form-item>
<el-form-item label="工位"><el-input v-model="query.stationNo" clearable style="width:90px" placeholder="如 3" @keyup.enter="search" /></el-form-item>
<el-form-item label="工单号"><el-input v-model="query.orderNo" clearable style="width:150px" @keyup.enter="search" /></el-form-item>
<el-form-item label="图号"><el-input v-model="query.materialCode" clearable style="width:150px" placeholder="不区分大小写" @keyup.enter="search" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="search">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="list" border size="small" v-loading="loading">
<el-table-column prop="reportNo" label="差异单号" width="100" />
<el-table-column prop="stationNo" label="工位" width="70" align="center" />
<el-table-column prop="orderNo" label="工单号" min-width="130" show-overflow-tooltip />
<el-table-column prop="materialCode" label="图号" min-width="110" show-overflow-tooltip />
<el-table-column prop="materialName" label="物料名称" min-width="110" show-overflow-tooltip />
<el-table-column prop="sn" label="序列号" min-width="120" show-overflow-tooltip />
<el-table-column prop="planQty" label="应发" width="70" align="center" />
<el-table-column prop="actualQty" label="实收" width="70" align="center" />
<el-table-column label="差异" width="80" align="center">
<template #default="{row}">
<span :class="row.diffQty > 0 ? 'diff-short' : row.diffQty < 0 ? 'diff-over' : ''">
{{ row.diffQty > 0 ? '少 ' + row.diffQty : row.diffQty < 0 ? '多 ' + (-row.diffQty) : '-' }}
</span>
</template>
</el-table-column>
<el-table-column prop="cause" label="上报原因" min-width="130" show-overflow-tooltip />
<el-table-column label="上报时间" width="150">
<template #default="{row}">{{ fmtTime(row.createdAt) }}</template>
</el-table-column>
<el-table-column prop="operator" label="上报人" width="90" />
<el-table-column label="状态" width="90">
<template #default="{row}">
<el-tag :type="statusTag(row.status)" size="small">{{ statusText(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="处理方式" width="90">
<template #default="{row}">{{ handleText(row.handleType) }}</template>
</el-table-column>
<el-table-column prop="handleDocNo" label="处理单据" min-width="170" show-overflow-tooltip />
<el-table-column prop="handleNote" label="处理说明" min-width="150" show-overflow-tooltip />
<el-table-column prop="handledBy" label="处理人" width="90" />
<el-table-column label="处理时间" width="150">
<template #default="{row}">{{ row.handledAt ? fmtTime(row.handledAt) : '-' }}</template>
</el-table-column>
<el-table-column label="操作" width="210" fixed="right" align="center">
<template #default="{row}">
<template v-if="isPending(row.status) && can('produce.qty:handle')">
<el-button size="small" type="success" :disabled="row.diffQty <= 0" @click="doHandle(row, 'REPLENISH')">补发</el-button>
<el-button size="small" type="warning" :disabled="row.diffQty >= 0" @click="doHandle(row, 'RETURN')">退库</el-button>
<el-button size="small" @click="doHandle(row, 'ADJUST')">调整</el-button>
</template>
<span v-else class="muted">{{ isPending(row.status) ? '无处理权限' : '已处理' }}</span>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top:12px; justify-content:flex-end"
background layout="total, sizes, prev, pager, next"
:total="total" :current-page="query.page" :page-size="query.pageSize"
:page-sizes="[20, 50, 100]" @current-change="onPage" @size-change="onSize" />
</el-card>
<PageHelp :data="helpQtyReport" page="QtyReport" />
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import request from '../utils/request'
import { can } from '../utils/perm'
import { fmtTime } from '../utils/format'
import PageHelp from '../components/PageHelp.vue'
import { helpQtyReport } from '../help'
const query = reactive({ status: '', stationNo: '', orderNo: '', materialCode: '', page: 1, pageSize: 20 })
const list = ref([])
const total = ref(0)
const loading = ref(false)
const stats = reactive({ pending: 0, replenished: 0, returned: 0, closed: 0 })
const statusText = (s) => ({ PENDING: '待处理', PROCESSING: '处理中', REPLENISHED: '已补发', RETURNED: '已退库', ADJUSTED: '已关闭', CLOSED: '已关闭', RESOLVED: '已关闭' }[s] || s)
const statusTag = (s) => (s === 'PENDING' || s === 'PROCESSING' ? 'warning' : s === 'REPLENISHED' ? 'success' : s === 'RETURNED' ? 'primary' : 'info')
const handleText = (t) => ({ REPLENISH: '补发', RETURN: '退库', ADJUST: '调整关闭' }[t] || '-')
const isPending = (s) => s === 'PENDING' || s === 'PROCESSING'
async function load() {
loading.value = true
try {
const data = await request.get('/qty-reports', { params: query })
list.value = data?.list || []
total.value = data?.total || 0
} catch {
// 拦截器已提示
}
loading.value = false
}
async function loadStats() {
try {
const data = await request.get('/qty-reports/stats')
Object.assign(stats, data || {})
} catch {
// 统计失败不影响列表
}
}
function search() { query.page = 1; load() }
function reset() {
Object.assign(query, { status: '', stationNo: '', orderNo: '', materialCode: '', page: 1 })
load()
}
function onPage(p) { query.page = p; load() }
function onSize(s) { query.pageSize = s; query.page = 1; load() }
// 处理差异:补发(少发)/ 退库(多发)/ 调整关闭(不补不退,必须填说明)
async function doHandle(row, type) {
if (type === 'REPLENISH' && row.diffQty <= 0) return ElMessage.warning('本单为「多发」,不能补发;请选择退库或调整')
if (type === 'RETURN' && row.diffQty >= 0) return ElMessage.warning('本单为「少发」,不能退库;请选择补发或调整')
const title = type === 'REPLENISH' ? '补发' : type === 'RETURN' ? '退库' : '调整关闭'
const tip = type === 'REPLENISH'
? `将为「${row.materialCode}」生成补料单(数量 ${row.diffQty}),随后自动出库并叫 AGV 送到工位 ${row.stationNo}。`
: type === 'RETURN'
? `将为「${row.materialCode}」生成退库单(数量 ${-row.diffQty}),WMS 确认收货后库存加回。`
: '调整关闭:不补发也不退库,仅登记双方确认结果(必填说明)。'
let note = ''
try {
const { value } = await ElMessageBox.prompt(tip, title, {
inputPlaceholder: type === 'ADJUST' ? '必填:说明双方确认结果' : '选填:处理说明',
inputValidator: (v) => (type !== 'ADJUST' || (v && v.trim().length > 0)) || '调整关闭必须填写处理说明',
confirmButtonText: '确定处理',
type: 'warning'
})
note = (value || '').trim()
} catch {
return
}
try {
const data = await request.post('/qty-reports/handle', { id: row.id, handleType: type, note })
ElMessage.success(`处理完成:${statusText(data?.status)}${data?.handleDocNo ? '' + data.handleDocNo + '' : ''}`)
load()
loadStats()
} catch {
// 拦截器已提示
}
}
onMounted(() => {
load()
loadStats()
})
</script>
<style scoped>
.stat { text-align: center; }
.stat-num { font-size: 22px; font-weight: 600; }
.stat-num.warn { color: #E6A23C; }
.stat-num.ok { color: #67C23A; }
.stat-num.info { color: #409EFF; }
.stat-label { font-size: 12px; color: #909399; margin-top: 4px; }
.diff-short { color: #E6A23C; font-weight: 600; }
.diff-over { color: #F56C6C; font-weight: 600; }
.muted { color: #909399; font-size: 12px; }
</style>