成本模块
This commit is contained in:
568
klp-ui/src/views/wms/cost/detail/index.vue
Normal file
568
klp-ui/src/views/wms/cost/detail/index.vue
Normal file
@@ -0,0 +1,568 @@
|
||||
<template>
|
||||
<div class="cost-search-page">
|
||||
<div class="hero">
|
||||
<div class="hero-content">
|
||||
<h1>成本检索中心</h1>
|
||||
<p>基于 wms_material_coil 实时计算,输入入场钢卷号即可查看成本</p>
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model.trim="searchForm.enterCoilNo"
|
||||
placeholder="请输入入场钢卷号(支持前缀)"
|
||||
clearable
|
||||
class="search-input"
|
||||
@input="handleInputChange"
|
||||
@keyup.enter.native="handleSearchClick"
|
||||
/>
|
||||
<el-date-picker
|
||||
v-model="searchForm.calcDate"
|
||||
type="date"
|
||||
placeholder="可选:指定计算日期"
|
||||
value-format="yyyy-MM-dd"
|
||||
class="search-date"
|
||||
:picker-options="datePickerOptions"
|
||||
@change="handleCalcDateChange"
|
||||
/>
|
||||
<el-button
|
||||
type="primary"
|
||||
icon="el-icon-search"
|
||||
:loading="loading"
|
||||
@click="handleSearchClick"
|
||||
>
|
||||
检索成本
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="hero-meta">
|
||||
<span>未发货:计算到选择日期(默认今日)</span>
|
||||
<span>已发货:计算到发货前一天</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="result-wrapper">
|
||||
<div class="result-actions">
|
||||
<el-button type="text" icon="el-icon-setting" @click="goToStandardConfig">成本标准配置</el-button>
|
||||
<el-divider direction="vertical"></el-divider>
|
||||
<el-button type="text" icon="el-icon-tickets" @click="goToDetail">查看历史明细</el-button>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-if="!searchExecuted && !loading"
|
||||
description="请输入入场钢卷号后开始检索"
|
||||
/>
|
||||
|
||||
<div v-else>
|
||||
<el-card v-loading="loading" shadow="never" class="summary-card">
|
||||
<div class="summary-header">
|
||||
<div>
|
||||
<div class="summary-label">入场钢卷号</div>
|
||||
<div class="summary-value">{{ summary.enterCoilNo || '-' }}</div>
|
||||
</div>
|
||||
<el-tag type="info" effect="plain">
|
||||
统计日期:{{ summary.calcDate || '-' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="summary-grid">
|
||||
<div class="grid-item">
|
||||
<div class="grid-label">总子钢卷数</div>
|
||||
<div class="grid-value primary">{{ summary.totalCoils }}</div>
|
||||
<div class="grid-desc">
|
||||
<span>在库 {{ summary.unshippedCount }}</span>
|
||||
<span>已发货 {{ summary.shippedCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<div class="grid-label">总毛重</div>
|
||||
<div class="grid-value">{{ formatWeight(summary.totalGrossWeight) }} 吨</div>
|
||||
<div class="grid-desc">毛重优先,缺失时用净重</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<div class="grid-label">总净重</div>
|
||||
<div class="grid-value">{{ formatWeight(summary.totalNetWeight) }} 吨</div>
|
||||
<div class="grid-desc">实时换算为吨</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<div class="grid-label">累计成本</div>
|
||||
<div class="grid-value accent">{{ formatMoney(summary.totalCost) }} 元</div>
|
||||
<div class="grid-desc">所有子钢卷累计值</div>
|
||||
</div>
|
||||
<div class="grid-item">
|
||||
<div class="grid-label">平均在库天数</div>
|
||||
<div class="grid-value">{{ formatNumber(summary.avgStorageDays, 2) }} 天</div>
|
||||
<div class="grid-desc">含在库与已发货</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div v-if="cards.length" class="card-section">
|
||||
<div class="card-grid">
|
||||
<div class="coil-card" v-for="card in cards" :key="card.coilId">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<div class="coil-no">{{ card.currentCoilNo }}</div>
|
||||
<div class="coil-meta">入库:{{ formatDate(card.startDate) }}</div>
|
||||
</div>
|
||||
<el-tag
|
||||
size="mini"
|
||||
:type="card.isShipped ? 'info' : 'success'"
|
||||
effect="plain"
|
||||
>
|
||||
{{ card.isShipped ? '已发货' : '在库' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="metric-row">
|
||||
<span class="metric-label">毛重(吨)</span>
|
||||
<span class="metric-value">{{ formatWeight(card.grossWeightTon) }}</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span class="metric-label">净重(吨)</span>
|
||||
<span class="metric-value">{{ formatWeight(card.netWeightTon) }}</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span class="metric-label">计费基准</span>
|
||||
<span class="metric-value">
|
||||
<el-tag size="mini" :type="card.weightBasis === 'gross' ? 'success' : 'info'" effect="plain">
|
||||
{{ card.weightBasis === 'gross' ? '毛重' : '净重' }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span class="metric-label">在库天数</span>
|
||||
<span :class="['metric-value', getStorageDaysClass(card.storageDays)]">
|
||||
{{ card.storageDays || '-' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span class="metric-label">单位成本</span>
|
||||
<span class="metric-value">{{ formatMoney(card.unitCost) }} 元/吨/天</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span class="metric-label">累计成本</span>
|
||||
<span class="metric-value accent">{{ formatMoney(card.totalCost) }} 元</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span class="metric-label">计费截至</span>
|
||||
<span class="metric-value">{{ formatDate(card.endDate) }}</span>
|
||||
</div>
|
||||
<div class="metric-row">
|
||||
<span class="metric-label">所在库区</span>
|
||||
<span class="metric-value">{{ card.warehouseName || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pagination" v-if="pagination.total > pagination.pageSize">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:page-size="pagination.pageSize"
|
||||
:total="pagination.total"
|
||||
:current-page.sync="pagination.pageNum"
|
||||
@current-change="handlePageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-empty
|
||||
v-else-if="!loading"
|
||||
description="暂无匹配钢卷"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { searchMaterialCost } from '@/api/wms/cost'
|
||||
|
||||
export default {
|
||||
name: 'CostSearchDashboard',
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
searchExecuted: false,
|
||||
searchForm: {
|
||||
enterCoilNo: '',
|
||||
calcDate: null
|
||||
},
|
||||
pagination: {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
total: 0
|
||||
},
|
||||
summary: {
|
||||
enterCoilNo: '',
|
||||
calcDate: '',
|
||||
totalCoils: 0,
|
||||
shippedCount: 0,
|
||||
unshippedCount: 0,
|
||||
totalGrossWeight: 0,
|
||||
totalNetWeight: 0,
|
||||
totalCost: 0,
|
||||
avgStorageDays: 0
|
||||
},
|
||||
cards: [],
|
||||
debounceTimer: null,
|
||||
datePickerOptions: {
|
||||
disabledDate(time) {
|
||||
return time.getTime() > Date.now()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer)
|
||||
this.debounceTimer = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleInputChange() {
|
||||
this.pagination.pageNum = 1
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer)
|
||||
}
|
||||
this.debounceTimer = setTimeout(() => {
|
||||
this.fetchResults()
|
||||
}, 400)
|
||||
},
|
||||
handleCalcDateChange() {
|
||||
if (!this.searchForm.enterCoilNo) return
|
||||
this.pagination.pageNum = 1
|
||||
this.fetchResults()
|
||||
},
|
||||
handleSearchClick() {
|
||||
this.pagination.pageNum = 1
|
||||
this.fetchResults()
|
||||
},
|
||||
async fetchResults() {
|
||||
if (!this.searchForm.enterCoilNo) {
|
||||
this.resetResults(false)
|
||||
return
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
const params = {
|
||||
enterCoilNo: this.searchForm.enterCoilNo.trim(),
|
||||
calcDate: this.searchForm.calcDate,
|
||||
pageNum: this.pagination.pageNum,
|
||||
pageSize: this.pagination.pageSize
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await searchMaterialCost(params)
|
||||
this.searchExecuted = true
|
||||
if (res.code === 200 && res.data) {
|
||||
const data = res.data
|
||||
const summaryData = data.summary || {}
|
||||
this.summary = {
|
||||
enterCoilNo: summaryData.enterCoilNo || params.enterCoilNo,
|
||||
calcDate: summaryData.calcDate || params.calcDate || this.formatDate(new Date()),
|
||||
totalCoils: summaryData.totalCoils || 0,
|
||||
shippedCount: summaryData.shippedCount || 0,
|
||||
unshippedCount: summaryData.unshippedCount || 0,
|
||||
totalGrossWeight: summaryData.totalGrossWeight || 0,
|
||||
totalNetWeight: summaryData.totalNetWeight || 0,
|
||||
totalCost: summaryData.totalCost || 0,
|
||||
avgStorageDays: summaryData.avgStorageDays || 0
|
||||
}
|
||||
this.cards = (data.records || []).map(item => ({
|
||||
...item,
|
||||
isShipped: item.isShipped === 1 || item.isShipped === true
|
||||
}))
|
||||
this.pagination.total = data.total || 0
|
||||
this.pagination.pageNum = data.pageNum || params.pageNum
|
||||
} else {
|
||||
this.resetResults(true)
|
||||
this.$message.warning(res.msg || '未查询到成本数据')
|
||||
}
|
||||
} catch (error) {
|
||||
this.$message.error('检索失败,请稍后再试')
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handlePageChange(page) {
|
||||
this.pagination.pageNum = page
|
||||
this.fetchResults()
|
||||
},
|
||||
resetResults(keepExecuted) {
|
||||
if (!keepExecuted) {
|
||||
this.searchExecuted = false
|
||||
}
|
||||
this.cards = []
|
||||
this.pagination.total = 0
|
||||
this.summary = {
|
||||
enterCoilNo: this.searchForm.enterCoilNo || '',
|
||||
calcDate: this.searchForm.calcDate || '',
|
||||
totalCoils: 0,
|
||||
shippedCount: 0,
|
||||
unshippedCount: 0,
|
||||
totalGrossWeight: 0,
|
||||
totalNetWeight: 0,
|
||||
totalCost: 0,
|
||||
avgStorageDays: 0
|
||||
}
|
||||
},
|
||||
goToStandardConfig() {
|
||||
this.$router.push('/wms/cost/standard')
|
||||
},
|
||||
goToDetail() {
|
||||
this.$router.push('/wms/cost/detail')
|
||||
},
|
||||
formatMoney(value) {
|
||||
if (!value) return '0.00'
|
||||
return Number(value).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
})
|
||||
},
|
||||
formatWeight(value) {
|
||||
if (!value) return '0.000'
|
||||
return Number(value).toFixed(3)
|
||||
},
|
||||
formatNumber(value, fraction = 2) {
|
||||
if (!value) return Number(0).toFixed(fraction)
|
||||
return Number(value).toFixed(fraction)
|
||||
},
|
||||
formatDate(value) {
|
||||
if (!value) return '-'
|
||||
if (value instanceof Date) {
|
||||
const yyyy = value.getFullYear()
|
||||
const mm = `${value.getMonth() + 1}`.padStart(2, '0')
|
||||
const dd = `${value.getDate()}`.padStart(2, '0')
|
||||
return `${yyyy}-${mm}-${dd}`
|
||||
}
|
||||
return value
|
||||
},
|
||||
getStorageDaysClass(days) {
|
||||
if (!days) return ''
|
||||
if (days >= 60) return 'storage-days-high'
|
||||
if (days >= 30) return 'storage-days-medium'
|
||||
return 'storage-days-normal'
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cost-search-page {
|
||||
min-height: 100%;
|
||||
background: #eef1f4;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
background: linear-gradient(120deg, #f7f8fa 0%, #e2e5ec 100%);
|
||||
border-bottom: 1px solid #d5d9e0;
|
||||
padding: 48px 20px 36px;
|
||||
color: #2f3135;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
|
||||
h1 {
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 24px;
|
||||
color: #5f646e;
|
||||
}
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.search-input {
|
||||
width: 320px;
|
||||
}
|
||||
|
||||
.search-date {
|
||||
width: 220px;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-meta {
|
||||
margin-top: 14px;
|
||||
font-size: 13px;
|
||||
color: #757a85;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.result-wrapper {
|
||||
max-width: 1100px;
|
||||
margin: -25px auto 0;
|
||||
padding: 0 20px 40px;
|
||||
}
|
||||
|
||||
.result-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
::v-deep .el-button--text {
|
||||
color: #5f6470;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border: 1px solid #d7dbe2;
|
||||
border-radius: 10px;
|
||||
background: #fefefe;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.summary-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.summary-label {
|
||||
font-size: 13px;
|
||||
color: #80848f;
|
||||
}
|
||||
|
||||
.summary-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #2f3135;
|
||||
}
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 14px;
|
||||
|
||||
.grid-item {
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e5ea;
|
||||
background: linear-gradient(180deg, #f9fafc 0%, #f0f2f5 100%);
|
||||
|
||||
.grid-label {
|
||||
font-size: 13px;
|
||||
color: #7b808a;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.grid-value {
|
||||
font-size: 22px;
|
||||
font-weight: 600;
|
||||
color: #2f3135;
|
||||
|
||||
&.primary {
|
||||
color: #435d7a;
|
||||
}
|
||||
|
||||
&.accent {
|
||||
color: #aa7728;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-desc {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #9a9fac;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card-section {
|
||||
border: 1px solid #d7dbe2;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
background: #fdfdfd;
|
||||
}
|
||||
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.coil-card {
|
||||
border: 1px solid #d0d4dc;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 6px 18px rgba(32, 41, 58, 0.08);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.coil-no {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #2e3240;
|
||||
}
|
||||
|
||||
.coil-meta {
|
||||
font-size: 12px;
|
||||
color: #8a8f99;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
color: #5a5f6b;
|
||||
|
||||
.metric-value {
|
||||
font-weight: 600;
|
||||
color: #30343c;
|
||||
}
|
||||
|
||||
.metric-value.accent {
|
||||
color: #b4792b;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.storage-days-normal {
|
||||
color: #4c805c;
|
||||
}
|
||||
|
||||
.storage-days-medium {
|
||||
color: #b78b38;
|
||||
}
|
||||
|
||||
.storage-days-high {
|
||||
color: #b3473f;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user