<template>
|
<div class="large-screen">
|
<div class="top-kpi-row">
|
<div class="group-card group-card--wide">
|
<div class="group-left">
|
<div class="group-icon group-icon--order"></div>
|
</div>
|
<div class="group-right">
|
<div class="group-metrics">
|
<div class="metric metric--plain">
|
<div class="metric-title">工单总数</div>
|
<div class="metric-value">{{ kpi.workOrderTotal }}<span class="unit">条</span></div>
|
</div>
|
<div class="metric metric--plain">
|
<div class="metric-title">进行中工单</div>
|
<div class="metric-value">{{ kpi.workOrderDoing }}<span class="unit">条</span></div>
|
</div>
|
<div class="metric metric--plain">
|
<div class="metric-title">完成工单</div>
|
<div class="metric-value">{{ kpi.workOrderDone }}<span class="unit">条</span></div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="group-card group-card--wide">
|
<div class="group-left">
|
<div class="group-icon group-icon--quality"></div>
|
</div>
|
<div class="group-right">
|
<div class="group-metrics">
|
<div class="metric metric--plain">
|
<div class="metric-title">合格率</div>
|
<div class="metric-value">{{ kpi.passRate }}<span class="unit">%</span></div>
|
</div>
|
<div class="metric metric--plain">
|
<div class="metric-title">不良率</div>
|
<div class="metric-value">{{ kpi.ngRate }}<span class="unit">%</span></div>
|
</div>
|
<div class="metric metric--plain">
|
<div class="metric-title">总报废数</div>
|
<div class="metric-value">{{ kpi.scrapTotal }}<span class="unit">条</span></div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="top-stat-row">
|
<div class="stat-card stat-card--primary">
|
<div class="stat-main">
|
<div class="stat-value">{{ formatMoney(kpi.productionAmount) }}<span class="unit1">吨</span></div>
|
<div class="stat-title">总生产总量</div>
|
<div class="stat-sub">
|
<span>较上月趋势:</span>
|
<span class="trend" :class="kpi.productionAmountTrend >= 0 ? 'up' : 'down'">
|
{{ kpi.productionAmountTrend >= 0 ? '+' : '' }}{{ kpi.productionAmountTrend }}%
|
</span>
|
<svg
|
class="trend-arrow-svg"
|
:class="[
|
kpi.productionAmountTrend >= 0 ? '' : 'trend-arrow-svg--up',
|
kpi.productionAmountTrend >= 0 ? 'is-up' : 'is-down'
|
]"
|
viewBox="0 0 16 10"
|
aria-hidden="true"
|
>
|
<path d="M1 2 L6 7 L10 3 L15 3" />
|
<path d="M13 1 L15 3 L13 5" />
|
</svg>
|
</div>
|
</div>
|
<div class="stat-primary-decor"></div>
|
</div>
|
|
<div class="stat-card">
|
<div class="stat-main">
|
<div class="stat-value">{{ formatMoney(kpi.productionCost) }}<span class="unit">吨</span></div>
|
<div class="stat-title">生产总消耗</div>
|
<div class="stat-sub">
|
<span>较上月趋势:</span>
|
<span class="trend" :class="kpi.productionCostTrend >= 0 ? 'up' : 'down'">
|
{{ kpi.productionCostTrend >= 0 ? '+' : '' }}{{ kpi.productionCostTrend }}%
|
</span>
|
<svg
|
class="trend-arrow-svg"
|
:class="[
|
kpi.productionCostTrend >= 0 ? '' : 'trend-arrow-svg--up',
|
kpi.productionCostTrend >= 0 ? 'is-up' : 'is-down'
|
]"
|
viewBox="0 0 16 10"
|
aria-hidden="true"
|
>
|
<path d="M1 2 L6 7 L10 3 L15 3" />
|
<path d="M13 1 L15 3 L13 5" />
|
</svg>
|
</div>
|
</div>
|
<div class="stat-icon stat-icon--cost"></div>
|
</div>
|
|
<div class="stat-card">
|
<div class="stat-main">
|
<div class="stat-value">{{ kpi.supplierCount }}<span class="unit">家</span></div>
|
<div class="stat-title">产品总供应公司</div>
|
<div class="stat-sub">
|
<span>较上月趋势:</span>
|
<span class="trend" :class="kpi.supplierCountTrend >= 0 ? 'up' : 'down'">
|
{{ kpi.supplierCountTrend >= 0 ? '+' : '' }}{{ kpi.supplierCountTrend }}%
|
</span>
|
<svg
|
class="trend-arrow-svg"
|
:class="[
|
kpi.supplierCountTrend >= 0 ? '' : 'trend-arrow-svg--up',
|
kpi.supplierCountTrend >= 0 ? 'is-up' : 'is-down'
|
]"
|
viewBox="0 0 16 10"
|
aria-hidden="true"
|
>
|
<path d="M1 2 L6 7 L10 3 L15 3" />
|
<path d="M13 1 L15 3 L13 5" />
|
</svg>
|
</div>
|
</div>
|
<div class="stat-icon stat-icon--supplier"></div>
|
</div>
|
</div>
|
|
<div class="mid-row">
|
<div class="panel panel--rect">
|
<div class="panel-header">
|
<div class="panel-title">产品产出分析</div>
|
</div>
|
<div class="panel-body">
|
<div class="product-pie-wrapper">
|
<Echarts
|
:chartStyle="chartStyle"
|
:legend="categoryPieLegend"
|
:tooltip="categoryPieTooltip"
|
:series="categoryPieSeries"
|
:xAxis="[]"
|
:yAxis="[]"
|
:color="categoryPieColors"
|
:options="transparentOptions"
|
style="height: 100%"
|
/>
|
</div>
|
<div class="category-cards">
|
<div v-for="(it, idx) in categoryPieData" :key="idx" class="category-card">
|
<div class="category-name">{{ it.name }}</div>
|
<div class="category-val">{{ it.value }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="panel panel--rect">
|
<div class="panel-header">
|
<div class="panel-title">不良原因统计分析</div>
|
</div>
|
<div class="panel-body">
|
<div class="ng-layout">
|
<div class="ng-list">
|
<div class="ng-list-head">
|
<div class="ng-col ng-col--name">不良原因</div>
|
<div class="ng-col ng-col--pct">占比</div>
|
<div class="ng-col ng-col--val">数量</div>
|
</div>
|
<div class="ng-list-body">
|
<div v-for="(row, idx) in ngRows" :key="row.name" class="ng-row">
|
<div class="ng-col ng-col--name">
|
<span class="ng-dot" :style="{ backgroundColor: ngColors[idx % ngColors.length] }"></span>
|
<span class="ng-name">{{ row.name }}</span>
|
</div>
|
<div class="ng-col ng-col--pct">{{ row.percent }}%</div>
|
<div class="ng-col ng-col--val">{{ row.value }}</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="ng-chart">
|
<div class="donut-wrap">
|
<Echarts
|
:chartStyle="chartStyle"
|
:legend="{ show: false }"
|
:tooltip="pieTooltip"
|
:series="pieSeries"
|
:color="ngColors"
|
:xAxis="[]"
|
:yAxis="[]"
|
:options="transparentOptions"
|
style="height: 100%"
|
/>
|
<div class="donut-center">
|
<div class="donut-label">不良原因</div>
|
<div class="donut-value">{{ formatMoney(ngTotal) }}</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
|
<div class="bottom-row">
|
<div class="panel panel-full">
|
<div class="panel-header">
|
<div class="panel-title">工序不良率分析</div>
|
<div class="panel-actions">
|
<div class="panel-action">筛选时间范围</div>
|
<el-date-picker
|
v-model="dateRange"
|
type="daterange"
|
range-separator="至"
|
start-placeholder="开始日期"
|
end-placeholder="结束日期"
|
format="YYYY-MM-DD"
|
value-format="YYYY-MM-DD"
|
:clearable="true"
|
class="panel-date-picker"
|
size="small"
|
@change="handleDateChange"
|
/>
|
</div>
|
</div>
|
<div class="panel-body">
|
<div class="bottom-chart-wrap" v-loading="isLoading" element-loading-text="加载中...">
|
<div class="chart-unit">单位:%</div>
|
<Echarts
|
:chartStyle="{ width: '100%', height: '260px' }"
|
:grid="lineGrid"
|
:legend="lineLegend"
|
:tooltip="axisTooltip"
|
:xAxis="lineXAxis"
|
:yAxis="lineYAxis"
|
:series="lineSeries"
|
:options="transparentOptions"
|
style="height: 100%"
|
/>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import {
|
findProductQualityStatistics,
|
findProductWorkOrderCountStatistics,
|
findProductProductionStatistics,
|
findProductOutputCategoryPieData,
|
findProductDefectReasonAnalysis,
|
findProductProcessDefectRateAnalysis
|
} from '@/api/productionManagement/productionStatistic.js'
|
|
import {computed, onMounted, ref} from 'vue'
|
import * as echarts from 'echarts'
|
import Echarts from '@/components/Echarts/echarts.vue'
|
import { ElSelect, ElOption, ElDatePicker } from 'element-plus'
|
|
const chartStyle = { width: '100%', height: '100%' }
|
const transparentOptions = {
|
backgroundColor: 'transparent',
|
textStyle: { color: '#3a3a3a' },
|
}
|
|
const kpi = ref({
|
workOrderTotal: 0,
|
workOrderDoing: 0,
|
workOrderDone: 0,
|
passRate: 0,
|
ngRate: 0,
|
scrapTotal: 0,
|
productionAmount: 0, // 总生产总量
|
productionAmountTrend: 0, // 总生产总量较上月趋势
|
productionCost: 0, // 生产总消耗
|
productionCostTrend: 0, // 生产总消耗较上月趋势
|
supplierCount: 0, // 总供应商
|
supplierCountTrend: 0, // 总供应商较上月趋势
|
})
|
const dateRange = ref([])
|
|
function formatMoney(n) {
|
const num = Number(n || 0)
|
return num.toLocaleString('zh-CN', { maximumFractionDigits: 0 })
|
}
|
|
const axisTooltip = {
|
trigger: 'axis',
|
axisPointer: { type: 'shadow' },
|
confine: true, // 限制在图表区域内显示
|
position: 'top', // 固定显示在顶部
|
enterable: true, // 允许鼠标进入提示框区域
|
extraCssText: 'max-height: 200px; overflow-y: auto; padding: 10px;', // 添加最大高度和滚动条
|
formatter: (params) => {
|
const first = params?.[0]
|
const x = first?.axisValueLabel || ''
|
const lines = [`${x}`]
|
params.forEach((p) => {
|
if (!p) return
|
const name = p.seriesName
|
const v = p.data
|
lines.push(`${name}: ${v}`)
|
})
|
return lines.join('<br/>')
|
},
|
}
|
|
// 中部左侧:饼状图(按产品大类统计产出数量)
|
const categoryPieColors = ['#2D5BFF', '#4E8AFF', '#00A4ED', '#26C6DA', '#7C3AED', '#F59E0B', '#EF4444', '#10B981']
|
const categoryPieData = ref([])
|
|
const categoryPieLegend = computed(() => {
|
// 设计图中饼图本身显示占比标签,不额外展示图例
|
return {
|
show: false,
|
data: categoryPieData.value.map((d) => d.name),
|
}
|
})
|
|
const categoryPieTooltip = {
|
trigger: 'item',
|
formatter: (p) => `${p.marker} ${p.name}<br/>产出数量:${p.value}<br/>占比:${p.percent}%`,
|
}
|
|
const categoryPieSeries = computed(() => [
|
{
|
name: '产出数量',
|
type: 'pie',
|
radius: ['40%', '68%'],
|
center: ['50%', '45%'],
|
avoidLabelOverlap: false,
|
label: {
|
show: true,
|
formatter: (p) => `${p.name} ${p.percent}%`,
|
color: '#6b7280',
|
fontSize: 12,
|
},
|
labelLine: {
|
show: true,
|
lineStyle: { color: '#e5e7eb' },
|
length: 16,
|
length2: 20,
|
},
|
data: categoryPieData.value,
|
emphasis: {
|
scale: true,
|
scaleSize: 8,
|
},
|
},
|
])
|
|
// 中部右侧:环形饼图
|
const ngColors = ['#2D5BFF', '#26C6DA', '#F59E0B', '#7C3AED', '#60A5FA', '#10B981']
|
|
const ngReasonData = ref([])
|
const ngRateData = ref([])
|
const isLoading = ref(false)
|
|
const ngTotal = computed(() => ngReasonData.value.reduce((s, it) => s + Number(it.value || 0), 0))
|
const ngRows = computed(() => {
|
const total = ngTotal.value || 0
|
return ngReasonData.value.map((it) => {
|
const v = Number(it.value || 0)
|
const p = total ? Math.round((v / total) * 100) : 0
|
return { name: it.name, value: v, percent: p }
|
})
|
})
|
|
const pieTooltip = {
|
trigger: 'item',
|
formatter: (p) => `${p.marker} ${p.name}:${p.value}(${p.percent}%)`,
|
}
|
const pieSeries = computed(() => [
|
{
|
name: '不良原因',
|
type: 'pie',
|
radius: ['52%', '76%'],
|
center: ['50%', '50%'],
|
avoidLabelOverlap: true,
|
label: { show: false },
|
labelLine: { show: false },
|
data: ngReasonData.value,
|
},
|
])
|
|
// 底部:折线图(年份联动的每个工序不良率)
|
const lineGrid = { left: '4%', right: '4%', top: 60, bottom: 30, containLabel: true }
|
const lineLegend = computed(() => {
|
const processes = chartData.value.processes || []
|
const processNames = processes.map(p => p.name)
|
return {
|
show: true,
|
top: 8,
|
type: 'scroll',
|
orient: 'horizontal',
|
textStyle: { color: '#666' },
|
data: ['平均不良率', ...processNames],
|
pageIconSize: 10,
|
pageTextStyle: { color: '#666' },
|
pageButtonItemGap: 5,
|
pageButtonGap: 10
|
}
|
})
|
|
const chartData = computed(() => {
|
const data = ngRateData.value
|
if (!data || data.length === 0) {
|
return {
|
x: [],
|
bar: [],
|
processes: []
|
}
|
}
|
|
// Extract data from the API response
|
const x = data.map(item => item.date)
|
const bar = data.map(item => item.averageDefectRate) // Convert to percentage
|
|
// Extract process names and their data
|
const processNames = new Set()
|
data.forEach(item => {
|
item.processes.forEach(process => {
|
Object.keys(process).forEach(name => processNames.add(name))
|
})
|
})
|
|
const processes = Array.from(processNames).map(name => {
|
return {
|
name,
|
data: data.map(item => {
|
const process = item.processes.find(p => p[name] !== undefined)
|
return process ? process[name] : 0 // Convert to percentage
|
})
|
}
|
})
|
|
return {
|
x,
|
bar,
|
processes
|
}
|
})
|
|
const lineXAxis = computed(() => {
|
return [
|
{
|
type: 'category',
|
data: chartData.value.x,
|
axisTick: { show: false },
|
axisLabel: { color: '#666' },
|
},
|
]
|
})
|
|
const lineYAxis = computed(() => {
|
return [
|
{
|
type: 'value',
|
axisLabel: { color: '#666' },
|
splitLine: { lineStyle: { color: 'rgba(0,0,0,0.06)' } },
|
name: '不良率(%)',
|
},
|
]
|
})
|
|
const lineSeries = computed(() => {
|
const series = [
|
{
|
name: '平均不良率',
|
type: 'bar',
|
barWidth: 18,
|
data: chartData.value.bar,
|
itemStyle: {
|
color: 'rgba(59, 130, 246, 0.15)',
|
borderRadius: [4, 4, 0, 0],
|
},
|
},
|
]
|
|
// Add process lines with different colors
|
const colors = ['#3b82f6', '#f59e0b', '#10b981', '#8b5cf6', '#ec4899', '#14b8a6', '#f97316']
|
chartData.value.processes.forEach((process, index) => {
|
series.push({
|
name: process.name,
|
type: 'line',
|
smooth: true,
|
data: process.data,
|
symbol: 'circle',
|
symbolSize: 6,
|
lineStyle: { width: 2, color: colors[index % colors.length] },
|
itemStyle: { color: colors[index % colors.length] },
|
})
|
})
|
|
return series
|
})
|
|
// 获取生产工单数量统计数据
|
const fetchProductWorkOrderCountStatistic = () => {
|
findProductWorkOrderCountStatistics().then((res) => {
|
const data = res.data
|
kpi.value.workOrderTotal = data.totalCount
|
kpi.value.workOrderDoing = data.inProgressCount
|
kpi.value.workOrderDone = data.completedCount
|
})
|
}
|
|
// 获取生产质量统计数据
|
const fetchProductQualityStatistics = () => {
|
findProductQualityStatistics().then((res) => {
|
const data = res.data
|
kpi.value.passRate = data.qualifiedRate
|
kpi.value.ngRate = data.defectRate
|
kpi.value.scrapTotal = data.scrapCount
|
})
|
}
|
|
// 获取生产数量统计数据
|
const fetchProductProductionStatistics = () => {
|
findProductProductionStatistics().then((res) => {
|
const data = res.data
|
kpi.value.productionAmount = data.productionOutput
|
kpi.value.productionAmountTrend = data.productionOutputMonthlyChange
|
kpi.value.productionCost = data.productionConsumption
|
kpi.value.productionCostTrend = data.productionConsumptionMonthlyChange
|
kpi.value.supplierCount = data.supplierCount
|
kpi.value.supplierCountTrend = data.supplierCountMonthlyChange
|
})
|
}
|
|
// 获取产品产出分析数据
|
const fetchProductOutputCategoryPieData = () => {
|
findProductOutputCategoryPieData().then((res) => {
|
categoryPieData.value = res.data
|
})
|
}
|
// 获取不良原因分析统计数据
|
const fetchProductDefectReasonAnalysis = () => {
|
findProductDefectReasonAnalysis().then((res) => {
|
ngReasonData.value = res.data
|
})
|
}
|
|
// 获取工序不良率分析
|
const fetchProductProcessDefectRateAnalysis = (dateRange) => {
|
isLoading.value = true
|
const params = dateRange ? {
|
startDate: dateRange[0],
|
endDate: dateRange[1]
|
} : {}
|
findProductProcessDefectRateAnalysis(params).then((res) => {
|
ngRateData.value = res.data
|
}).finally(() => {
|
isLoading.value = false
|
})
|
}
|
|
// 处理日期选择变化
|
const handleDateChange = (range) => {
|
fetchProductProcessDefectRateAnalysis(range)
|
}
|
|
onMounted(() => {
|
// 初始化时获取生产工单数量统计数据
|
fetchProductWorkOrderCountStatistic()
|
// 初始化时获取生产质量统计数据
|
fetchProductQualityStatistics()
|
// 初始化时获取生产数量统计数据
|
fetchProductProductionStatistics()
|
// 初始化时获取产品产出分析数据
|
fetchProductOutputCategoryPieData()
|
// 初始化时获取不良原因分析统计数据
|
fetchProductDefectReasonAnalysis()
|
// 初始化时获取工序不良率分析
|
fetchProductProcessDefectRateAnalysis()
|
})
|
|
</script>
|
|
<style scoped>
|
.large-screen {
|
padding: 18px 20px;
|
background: #f5f7fb;
|
}
|
|
.top-kpi-row {
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
gap: 12px;
|
}
|
|
.top-stat-row {
|
margin-top: 12px;
|
display: grid;
|
grid-template-columns: 1fr 1fr 1fr;
|
gap: 12px;
|
}
|
|
.group-card {
|
background: #fff;
|
border-radius: 8px;
|
padding: 14px 16px;
|
box-shadow: 0 6px 22px rgba(16, 24, 40, 0.08);
|
border: 1px solid rgba(2, 6, 23, 0.06);
|
display: grid;
|
grid-template-columns: 120px 1fr;
|
min-height: 112px;
|
}
|
|
.group-card--wide {
|
grid-column: span 1;
|
}
|
|
.group-left {
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.group-icon {
|
width: 110px;
|
height: 110px;
|
border-radius: 18px;
|
background: transparent;
|
border: 0;
|
position: relative;
|
overflow: hidden;
|
}
|
|
.group-icon::before {
|
content: none;
|
}
|
|
.group-icon::after {
|
content: '';
|
position: absolute;
|
inset: 0;
|
border-radius: 999px;
|
background-size: contain;
|
background-position: center;
|
background-repeat: no-repeat;
|
}
|
|
.group-icon--order::after {
|
background-image: url('@/assets/BI/kpiIcons/kpi-dataBoard.png');
|
}
|
|
.group-icon--quality::after {
|
background-image: url('@/assets/BI/kpiIcons/kpi-shield.png');
|
}
|
|
.group-right {
|
display: flex;
|
align-items: center;
|
padding-left: 60px;
|
}
|
|
.group-metrics {
|
display: grid;
|
grid-template-columns: repeat(3, 1fr);
|
gap: 18px;
|
width: 100%;
|
}
|
|
.metric {
|
background: transparent;
|
border: 0;
|
border-radius: 0;
|
padding: 0;
|
}
|
|
.metric--plain .metric-title {
|
font-weight: 400;
|
font-size: 16px;
|
color: #111827;
|
line-height: 1.1;
|
}
|
|
.metric-value {
|
margin-top: 10px;
|
font-size: 30px;
|
font-weight: 400;
|
color: #111827;
|
line-height: 1;
|
letter-spacing: 0.5px;
|
}
|
|
.metric-sub {
|
margin-top: 10px;
|
font-size: 12px;
|
color: rgba(2, 6, 23, 0.58);
|
}
|
|
.unit {
|
margin-left: 4px;
|
font-size: 12px;
|
font-weight: 600;
|
color: rgba(2, 6, 23, 0.55);
|
}
|
.unit1 {
|
margin-left: 4px;
|
font-size: 12px;
|
font-weight: 600;
|
color: #fff;
|
}
|
|
.stat-card {
|
background: #fff;
|
border-radius: 8px;
|
padding: 14px 18px;
|
box-shadow: 0 6px 22px rgba(16, 24, 40, 0.08);
|
border: 1px solid rgba(2, 6, 23, 0.06);
|
display: grid;
|
grid-template-columns: 1fr 58px;
|
column-gap: 12px;
|
align-items: center;
|
min-height: 116px;
|
}
|
|
.stat-card--primary {
|
background: linear-gradient(135deg, #2f6bff 0%, #3a87ff 100%);
|
border-color: rgba(47, 107, 255, 0.18);
|
color: #fff;
|
grid-template-columns: 1fr 1fr;
|
position: relative;
|
overflow: hidden;
|
}
|
|
.stat-card--primary .stat-title,
|
.stat-card--primary .stat-sub,
|
.stat-card--primary .stat-value,
|
.stat-card--primary .trend {
|
color: #fff;
|
}
|
|
.stat-card--primary .trend.up,
|
.stat-card--primary .trend.down {
|
color: #fff;
|
}
|
|
.stat-card--primary .trend-arrow-svg.is-up {
|
color: #fff;
|
}
|
|
.stat-card--primary .trend-arrow-svg.is-down {
|
color: #fff;
|
}
|
|
.stat-primary-decor {
|
width: 100%;
|
height: 100%;
|
border-radius: 12px;
|
position: relative;
|
}
|
|
.stat-primary-decor::after {
|
content: none;
|
}
|
|
.stat-card--primary::after {
|
content: '';
|
position: absolute;
|
right: -12px;
|
bottom: -22px;
|
width: 120px;
|
height: 120px;
|
background-image: url('@/assets/BI/kpiIcons/kpi-cartCutout.png');
|
background-repeat: no-repeat;
|
background-position: right bottom;
|
background-size: contain;
|
opacity: 0.95;
|
pointer-events: none;
|
}
|
|
.stat-main {
|
display: flex;
|
flex-direction: column;
|
}
|
|
.stat-title {
|
margin-top: 8px;
|
font-size: 13px;
|
color: rgba(17, 24, 39, 0.86);
|
}
|
|
.stat-value {
|
font-size: 38px;
|
font-weight: 400;
|
color: #111827;
|
line-height: 1;
|
letter-spacing: 0.2px;
|
}
|
|
.currency-sign {
|
font-size: 0.58em;
|
margin-right: 2px;
|
vertical-align: 8%;
|
}
|
|
.stat-sub {
|
margin-top: 10px;
|
font-size: 12px;
|
color: rgba(17, 24, 39, 0.75);
|
display: flex;
|
align-items: center;
|
gap: 4px;
|
}
|
|
.trend {
|
font-size: 20px;
|
font-weight: 400;
|
color: #16a34a;
|
}
|
.trend.up {
|
color: #16a34a;
|
}
|
.trend.down {
|
color: #dc2626;
|
}
|
|
.trend-arrow-svg {
|
width: 17px;
|
height: 11px;
|
margin-left: 3px;
|
stroke: currentColor;
|
fill: none;
|
stroke-width: 1.8;
|
stroke-linecap: round;
|
stroke-linejoin: round;
|
}
|
|
.trend-arrow-svg--up {
|
transform: scaleY(-1);
|
transform-origin: center;
|
}
|
|
.trend-arrow-svg.is-up {
|
color: #16a34a;
|
}
|
|
.trend-arrow-svg.is-down {
|
color: #dc2626;
|
}
|
|
|
.stat-icon {
|
width: 54px;
|
height: 54px;
|
border-radius: 999px;
|
border: 1px solid rgba(2, 6, 23, 0.04);
|
box-shadow: 0 4px 10px rgba(2, 6, 23, 0.08), inset 0 2px 10px rgba(255, 255, 255, 0.55);
|
}
|
|
.stat-card--blue .stat-icon {
|
background: rgba(255, 255, 255, 0.22);
|
border-color: rgba(255, 255, 255, 0.25);
|
}
|
|
.stat-icon--money {
|
background: radial-gradient(circle at 30% 30%, rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.12) 45%),
|
linear-gradient(135deg, rgba(255, 183, 77, 0.22), rgba(255, 171, 64, 0.4));
|
}
|
.stat-icon--qty {
|
background: transparent;
|
border: 0;
|
box-shadow: none;
|
background-image: url('@/assets/BI/kpiIcons/kpi-dataBoard.png');
|
background-size: contain;
|
background-position: center;
|
background-repeat: no-repeat;
|
}
|
.stat-icon--cost {
|
background: transparent;
|
border: 0;
|
box-shadow: none;
|
background-image: url('@/assets/BI/kpiIcons/kpi-dbIcon.png');
|
background-size: contain;
|
background-position: center;
|
background-repeat: no-repeat;
|
}
|
.stat-icon--supplier {
|
background: transparent;
|
border: 0;
|
box-shadow: none;
|
background-image: url('@/assets/BI/kpiIcons/kpi-cloud.png');
|
background-size: contain;
|
background-position: center;
|
background-repeat: no-repeat;
|
}
|
|
.mid-row {
|
margin-top: 14px;
|
display: grid;
|
grid-template-columns: 1fr 1fr;
|
gap: 14px;
|
align-items: stretch;
|
}
|
|
.bottom-row {
|
margin-top: 14px;
|
}
|
|
.panel {
|
background: #fff;
|
border-radius: 10px;
|
box-shadow: 0 4px 18px rgba(20, 28, 47, 0.06);
|
border: 1px solid rgba(20, 28, 47, 0.06);
|
overflow: hidden;
|
display: flex;
|
flex-direction: column;
|
min-height: 360px;
|
}
|
|
.panel--rect {
|
border-radius: 0;
|
}
|
|
.panel-full {
|
min-height: 320px;
|
}
|
|
.panel-header {
|
padding: 12px 14px;
|
border-bottom: 1px solid rgba(2, 6, 23, 0.06);
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
}
|
|
.panel-actions {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
font-size: 12px;
|
color: rgba(2, 6, 23, 0.6);
|
white-space: nowrap;
|
}
|
|
.panel-select {
|
min-width: 88px;
|
}
|
|
:deep(.panel-select .el-select__wrapper) {
|
width: 88px;
|
}
|
|
:deep(.panel-select .el-select__selected-text) {
|
white-space: nowrap;
|
}
|
|
.panel-date-picker {
|
width: 240px;
|
margin-left: 6px;
|
}
|
|
.panel-date-picker {
|
width: 240px;
|
margin-left: 6px;
|
}
|
|
.panel-title {
|
font-weight: 800;
|
color: #030303;
|
font-size: 14px;
|
letter-spacing: 1px;
|
position: relative;
|
padding-left: 10px;
|
}
|
|
.panel-title::before {
|
content: '';
|
position: absolute;
|
left: 0;
|
top: 50%;
|
transform: translateY(-50%);
|
width: 3px;
|
height: 14px;
|
border-radius: 999px;
|
background: #0047ff;
|
}
|
|
.panel-subtitle {
|
margin-top: 6px;
|
font-size: 13px;
|
color: rgba(2, 6, 23, 0.58);
|
}
|
|
.panel-body {
|
padding: 12px 14px 14px;
|
flex: 1;
|
min-height: 280px;
|
position: relative;
|
display: flex;
|
flex-direction: column;
|
}
|
|
.bottom-chart-wrap {
|
width: 90%;
|
margin: 0 auto;
|
}
|
|
.product-pie-wrapper {
|
flex: 0 0 260px;
|
}
|
|
.chart-unit {
|
font-size: 12px;
|
color: rgba(107, 114, 128, 1);
|
margin-bottom: 4px;
|
}
|
.category-cards {
|
margin-top: 18px;
|
display: grid;
|
grid-template-columns: repeat(5, 1fr);
|
gap: 12px;
|
}
|
|
.category-card {
|
height: 64px;
|
background: #f9fafb;
|
border-radius: 6px;
|
padding: 10px 14px;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
}
|
|
.category-name {
|
font-size: 13px;
|
color: rgba(17, 24, 39, 0.7);
|
}
|
|
.category-val {
|
margin-top: 6px;
|
font-size: 14px;
|
color: rgba(17, 24, 39, 0.9);
|
}
|
|
.donut-wrap {
|
position: relative;
|
height: 100%;
|
}
|
|
.donut-center {
|
position: absolute;
|
left: 50%;
|
top: 50%;
|
transform: translate(-50%, -50%);
|
text-align: center;
|
pointer-events: none;
|
}
|
|
.donut-label {
|
font-size: 12px;
|
color: rgba(2, 6, 23, 0.58);
|
}
|
|
.donut-value {
|
margin-top: 6px;
|
font-size: 28px;
|
font-weight: 900;
|
color: #111827;
|
}
|
|
.ng-layout {
|
display: grid;
|
grid-template-columns: 1.15fr 0.85fr;
|
gap: 14px;
|
height: 100%;
|
}
|
|
.ng-list {
|
padding: 8px 10px;
|
background: #f7f8fb;
|
border-radius: 6px;
|
}
|
|
.ng-list-title {
|
font-weight: 400;
|
font-size: 14px;
|
color: rgba(2, 6, 23, 0.75);
|
margin: 4px 0 10px;
|
}
|
|
.ng-list-head {
|
display: grid;
|
grid-template-columns: 1fr 64px 64px;
|
gap: 6px;
|
padding: 10px 8px 8px;
|
border-radius: 0;
|
background: transparent;
|
color: rgba(2, 6, 23, 0.6);
|
font-size: 13px;
|
font-weight: 400;
|
border-bottom: 1px solid rgba(2, 6, 23, 0.08);
|
}
|
|
.ng-list-body {
|
margin-top: 2px;
|
display: flex;
|
flex-direction: column;
|
gap: 0;
|
}
|
|
.ng-row {
|
display: grid;
|
grid-template-columns: 1fr 64px 64px;
|
gap: 6px;
|
padding: 12px 8px;
|
border-radius: 0;
|
border: 0;
|
border-bottom: 1px dashed rgba(2, 6, 23, 0.14);
|
background: transparent;
|
}
|
|
.ng-col {
|
display: flex;
|
align-items: center;
|
font-size: 18px;
|
color: rgba(2, 6, 23, 0.72);
|
}
|
|
.ng-col--pct,
|
.ng-col--val {
|
justify-content: flex-end;
|
font-variant-numeric: tabular-nums;
|
}
|
|
.ng-col--val {
|
font-weight: 400;
|
color: rgba(2, 6, 23, 0.86);
|
}
|
|
.ng-dot {
|
width: 7px;
|
height: 7px;
|
border-radius: 999px;
|
margin-right: 8px;
|
}
|
|
.ng-name {
|
font-weight: 400;
|
color: rgba(2, 6, 23, 0.78);
|
}
|
|
.ng-col--pct,
|
.ng-col--val {
|
font-size: 20px;
|
color: rgba(2, 6, 23, 0.82);
|
}
|
|
.ng-chart {
|
height: 100%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
.ng-chart .donut-wrap {
|
width: 100%;
|
max-width: 360px;
|
height: 320px;
|
}
|
|
@media (max-width: 1100px) {
|
.ng-layout {
|
grid-template-columns: 1fr;
|
}
|
.donut-wrap :deep(canvas) {
|
margin: 0 auto;
|
}
|
}
|
|
@media (max-width: 1600px) {
|
/* 修复旧布局残留:避免强行 span 导致 top-kpi/top-stat 网格错位 */
|
.top-stat-row {
|
grid-template-columns: 1fr 1fr 1fr;
|
}
|
.top-kpi-row {
|
grid-template-columns: 1fr 1fr;
|
}
|
}
|
|
@media (max-width: 1100px) {
|
.mid-row {
|
grid-template-columns: 1fr;
|
}
|
}
|
</style>
|