|
|
@@ -48,7 +48,9 @@
|
|
|
<div class="metrics-grid">
|
|
|
<div class="metric-card primary">
|
|
|
<div class="metric-label">固定成本(a)</div>
|
|
|
- <div class="metric-value">{{ costAnalysisResult.fixedCost.toFixed(2) }} <span class="unit">元</span></div>
|
|
|
+ <div class="metric-value">{{ costAnalysisResult.fixedCost.toFixed(2) }} <span class="unit">元</span>
|
|
|
+ <div v-if="costAnalysisResult.fixedCostNote" class="note">{{ costAnalysisResult.fixedCostNote }}</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div class="metric-card primary">
|
|
|
<div class="metric-label">单位变动成本(b)</div>
|
|
|
@@ -72,9 +74,11 @@
|
|
|
</div>
|
|
|
<div class="metric-card warning">
|
|
|
<div class="metric-label">显著性</div>
|
|
|
- <div class="metric-value">{{ costAnalysisResult.significance }}</div>
|
|
|
- <div class="p-value" v-if="costAnalysisResult.pValue !== undefined && costAnalysisResult.pValue !== null">
|
|
|
- (P={{ formatPValue(costAnalysisResult.pValue) }})
|
|
|
+ <div class="metric-value">
|
|
|
+ {{ costAnalysisResult.significance }}
|
|
|
+ <span class="p-value" v-if="costAnalysisResult.pValue !== undefined && costAnalysisResult.pValue !== null">
|
|
|
+ (P={{ formatPValue(costAnalysisResult.pValue) }})
|
|
|
+ </span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="metric-card warning">
|
|
|
@@ -95,7 +99,9 @@
|
|
|
<div class="metrics-grid">
|
|
|
<div class="metric-card primary">
|
|
|
<div class="metric-label">固定成本(a)</div>
|
|
|
- <div class="metric-value">{{ waterElectricityAnalysisResult.fixedCost.toFixed(2) }} <span class="unit">元</span></div>
|
|
|
+ <div class="metric-value">{{ waterElectricityAnalysisResult.fixedCost.toFixed(2) }} <span class="unit">元</span>
|
|
|
+ <div v-if="waterElectricityAnalysisResult.fixedCostNote" class="note">{{ waterElectricityAnalysisResult.fixedCostNote }}</div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
<div class="metric-card primary">
|
|
|
<div class="metric-label">单位变动成本(b)</div>
|
|
|
@@ -119,9 +125,11 @@
|
|
|
</div>
|
|
|
<div class="metric-card warning">
|
|
|
<div class="metric-label">显著性</div>
|
|
|
- <div class="metric-value">{{ waterElectricityAnalysisResult.significance }}</div>
|
|
|
- <div class="p-value" v-if="waterElectricityAnalysisResult.pValue !== undefined && waterElectricityAnalysisResult.pValue !== null">
|
|
|
- (P={{ formatPValue(waterElectricityAnalysisResult.pValue) }})
|
|
|
+ <div class="metric-value">
|
|
|
+ {{ waterElectricityAnalysisResult.significance }}
|
|
|
+ <span class="p-value" v-if="waterElectricityAnalysisResult.pValue !== undefined && waterElectricityAnalysisResult.pValue !== null">
|
|
|
+ (P={{ formatPValue(waterElectricityAnalysisResult.pValue) }})
|
|
|
+ </span>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="metric-card warning">
|
|
|
@@ -134,6 +142,14 @@
|
|
|
</div>
|
|
|
</el-card>
|
|
|
|
|
|
+ <!-- 固定成本和单位变动成本趋势图 -->
|
|
|
+ <el-card class="chart-card" v-if="rollingCostData.length > 0">
|
|
|
+ <div slot="header" class="clearfix">
|
|
|
+ <span>成本趋势分析(12个月滚动计算)</span>
|
|
|
+ </div>
|
|
|
+ <div ref="costTrendChart" class="chart-container"></div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
<el-card class="table-card">
|
|
|
<div slot="header" class="clearfix">
|
|
|
<span>成本数据明细</span>
|
|
|
@@ -237,6 +253,8 @@
|
|
|
|
|
|
<script setup name="CostForecast">
|
|
|
import {listCost} from "@/api/lean/cost";
|
|
|
+import * as echarts from 'echarts';
|
|
|
+import { parseTime } from "@/utils/ruoyi.js";
|
|
|
|
|
|
const {proxy} = getCurrentInstance();
|
|
|
const {all_ws} = proxy.useDict('all_ws');
|
|
|
@@ -250,6 +268,9 @@ const originalRows = ref({}); // 用于保存原始数据,便于取消编辑
|
|
|
const selectedQueryYear = ref(new Date().getFullYear().toString()); // 查询条件中的年份,默认当年,转为字符串
|
|
|
const costAnalysisResult = ref(null); // 成本性态分析结果
|
|
|
const waterElectricityAnalysisResult = ref(null); // 水电费分析结果
|
|
|
+const rollingCostData = ref([]); // 用于存储滚动计算的成本数据
|
|
|
+const costTrendChart = ref(null); // 图表实例
|
|
|
+let chartInstance = null; // 图表实例引用
|
|
|
|
|
|
// 获取车间名称
|
|
|
function getWsName(wsValue) {
|
|
|
@@ -419,16 +440,30 @@ function performCostAnalysis(data, property = 'totalCost') {
|
|
|
// 计算 P 值 (使用近似计算)
|
|
|
const pValue = calculatePValue(fStatistic, 1, n - 2);
|
|
|
|
|
|
+ // 检查计算结果是否合理
|
|
|
+ const isResultValid = !isNaN(a) && !isNaN(b) && isFinite(a) && isFinite(b);
|
|
|
+
|
|
|
+ if (!isResultValid) {
|
|
|
+ return null; // 返回空结果,避免显示无效数据
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加注释说明固定成本为负数的情况
|
|
|
+ let fixedCostNote = "";
|
|
|
+ if (a < 0) {
|
|
|
+ fixedCostNote = " (负数,可能数据分布特殊)";
|
|
|
+ }
|
|
|
+
|
|
|
return {
|
|
|
- fixedCost: a, // 固定成本
|
|
|
- variableCostPerUnit: b, // 单位变动成本(元/米)
|
|
|
- correlationCoefficient: r, // 相关系数
|
|
|
- rSquared: rSquared, // R方 (决定系数)
|
|
|
- fStatistic: fStatistic, // F统计量
|
|
|
- pValue: pValue, // P 值
|
|
|
+ fixedCost: parseFloat(a.toFixed(2)), // 固定成本,保留2位小数
|
|
|
+ variableCostPerUnit: parseFloat(b.toFixed(2)), // 单位变动成本(元/米),保留2位小数
|
|
|
+ correlationCoefficient: parseFloat(r.toFixed(4)), // 相关系数,保留4位小数
|
|
|
+ rSquared: rSquared !== null ? parseFloat(rSquared.toFixed(4)) : null, // R方 (决定系数),保留4位小数
|
|
|
+ fStatistic: fStatistic !== null ? parseFloat(fStatistic.toFixed(2)) : null, // F统计量,保留2位小数
|
|
|
+ pValue: pValue !== null ? parseFloat(pValue.toFixed(6)) : null, // P 值,保留6位小数
|
|
|
significance: pValue !== null ? (pValue < 0.05 ? "显著" : "不显著") : "无法计算", // 显著性
|
|
|
- standardError: standardError, // 标准误差
|
|
|
- dataPoints: validData.length // 有效数据点数量
|
|
|
+ standardError: standardError !== null ? parseFloat(standardError.toFixed(2)) : null, // 标准误差,保留2位小数
|
|
|
+ dataPoints: validData.length, // 有效数据点数量
|
|
|
+ fixedCostNote: fixedCostNote // 固定成本注释
|
|
|
};
|
|
|
}
|
|
|
|
|
|
@@ -622,6 +657,170 @@ function performCostForecast() {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+// 计算滚动12个月的成本数据
|
|
|
+function calculateRollingCostData(data) {
|
|
|
+ const rollingData = [];
|
|
|
+
|
|
|
+ // 按时间排序
|
|
|
+ const sortedData = [...data].sort((a, b) => new Date(a.costTime) - new Date(b.costTime));
|
|
|
+
|
|
|
+ // 对每个月计算前12个月的滚动平均值
|
|
|
+ for (let i = 0; i < sortedData.length; i++) {
|
|
|
+ // 当前月份
|
|
|
+ const currentDate = new Date(sortedData[i].costTime);
|
|
|
+
|
|
|
+ // 计算12个月时间窗口的开始和结束时间
|
|
|
+ // 开始时间:当前月份的前11个月
|
|
|
+ const startDate = new Date(currentDate);
|
|
|
+ startDate.setMonth(startDate.getMonth() - 11);
|
|
|
+
|
|
|
+ // 结束时间:当前月份
|
|
|
+ const endDate = new Date(currentDate);
|
|
|
+
|
|
|
+ // 筛选出在这个12个月时间窗口内的数据
|
|
|
+ const windowData = sortedData.filter(item => {
|
|
|
+ const itemDate = new Date(item.costTime);
|
|
|
+ return itemDate >= startDate && itemDate <= endDate;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 过滤有效数据用于总成本分析
|
|
|
+ const validTotalCostData = windowData.filter(item =>
|
|
|
+ item.length !== null &&
|
|
|
+ item.totalCost !== null &&
|
|
|
+ !isNaN(parseFloat(item.length)) &&
|
|
|
+ !isNaN(parseFloat(item.totalCost))
|
|
|
+ );
|
|
|
+
|
|
|
+ // 过滤有效数据用于水电费分析
|
|
|
+ const validWaterElectricityData = windowData.filter(item =>
|
|
|
+ item.length !== null &&
|
|
|
+ item.waterElectricity !== null &&
|
|
|
+ !isNaN(parseFloat(item.length)) &&
|
|
|
+ !isNaN(parseFloat(item.waterElectricity))
|
|
|
+ );
|
|
|
+
|
|
|
+ // 计算总成本的固定成本和单位变动成本
|
|
|
+ let totalCostAnalysis = null;
|
|
|
+ if (validTotalCostData.length >= 2) {
|
|
|
+ totalCostAnalysis = performCostAnalysis(validTotalCostData, 'totalCost');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算水电费的固定成本和单位变动成本
|
|
|
+ let waterElectricityAnalysis = null;
|
|
|
+ if (validWaterElectricityData.length >= 2) {
|
|
|
+ waterElectricityAnalysis = performCostAnalysis(validWaterElectricityData, 'waterElectricity');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加到结果数组
|
|
|
+ rollingData.push({
|
|
|
+ month: parseTime(sortedData[i].costTime, '{y}-{m}'),
|
|
|
+ costTime: sortedData[i].costTime,
|
|
|
+ totalCostFixed: totalCostAnalysis ? totalCostAnalysis.fixedCost : null,
|
|
|
+ totalCostVariable: totalCostAnalysis ? totalCostAnalysis.variableCostPerUnit : null,
|
|
|
+ waterElectricityFixed: waterElectricityAnalysis ? waterElectricityAnalysis.fixedCost : null,
|
|
|
+ waterElectricityVariable: waterElectricityAnalysis ? waterElectricityAnalysis.variableCostPerUnit : null
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return rollingData;
|
|
|
+}
|
|
|
+
|
|
|
+// 绘制成本趋势图
|
|
|
+function drawCostTrendChart() {
|
|
|
+ if (!costTrendChart.value) return;
|
|
|
+
|
|
|
+ // 销毁之前的图表实例
|
|
|
+ if (chartInstance) {
|
|
|
+ chartInstance.dispose();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 初始化图表
|
|
|
+ chartInstance = echarts.init(costTrendChart.value);
|
|
|
+
|
|
|
+ // 准备图表数据
|
|
|
+ const months = rollingCostData.value.map(item => item.month);
|
|
|
+ const totalCostFixedData = rollingCostData.value.map(item => item.totalCostFixed);
|
|
|
+ const totalCostVariableData = rollingCostData.value.map(item => item.totalCostVariable);
|
|
|
+ const waterElectricityFixedData = rollingCostData.value.map(item => item.waterElectricityFixed);
|
|
|
+ const waterElectricityVariableData = rollingCostData.value.map(item => item.waterElectricityVariable);
|
|
|
+
|
|
|
+ // 图表配置
|
|
|
+ const option = {
|
|
|
+ tooltip: {
|
|
|
+ trigger: 'axis',
|
|
|
+ axisPointer: {
|
|
|
+ type: 'cross'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ legend: {
|
|
|
+ data: ['总成本固定部分', '总成本单位变动', '水电费固定部分', '水电费单位变动']
|
|
|
+ },
|
|
|
+ grid: {
|
|
|
+ left: '3%',
|
|
|
+ right: '4%',
|
|
|
+ bottom: '3%',
|
|
|
+ containLabel: true
|
|
|
+ },
|
|
|
+ xAxis: [
|
|
|
+ {
|
|
|
+ type: 'category',
|
|
|
+ boundaryGap: false,
|
|
|
+ data: months
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ yAxis: [
|
|
|
+ {
|
|
|
+ type: 'value',
|
|
|
+ name: '固定成本(元)',
|
|
|
+ position: 'left'
|
|
|
+ },
|
|
|
+ {
|
|
|
+ type: 'value',
|
|
|
+ name: '单位变动成本(元/米)',
|
|
|
+ position: 'right'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ series: [
|
|
|
+ {
|
|
|
+ name: '总成本固定部分',
|
|
|
+ type: 'line',
|
|
|
+ yAxisIndex: 0,
|
|
|
+ data: totalCostFixedData,
|
|
|
+ smooth: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '总成本单位变动',
|
|
|
+ type: 'line',
|
|
|
+ yAxisIndex: 1,
|
|
|
+ data: totalCostVariableData,
|
|
|
+ smooth: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '水电费固定部分',
|
|
|
+ type: 'line',
|
|
|
+ yAxisIndex: 0,
|
|
|
+ data: waterElectricityFixedData,
|
|
|
+ smooth: true
|
|
|
+ },
|
|
|
+ {
|
|
|
+ name: '水电费单位变动',
|
|
|
+ type: 'line',
|
|
|
+ yAxisIndex: 1,
|
|
|
+ data: waterElectricityVariableData,
|
|
|
+ smooth: true
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ };
|
|
|
+
|
|
|
+ // 设置图表选项
|
|
|
+ chartInstance.setOption(option);
|
|
|
+
|
|
|
+ // 监听窗口大小变化,自适应图表
|
|
|
+ window.addEventListener('resize', () => {
|
|
|
+ chartInstance.resize();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
const data = reactive({
|
|
|
form: {},
|
|
|
queryParams: {
|
|
|
@@ -664,16 +863,24 @@ function getList() {
|
|
|
selectedQueryYear.value = new Date().getFullYear().toString();
|
|
|
}
|
|
|
queryParams.value.params = {};
|
|
|
- // 如果选择了年份,则构造该年份的开始和结束时间
|
|
|
- if (selectedQueryYear.value) {
|
|
|
- queryParams.value.params["year"] = selectedQueryYear.value;
|
|
|
- }
|
|
|
+ // 不传递年份参数,获取该车间下所有数据
|
|
|
+ // 年份过滤在前端进行
|
|
|
|
|
|
// 构造查询条件
|
|
|
const queryParam = {...queryParams.value};
|
|
|
|
|
|
listCost(queryParam).then(response => {
|
|
|
- // 生成完整的12个月数据
|
|
|
+ // 先按时间排序
|
|
|
+ const sortedData = [...response.rows].sort((a, b) => new Date(a.costTime) - new Date(b.costTime));
|
|
|
+
|
|
|
+ // 过滤出选定年份及前一年的数据用于12个月滚动计算
|
|
|
+ const filteredData = sortedData.filter(item => {
|
|
|
+ const itemYear = new Date(item.costTime).getFullYear();
|
|
|
+ // 保留选定年份和前一年的数据用于滚动计算
|
|
|
+ return itemYear >= (parseInt(selectedQueryYear.value) - 1) && itemYear <= parseInt(selectedQueryYear.value);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 生成选定年份的完整12个月数据用于表格显示
|
|
|
const fullYearData = generateFullYearData(response.rows, selectedQueryYear.value);
|
|
|
|
|
|
costList.value = fullYearData.map(item => ({
|
|
|
@@ -706,6 +913,22 @@ function getList() {
|
|
|
!isNaN(parseFloat(item.waterElectricity))
|
|
|
), 'waterElectricity');
|
|
|
|
|
|
+ // 计算滚动成本数据,使用包含前一年数据的完整数据集
|
|
|
+ rollingCostData.value = calculateRollingCostData(filteredData);
|
|
|
+
|
|
|
+ // 过滤滚动数据,只显示选定年份的数据用于趋势图显示
|
|
|
+ const chartFilteredData = rollingCostData.value.filter(item => {
|
|
|
+ const itemYear = new Date(item.costTime).getFullYear();
|
|
|
+ return itemYear == parseInt(selectedQueryYear.value);
|
|
|
+ });
|
|
|
+
|
|
|
+ rollingCostData.value = chartFilteredData;
|
|
|
+
|
|
|
+ // 绘制趋势图
|
|
|
+ nextTick(() => {
|
|
|
+ drawCostTrendChart();
|
|
|
+ });
|
|
|
+
|
|
|
// 执行成本预测
|
|
|
if (costAnalysisResult.value) {
|
|
|
performCostForecast();
|
|
|
@@ -769,6 +992,13 @@ onMounted(() => {
|
|
|
}
|
|
|
handleQuery();
|
|
|
});
|
|
|
+
|
|
|
+// 组件卸载时销毁图表
|
|
|
+onUnmounted(() => {
|
|
|
+ if (chartInstance) {
|
|
|
+ chartInstance.dispose();
|
|
|
+ }
|
|
|
+});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
@@ -780,6 +1010,10 @@ onMounted(() => {
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
|
|
|
+.chart-card {
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
.table-card {
|
|
|
margin-bottom: 20px;
|
|
|
}
|
|
|
@@ -806,34 +1040,39 @@ onMounted(() => {
|
|
|
margin: 0 20px;
|
|
|
}
|
|
|
|
|
|
+.analysis-section {
|
|
|
+ padding: 10px;
|
|
|
+}
|
|
|
+
|
|
|
.section-title {
|
|
|
- font-size: 18px;
|
|
|
+ font-size: 16px;
|
|
|
font-weight: bold;
|
|
|
color: #333;
|
|
|
- margin-bottom: 15px;
|
|
|
+ margin-bottom: 10px;
|
|
|
text-align: center;
|
|
|
- padding-bottom: 10px;
|
|
|
+ padding-bottom: 8px;
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
}
|
|
|
|
|
|
.metrics-grid {
|
|
|
display: grid;
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
- gap: 15px;
|
|
|
- margin-top: 15px;
|
|
|
+ gap: 6px;
|
|
|
+ margin-top: 6px;
|
|
|
}
|
|
|
|
|
|
.metric-card {
|
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|
|
- border-radius: 8px;
|
|
|
- padding: 15px 10px;
|
|
|
+ border-radius: 6px;
|
|
|
+ padding: 5px 4px;
|
|
|
text-align: center;
|
|
|
transition: all 0.3s ease;
|
|
|
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
|
|
|
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.05);
|
|
|
border: 1px solid #e9ecef;
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
justify-content: center;
|
|
|
+ min-height: 45px;
|
|
|
}
|
|
|
|
|
|
.metric-card:hover {
|
|
|
@@ -863,19 +1102,20 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
.metric-label {
|
|
|
- font-size: 12px;
|
|
|
+ font-size: 11px;
|
|
|
color: #495057;
|
|
|
- margin-bottom: 8px;
|
|
|
+ margin-bottom: 2px;
|
|
|
font-weight: 500;
|
|
|
}
|
|
|
|
|
|
.metric-value {
|
|
|
- font-size: 16px;
|
|
|
+ font-size: 13px;
|
|
|
font-weight: bold;
|
|
|
color: #212529;
|
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
|
+ line-height: 1.2;
|
|
|
}
|
|
|
|
|
|
.unit {
|
|
|
@@ -884,9 +1124,17 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
.p-value {
|
|
|
- font-size: 11px;
|
|
|
+ font-size: 10px;
|
|
|
color: #666;
|
|
|
- margin-top: 3px;
|
|
|
+ margin-top: 1px;
|
|
|
+ display: inline-block;
|
|
|
+ line-height: 1;
|
|
|
+}
|
|
|
+
|
|
|
+.note {
|
|
|
+ font-size: 10px;
|
|
|
+ color: #ff9900;
|
|
|
+ margin-top: 2px;
|
|
|
}
|
|
|
|
|
|
.model-display {
|
|
|
@@ -932,6 +1180,11 @@ onMounted(() => {
|
|
|
font-weight: bold;
|
|
|
}
|
|
|
|
|
|
+.chart-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 240px;
|
|
|
+}
|
|
|
+
|
|
|
@media (max-width: 1200px) {
|
|
|
.metrics-grid {
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
@@ -953,4 +1206,4 @@ onMounted(() => {
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
}
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|