opt: 任务托盘码修正、出入库分配的时候不在操作Qty字段

This commit is contained in:
2025-08-11 09:49:23 +08:00
parent 1387b31ba5
commit 7328d5cc7a
4 changed files with 603 additions and 3 deletions

View File

@@ -0,0 +1,599 @@
<template>
<div class="screen-container">
<div class="screen-header">
<div class="title">库存看板</div>
<div class="actions">
<el-switch
v-model="useMock"
active-text="使用模拟数据"
inactive-text="使用接口"
:active-color="primaryColor"
:inactive-color="'#1f2d3d'"
@change="fetchAndRender"
/>
<el-button size="mini" type="primary" @click="fetchAndRender">刷新</el-button>
</div>
</div>
<el-row :gutter="16" class="kpi-row">
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<div class="kpi-card">
<div class="kpi-label">库存总数</div>
<div class="kpi-value">{{ formatNumber(metrics.totalQty) }}</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<div class="kpi-card">
<div class="kpi-label">可用数</div>
<div class="kpi-value kpi-green">{{ formatNumber(metrics.availableQty) }}</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<div class="kpi-card">
<div class="kpi-label">冻结数</div>
<div class="kpi-value kpi-orange">{{ formatNumber(metrics.frozenQty) }}</div>
</div>
</el-col>
<el-col :xs="12" :sm="6" :md="6" :lg="6">
<div class="kpi-card">
<div class="kpi-label">物料/载具种类</div>
<div class="kpi-value">{{ metrics.materialKinds }} / {{ metrics.vehicleKinds }}</div>
</div>
</el-col>
</el-row>
<el-row :gutter="16" class="chart-row">
<el-col :xs="24" :sm="12" :md="12" :lg="12">
<div class="panel">
<div class="panel-title">
库区可用数 Top
</div>
<div ref="sectBar" class="chart" />
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="12" :lg="12">
<div class="panel">
<div class="panel-title">仓库分布按总数</div>
<div ref="storPie" class="chart" />
</div>
</el-col>
</el-row>
<el-row :gutter="16" class="list-row">
<el-col :xs="24" :sm="24" :md="24" :lg="24">
<div class="panel">
<div class="panel-title">最新入库记录</div>
<div class="list">
<el-table
:data="records"
size="mini"
stripe
:header-cell-style="tableHeaderStyle"
:cell-style="tableCellStyle"
:row-style="tableRowStyle"
height="360"
class="tech-table"
>
<el-table-column prop="create_time" label="入库时间" min-width="140" />
<el-table-column prop="stor_name" label="仓库" min-width="110" />
<el-table-column prop="sect_name" label="库区" min-width="110" />
<el-table-column prop="struct_code" label="仓位" min-width="100" />
<el-table-column prop="material_name" label="物料" min-width="140" />
<el-table-column prop="pcsn" label="批次号" min-width="100" />
<el-table-column prop="qty" label="总数" min-width="80" :formatter="n3" />
<el-table-column label="可用" min-width="80">
<template slot-scope="scope">{{ n3(null, null, scope.row.qty - scope.row.frozen_qty) }}</template>
</el-table-column>
<el-table-column prop="frozen_qty" label="冻结" min-width="80" :formatter="n3" />
<el-table-column prop="qty_unit_name" label="单位" min-width="80" />
<el-table-column prop="storagevehicle_code" label="载具号" min-width="100" />
</el-table>
</div>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import echarts from 'echarts'
require('echarts/theme/macarons')
import { initData } from '@/api/data'
export default {
name: 'report1',
data() {
return {
loading: false,
useMock: true,
primaryColor: '#00e4ff',
records: [],
charts: {
sectBar: null,
storPie: null
},
metrics: {
totalQty: 0,
availableQty: 0,
frozenQty: 0,
materialKinds: 0,
vehicleKinds: 0
},
refreshTimer: null
}
},
mounted() {
this.fetchAndRender()
this.$nextTick(() => {
window.addEventListener('resize', this.handleResize)
})
// 可选自动刷新5 分钟)
// this.refreshTimer = setInterval(this.fetchAndRender, 5 * 60 * 1000)
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize)
if (this.refreshTimer) clearInterval(this.refreshTimer)
this.disposeCharts()
},
methods: {
async fetchAndRender() {
this.loading = true
try {
let content = []
if (this.useMock) {
content = this.getMock().content
} else {
const params = { page: 0, size: 10, sort: 'stockrecord_id,desc' }
const res = await initData('api/structivt', params)
// 接口兼容:有的返回 {content, totalElements},有的包一层 {data}
content = (res && (res.content || (res.data && res.data.content))) || []
if (!Array.isArray(content) || content.length === 0) {
content = this.getMock().content
}
}
this.records = content
this.computeMetrics()
this.$nextTick(() => {
this.renderCharts()
})
} catch (e) {
// 回退模拟
this.records = this.getMock().content
this.computeMetrics()
this.$nextTick(() => {
this.renderCharts()
})
} finally {
this.loading = false
}
},
computeMetrics() {
const totalQty = this.records.reduce((s, r) => s + (Number(r.qty) || 0), 0)
const frozenQty = this.records.reduce((s, r) => s + (Number(r.frozen_qty) || 0), 0)
const availableQty = this.records.reduce((s, r) => s + ((Number(r.qty) || 0) - (Number(r.frozen_qty) || 0)), 0)
const materialKinds = new Set(this.records.map(r => r.material_code)).size
const vehicleKinds = new Set(this.records.map(r => r.storagevehicle_code)).size
this.metrics = { totalQty, availableQty, frozenQty, materialKinds, vehicleKinds }
},
renderCharts() {
this.renderSectBar()
this.renderStorPie()
},
renderSectBar() {
const el = this.$refs.sectBar
if (!el) return
if (!this.charts.sectBar) this.charts.sectBar = echarts.init(el, 'macarons')
// 聚合库区可用数
const map = {}
this.records.forEach(r => {
const key = r.sect_name || '未分配'
const val = (Number(r.qty) || 0) - (Number(r.frozen_qty) || 0)
map[key] = (map[key] || 0) + val
})
const arr = Object.keys(map).map(k => ({ name: k, value: map[k] }))
arr.sort((a, b) => b.value - a.value)
const names = arr.map(i => i.name)
const values = arr.map(i => i.value)
const option = {
backgroundColor: 'transparent',
grid: { top: 30, right: 20, bottom: 30, left: 60 },
tooltip: { trigger: 'axis' },
xAxis: {
type: 'value',
axisLabel: { color: '#9ecbff' },
splitLine: { lineStyle: { color: 'rgba(0, 228, 255, .15)' } }
},
yAxis: {
type: 'category',
data: names,
axisLabel: { color: '#cfe8ff' },
axisLine: { lineStyle: { color: 'rgba(0, 228, 255, .3)' } }
},
series: [
{
name: '可用数',
type: 'bar',
data: values,
barWidth: 14,
itemStyle: {
color: new echarts.graphic.LinearGradient(1, 0, 0, 0, [
{ offset: 0, color: '#00e4ff' },
{ offset: 1, color: '#007bff' }
]),
shadowBlur: 8,
shadowColor: 'rgba(0, 228, 255, .5)'
},
label: { show: true, position: 'right', color: '#8fdcff' }
}
]
}
this.charts.sectBar.setOption(option, true)
this.charts.sectBar.resize()
},
renderStorPie() {
const el = this.$refs.storPie
if (!el) return
if (!this.charts.storPie) this.charts.storPie = echarts.init(el, 'macarons')
// 聚合仓库总数
const map = {}
this.records.forEach(r => {
const key = r.stor_name || '未分配'
const val = Number(r.qty) || 0
map[key] = (map[key] || 0) + val
})
const data = Object.keys(map).map(k => ({ name: k, value: map[k] }))
const option = {
backgroundColor: 'transparent',
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
legend: {
orient: 'vertical',
right: 10,
top: 'center',
textStyle: { color: '#cfe8ff' }
},
series: [
{
name: '仓库分布',
type: 'pie',
radius: ['36%', '60%'],
center: ['38%', '50%'],
roseType: false,
data,
label: { color: '#e2f6ff' },
labelLine: { lineStyle: { color: 'rgba(0, 228, 255, .35)' } },
itemStyle: {
color: (params) => {
const palette = ['#00e4ff', '#3dd9a3', '#ffcc00', '#ff7b7b', '#8a7dff', '#00bcd4']
return palette[params.dataIndex % palette.length]
},
shadowBlur: 10,
shadowColor: 'rgba(0, 228, 255, .45)'
}
}
]
}
this.charts.storPie.setOption(option, true)
this.charts.storPie.resize()
},
disposeCharts() {
Object.keys(this.charts).forEach(k => {
if (this.charts[k]) {
this.charts[k].dispose()
this.charts[k] = null
}
})
},
handleResize() {
Object.keys(this.charts).forEach(k => {
if (this.charts[k]) this.charts[k].resize()
})
},
formatNumber(val) {
if (val == null) return '-'
const n = Number(val)
if (Number.isNaN(n)) return val
return n.toLocaleString()
},
n3(row, column, cellValue) {
const n = Number(cellValue)
if (Number.isNaN(n)) return cellValue
return n.toLocaleString(undefined, { minimumFractionDigits: 0, maximumFractionDigits: 3 })
},
tableHeaderStyle() {
return {
background: 'linear-gradient(135deg, rgba(0,228,255,0.25), rgba(0,123,255,0.15), rgba(0,0,0,0.4))',
color: '#e6f5ff',
borderColor: 'rgba(0,228,255,.4)',
fontWeight: '600',
textShadow: '0 0 8px rgba(0,228,255,0.5)'
}
},
tableCellStyle() {
return {
background: 'rgba(0,0,0,0.2)',
color: '#e6f5ff',
borderColor: 'rgba(0,228,255,.1)'
}
},
tableRowStyle({ row, rowIndex }) {
return {
background: rowIndex % 2 === 0 ? 'rgba(0,0,0,0.15)' : 'rgba(0,228,255,0.05)',
borderBottom: '1px solid rgba(0,228,255,0.08)'
}
},
getMock() {
return {
totalElements: 10,
content: [
{
create_time: '2025-08-07 15:49:12',
remark: null,
storagevehicle_code: 'MTP0066',
stor_name: '原料仓库',
qty: 30,
qty_unit_name: '套',
pcsn: '1231231',
struct_name: 'T6',
sect_name: '加钠区',
frozen_qty: 0,
struct_code: 'T6',
material_name: '钨粉233',
storagevehicleext_id: '1953362778513346560',
material_code: 'OPEN123'
},
{
create_time: '2025-08-07 15:47:07',
remark: null,
storagevehicle_code: 'MTP0065',
stor_name: '原料仓库',
qty: 30,
qty_unit_name: 'Pcs',
pcsn: '111',
struct_name: 'T5',
sect_name: '加钠区',
frozen_qty: 0,
struct_code: 'T5',
material_name: '钨粉233',
storagevehicleext_id: '1953362254179209216',
material_code: 'OPEN123'
},
{
create_time: '2025-08-07 09:11:59',
remark: null,
storagevehicle_code: 'MTP0064',
stor_name: '原料仓库',
qty: 1,
qty_unit_name: '套',
pcsn: 'pci',
struct_name: 'T98',
sect_name: '加钠区',
frozen_qty: 0,
struct_code: 'T98',
material_name: '钨粉233',
storagevehicleext_id: '1953262815569645568',
material_code: 'OPEN123'
},
{
create_time: '2025-07-31 14:06:37',
remark: null,
storagevehicle_code: 'LT0012',
stor_name: '料桶缓存库',
qty: 0,
qty_unit_name: '分米',
pcsn: 'S',
struct_name: 'T95',
sect_name: '超细区',
frozen_qty: 2,
struct_code: 'T95',
material_name: '电源线',
storagevehicleext_id: '1950800244728008704',
material_code: '105011008001001'
},
{
create_time: '2025-07-31 14:06:37',
remark: null,
storagevehicle_code: 'LT0012',
stor_name: '料桶缓存库',
qty: 0,
qty_unit_name: '分米',
pcsn: 'S',
struct_name: 'A12',
sect_name: '超细区',
frozen_qty: 2,
struct_code: 'A12',
material_name: '电源线',
storagevehicleext_id: '1950800244728008704',
material_code: '105011008001001'
},
{
create_time: '2025-07-24 17:27:52',
remark: null,
storagevehicle_code: 'MTP0051',
stor_name: '原料仓库',
qty: 1,
qty_unit_name: '个',
pcsn: '1231211',
struct_name: 'T99',
sect_name: '加钠区',
frozen_qty: 0,
struct_code: 'T99',
material_name: '空托盘',
storagevehicleext_id: '1948314178171310080',
material_code: 'KT001'
},
{
create_time: '2025-07-24 17:27:52',
remark: null,
storagevehicle_code: 'MTP0051',
stor_name: '原料仓库',
qty: 1,
qty_unit_name: '个',
pcsn: '1231211',
struct_name: 'T2',
sect_name: '加钠区',
frozen_qty: 0,
struct_code: 'T2',
material_name: '空托盘',
storagevehicleext_id: '1948314178171310080',
material_code: 'KT001'
},
{
create_time: '2025-07-24 14:15:56',
remark: null,
storagevehicle_code: 'MTP0047',
stor_name: '原料仓库',
qty: 0,
qty_unit_name: '个',
pcsn: '123',
struct_name: 'T3',
sect_name: '加钠区',
frozen_qty: 1,
struct_code: 'T3',
material_name: '漏保电源线',
storagevehicleext_id: '1948265873261334528',
material_code: '105030400000055'
},
{
create_time: '2025-07-14 09:37:46',
remark: null,
storagevehicle_code: 'MTP0049',
stor_name: '高位库',
qty: 50,
qty_unit_name: '斤',
pcsn: '123XXX',
struct_name: 'A1-3-1',
sect_name: '高位库区01',
frozen_qty: 101,
struct_code: 'A1-3-1',
material_name: '电源线',
storagevehicleext_id: '1944571992468492288',
material_code: '105011008001001'
},
{
create_time: '2025-07-14 08:56:06',
remark: null,
storagevehicle_code: 'MTP0048',
stor_name: '原料仓库',
qty: 0,
qty_unit_name: 'Pcs',
pcsn: '123',
struct_name: 'T7',
sect_name: '加钠区',
frozen_qty: 0,
struct_code: 'T7',
material_name: '漏保电源线',
storagevehicleext_id: '1944561507484438528',
material_code: '105030400000055'
}
],
code: '200',
msg: '查询成功',
data: null
}
}
}
}
</script>
<style scoped>
.screen-container {
padding: 16px;
background: radial-gradient(1200px 600px at 80% -10%, rgba(0, 228, 255, 0.15), transparent 60%),
radial-gradient(800px 400px at -10% 120%, rgba(61, 217, 163, 0.15), transparent 60%),
#0b1020;
min-height: calc(100vh - 100px);
}
.screen-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.title {
font-size: 22px;
font-weight: 600;
letter-spacing: 2px;
color: #e6f5ff;
text-shadow: 0 0 12px rgba(0, 228, 255, 0.65);
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
.kpi-row .kpi-card {
background: linear-gradient(135deg, rgba(0, 228, 255, 0.12), rgba(0, 0, 0, 0.25));
border: 1px solid rgba(0, 228, 255, 0.22);
border-radius: 10px;
padding: 14px 16px;
box-shadow: 0 10px 24px rgba(0, 0, 0, 0.35), inset 0 0 18px rgba(0, 228, 255, 0.12);
}
.kpi-label {
color: #9ecbff;
font-size: 12px;
}
.kpi-value {
margin-top: 6px;
color: #e6f5ff;
font-size: 22px;
font-weight: 700;
}
.kpi-green { color: #3dd9a3; }
.kpi-orange { color: #ffcc66; }
.chart-row .panel, .list-row .panel {
background: linear-gradient(180deg, rgba(0, 0, 0, 0.25), rgba(0, 0, 0, 0.15));
border: 1px solid rgba(0, 228, 255, 0.2);
border-radius: 12px;
padding: 10px 10px 6px 10px;
}
.panel-title {
height: 32px;
line-height: 32px;
padding-left: 10px;
color: #cfe8ff;
font-weight: 600;
border-left: 3px solid #00e4ff;
margin-bottom: 6px;
}
.chart { height: 360px; width: 100%; }
.list { height: 400px; }
/* 表格科技感样式 */
.tech-table {
background: transparent !important;
}
.tech-table .el-table__body-wrapper {
background: transparent !important;
}
.tech-table .el-table__body tr:hover > td {
background: rgba(0, 228, 255, 0.1) !important;
}
.tech-table .el-table__body tr.current-row > td {
background: rgba(0, 228, 255, 0.15) !important;
}
.tech-table .el-table__fixed-header-wrapper {
background: transparent !important;
}
.tech-table .el-table__fixed-body-wrapper {
background: transparent !important;
}
/* 表头科技感样式 */
.tech-table .el-table__header-wrapper {
background: transparent !important;
}
.tech-table .el-table__header th {
background: linear-gradient(135deg, rgba(0,228,255,0.25), rgba(0,123,255,0.15), rgba(0,0,0,0.4)) !important;
color: #e6f5ff !important;
border-bottom: 1px solid rgba(0,228,255,0.4) !important;
font-weight: 600 !important;
text-shadow: 0 0 8px rgba(0,228,255,0.5) !important;
}
.tech-table .el-table__header th:hover {
background: linear-gradient(135deg, rgba(0,228,255,0.35), rgba(0,123,255,0.25), rgba(0,0,0,0.5)) !important;
}
</style>