547 lines
14 KiB
Vue
547 lines
14 KiB
Vue
<template>
|
|
<div class="cost-screen">
|
|
<!-- 顶部标题 -->
|
|
<header class="screen-header">
|
|
<h1 class="title">成本分析大屏</h1>
|
|
<span class="time">{{ currentTime }}</span>
|
|
</header>
|
|
|
|
<!-- 主体区域 -->
|
|
<main class="screen-body">
|
|
<!-- 成本概览卡片 -->
|
|
<div class="card-grid">
|
|
<div class="data-card" v-for="card in costOverview" :key="card.title">
|
|
<div class="card-title">{{ card.title }}</div>
|
|
<div class="card-value" :style="{ color: card.color }">{{ card.value }}</div>
|
|
<div class="card-unit">{{ card.unit }}</div>
|
|
<div class="card-trend" :class="card.trend > 0 ? 'up' : 'down'">
|
|
{{ card.trend > 0 ? '↑' : '↓' }} {{ Math.abs(card.trend) }}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表区域 -->
|
|
<div class="chart-area">
|
|
<div class="chart-box">
|
|
<div class="box-title">成本构成分析</div>
|
|
<div ref="pieChartRef" class="chart"></div>
|
|
</div>
|
|
<div class="chart-box">
|
|
<div class="box-title">月度成本趋势</div>
|
|
<div ref="lineChartRef" class="chart"></div>
|
|
</div>
|
|
<div class="chart-box">
|
|
<div class="box-title">成本类型分布</div>
|
|
<div ref="barChartRef" class="chart"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 底部区域 -->
|
|
<div class="bottom-area">
|
|
<div class="bottom-box">
|
|
<div class="box-title">成本明细</div>
|
|
<el-table :data="costDetails" border size="small" max-height="250">
|
|
<el-table-column prop="item" label="成本项目" />
|
|
<el-table-column prop="budget" label="预算" align="right" />
|
|
<el-table-column prop="actual" label="实际" align="right" />
|
|
<el-table-column prop="diff" label="差异" align="right">
|
|
<template #default="scope">
|
|
<span :style="{ color: scope.row.diff < 0 ? '#67c23a' : '#f56c6c' }">
|
|
{{ scope.row.diff > 0 ? '+' : '' }}{{ scope.row.diff }}%
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</div>
|
|
<div class="bottom-box">
|
|
<div class="box-title">部门成本排名</div>
|
|
<div class="ranking-list">
|
|
<div class="ranking-item" v-for="(item, index) in deptRanking" :key="item.name">
|
|
<span class="rank" :class="'rank-' + (index + 1)">{{ index + 1 }}</span>
|
|
<span class="name">{{ item.name }}</span>
|
|
<div class="bar-wrapper">
|
|
<div class="bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
|
</div>
|
|
<span class="value">{{ item.value }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bottom-box">
|
|
<div class="box-title">成本预警</div>
|
|
<div class="alarm-list">
|
|
<div class="alarm-item" v-for="alarm in alarms" :key="alarm.time" :class="alarm.level">
|
|
<span class="alarm-icon">{{ alarm.icon }}</span>
|
|
<div class="alarm-content">
|
|
<div class="alarm-title">{{ alarm.title }}</div>
|
|
<div class="alarm-time">{{ alarm.time }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, onMounted, onUnmounted } from 'vue'
|
|
import * as echarts from 'echarts'
|
|
|
|
const currentTime = ref('')
|
|
const pieChartRef = ref(null)
|
|
const lineChartRef = ref(null)
|
|
const barChartRef = ref(null)
|
|
let pieChart = null
|
|
let lineChart = null
|
|
let barChart = null
|
|
let timeInterval = null
|
|
|
|
const costOverview = ref([
|
|
{ title: '本月总成本', value: '1,568,000', unit: '元', color: '#f56c6c', trend: 3.2 },
|
|
{ title: '单位成本', value: '2,850', unit: '元/吨', color: '#e6a23c', trend: -1.5 },
|
|
{ title: '预算执行率', value: '96.8', unit: '%', color: '#67c23a', trend: 0.8 },
|
|
{ title: '成本环比', value: '-2.3', unit: '%', color: '#409eff', trend: -2.3 }
|
|
])
|
|
|
|
const costDetails = ref([
|
|
{ item: '原材料成本', budget: '1,000,000', actual: '985,000', diff: -1.5 },
|
|
{ item: '人工成本', budget: '250,000', actual: '234,000', diff: -6.4 },
|
|
{ item: '能源成本', budget: '180,000', actual: '189,000', diff: 5.0 },
|
|
{ item: '设备折旧', budget: '120,000', actual: '115,000', diff: -4.2 },
|
|
{ item: '维修费用', budget: '80,000', actual: '45,000', diff: -43.8 }
|
|
])
|
|
|
|
const deptRanking = ref([
|
|
{ name: '生产部', value: '680,000', percent: 85, color: '#f56c6c' },
|
|
{ name: '技术部', value: '320,000', percent: 40, color: '#e6a23c' },
|
|
{ name: '质检部', value: '210,000', percent: 26, color: '#409eff' },
|
|
{ name: '采购部', value: '185,000', percent: 23, color: '#67c23a' },
|
|
{ name: '仓储部', value: '173,000', percent: 22, color: '#909399' }
|
|
])
|
|
|
|
const alarms = ref([
|
|
{ icon: '🔴', title: '能源成本超预算5%', time: '15:30:00', level: 'danger' },
|
|
{ icon: '⚠️', title: '原材料成本接近预算上限', time: '10:20:00', level: 'warning' },
|
|
{ icon: '⚠️', title: '维修费用大幅低于预算', time: '09:15:00', level: 'success' }
|
|
])
|
|
|
|
const updateTime = () => {
|
|
currentTime.value = new Date().toLocaleString('zh-CN', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
second: '2-digit'
|
|
})
|
|
}
|
|
|
|
const initCharts = () => {
|
|
if (pieChartRef.value) {
|
|
pieChart = echarts.init(pieChartRef.value)
|
|
pieChart.setOption({
|
|
tooltip: { trigger: 'item', formatter: '{b}: {c}万 ({d}%)' },
|
|
legend: { bottom: 10, textStyle: { color: '#999' } },
|
|
series: [{
|
|
type: 'pie',
|
|
radius: ['45%', '75%'],
|
|
center: ['50%', '45%'],
|
|
data: [
|
|
{ value: 985, name: '原材料', itemStyle: { color: '#f56c6c' } },
|
|
{ value: 234, name: '人工', itemStyle: { color: '#e6a23c' } },
|
|
{ value: 189, name: '能源', itemStyle: { color: '#409eff' } },
|
|
{ value: 115, name: '折旧', itemStyle: { color: '#67c23a' } },
|
|
{ value: 45, name: '维修', itemStyle: { color: '#909399' } }
|
|
],
|
|
label: { show: false }
|
|
}]
|
|
})
|
|
}
|
|
|
|
if (lineChartRef.value) {
|
|
lineChart = echarts.init(lineChartRef.value)
|
|
lineChart.setOption({
|
|
tooltip: { trigger: 'axis' },
|
|
legend: { data: ['实际', '预算'], bottom: 10, textStyle: { color: '#999' } },
|
|
grid: { top: 20, right: 20, bottom: 40, left: 50 },
|
|
xAxis: {
|
|
type: 'category',
|
|
data: ['1月', '2月', '3月', '4月', '5月', '6月'],
|
|
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
|
axisLabel: { color: '#999' }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLine: { show: false },
|
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
|
axisLabel: { color: '#999' }
|
|
},
|
|
series: [
|
|
{
|
|
name: '实际',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: [1450, 1520, 1380, 1610, 1580, 1568],
|
|
lineStyle: { color: '#f56c6c', width: 2 },
|
|
itemStyle: { color: '#f56c6c' }
|
|
},
|
|
{
|
|
name: '预算',
|
|
type: 'line',
|
|
smooth: true,
|
|
data: [1500, 1500, 1500, 1500, 1500, 1500],
|
|
lineStyle: { color: '#67c23a', width: 2, type: 'dashed' },
|
|
itemStyle: { color: '#67c23a' }
|
|
}
|
|
]
|
|
})
|
|
}
|
|
|
|
if (barChartRef.value) {
|
|
barChart = echarts.init(barChartRef.value)
|
|
barChart.setOption({
|
|
tooltip: { trigger: 'axis' },
|
|
grid: { top: 20, right: 20, bottom: 30, left: 50 },
|
|
xAxis: {
|
|
type: 'category',
|
|
data: ['原材料', '人工', '能源', '折旧', '维修'],
|
|
axisLine: { lineStyle: { color: '#2a3f5c' } },
|
|
axisLabel: { color: '#999' }
|
|
},
|
|
yAxis: {
|
|
type: 'value',
|
|
axisLine: { show: false },
|
|
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.1)' } },
|
|
axisLabel: { color: '#999' }
|
|
},
|
|
series: [{
|
|
type: 'bar',
|
|
data: [985, 234, 189, 115, 45],
|
|
itemStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: '#f56c6c' },
|
|
{ offset: 1, color: '#e6a23c' }
|
|
])
|
|
},
|
|
barWidth: '50%'
|
|
}]
|
|
})
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
updateTime()
|
|
timeInterval = setInterval(updateTime, 1000)
|
|
initCharts()
|
|
|
|
window.addEventListener('resize', () => {
|
|
pieChart?.resize()
|
|
lineChart?.resize()
|
|
barChart?.resize()
|
|
})
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (timeInterval) clearInterval(timeInterval)
|
|
pieChart?.dispose()
|
|
lineChart?.dispose()
|
|
barChart?.dispose()
|
|
})
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.cost-screen {
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: calc(100vh - 180px);
|
|
background: linear-gradient(135deg, #1a1f4e 0%, #2d1b3d 100%);
|
|
color: #d3d6dd;
|
|
position: relative;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.screen-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 15px 30px;
|
|
flex-shrink: 0;
|
|
|
|
.title {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
color: #fff;
|
|
text-shadow: 0 0 20px rgba(230, 162, 60, 0.8);
|
|
letter-spacing: 4px;
|
|
margin: 0;
|
|
}
|
|
|
|
.time {
|
|
font-size: 16px;
|
|
color: #e6a23c;
|
|
font-family: 'Courier New', monospace;
|
|
}
|
|
}
|
|
|
|
.screen-body {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 0 15px 15px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 15px;
|
|
padding: 15px 0;
|
|
flex-shrink: 0;
|
|
|
|
.data-card {
|
|
background: rgba(19, 25, 47, 0.8);
|
|
border: 1px solid rgba(230, 162, 60, 0.3);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
text-align: center;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
|
|
.card-title {
|
|
font-size: 14px;
|
|
color: #999;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.card-value {
|
|
font-size: 28px;
|
|
font-weight: bold;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.card-unit {
|
|
font-size: 12px;
|
|
color: #666;
|
|
}
|
|
|
|
.card-trend {
|
|
font-size: 12px;
|
|
margin-top: 6px;
|
|
|
|
&.up { color: #f56c6c; }
|
|
&.down { color: #67c23a; }
|
|
}
|
|
}
|
|
}
|
|
|
|
.chart-area {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 15px;
|
|
padding: 15px 0;
|
|
flex: 1;
|
|
min-height: 0;
|
|
|
|
.chart-box {
|
|
background: rgba(19, 25, 47, 0.8);
|
|
border: 1px solid rgba(230, 162, 60, 0.3);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.box-title {
|
|
font-size: 14px;
|
|
color: #e6a23c;
|
|
margin-bottom: 12px;
|
|
padding-left: 8px;
|
|
border-left: 3px solid #e6a23c;
|
|
}
|
|
|
|
.chart {
|
|
flex: 1;
|
|
min-height: 200px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.bottom-area {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 15px;
|
|
padding: 15px 0;
|
|
flex-shrink: 0;
|
|
|
|
.bottom-box {
|
|
background: rgba(19, 25, 47, 0.8);
|
|
border: 1px solid rgba(230, 162, 60, 0.3);
|
|
border-radius: 8px;
|
|
padding: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
|
|
.box-title {
|
|
font-size: 14px;
|
|
color: #e6a23c;
|
|
margin-bottom: 12px;
|
|
padding-left: 8px;
|
|
border-left: 3px solid #e6a23c;
|
|
}
|
|
|
|
:deep(.el-table) {
|
|
flex: 1;
|
|
font-size: 12px;
|
|
|
|
th, td {
|
|
padding: 8px 5px;
|
|
}
|
|
}
|
|
}
|
|
|
|
.ranking-list {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-around;
|
|
|
|
.ranking-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 8px 12px;
|
|
background: rgba(230, 162, 60, 0.1);
|
|
border-radius: 6px;
|
|
|
|
.rank {
|
|
width: 20px;
|
|
height: 20px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
font-weight: bold;
|
|
margin-right: 10px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
|
&.rank-1 { background: linear-gradient(135deg, #ffd700, #ffb700); color: #fff; }
|
|
&.rank-2 { background: linear-gradient(135deg, #c0c0c0, #a0a0a0); color: #fff; }
|
|
&.rank-3 { background: linear-gradient(135deg, #cd7f32, #b87333); color: #fff; }
|
|
}
|
|
|
|
.name {
|
|
width: 60px;
|
|
font-size: 13px;
|
|
color: #d3d6dd;
|
|
}
|
|
|
|
.bar-wrapper {
|
|
flex: 1;
|
|
height: 8px;
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-radius: 4px;
|
|
margin: 0 10px;
|
|
overflow: hidden;
|
|
|
|
.bar-fill {
|
|
height: 100%;
|
|
border-radius: 4px;
|
|
transition: width 0.5s ease;
|
|
}
|
|
}
|
|
|
|
.value {
|
|
font-size: 13px;
|
|
font-weight: bold;
|
|
color: #e6a23c;
|
|
}
|
|
}
|
|
}
|
|
|
|
.alarm-list {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-around;
|
|
|
|
.alarm-item {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 10px;
|
|
border-radius: 6px;
|
|
border-left: 4px solid;
|
|
|
|
&.danger {
|
|
background: rgba(245, 108, 108, 0.15);
|
|
border-color: #f56c6c;
|
|
}
|
|
|
|
&.warning {
|
|
background: rgba(230, 162, 60, 0.15);
|
|
border-color: #e6a23c;
|
|
}
|
|
|
|
&.success {
|
|
background: rgba(103, 194, 58, 0.15);
|
|
border-color: #67c23a;
|
|
}
|
|
|
|
.alarm-icon {
|
|
font-size: 16px;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.alarm-content {
|
|
flex: 1;
|
|
|
|
.alarm-title {
|
|
font-size: 13px;
|
|
color: #fff;
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.alarm-time {
|
|
font-size: 11px;
|
|
color: #999;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 1200px) {
|
|
.card-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.chart-area,
|
|
.bottom-area {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.chart-area .chart-box:last-child,
|
|
.bottom-area .bottom-box:last-child {
|
|
grid-column: span 2;
|
|
}
|
|
}
|
|
|
|
@media screen and (max-width: 768px) {
|
|
.card-grid,
|
|
.chart-area,
|
|
.bottom-area {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.chart-area .chart-box:last-child,
|
|
.bottom-area .bottom-box:last-child {
|
|
grid-column: span 1;
|
|
}
|
|
|
|
.screen-header .title {
|
|
font-size: 18px;
|
|
}
|
|
}
|
|
</style>
|