Files
screen/src/views/dashboard/cost/index.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>