Files
xgy-oa/klp-ui/src/views/ems/dashboard/panels/MonthToMonth.vue
2025-09-30 20:52:52 +08:00

608 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="month-on-month-analysis">
<!-- 筛选区域//年切换 + 单周期选择 -->
<div class="filter-section">
<el-row :gutter="20" align="middle">
<!-- 时间类型切换默认选中按周 -->
<el-col :span="6">
<el-select
v-model="timeType"
placeholder="请选择时间类型"
@change="handleTimeTypeChange"
clearable
>
<el-option label="按周" value="week"></el-option>
<el-option label="按月" value="month"></el-option>
<el-option label="按年" value="year"></el-option>
</el-select>
</el-col>
<!-- 日期选择器单周期选择非范围 -->
<el-col :span="12">
<!-- 1. 按周单周选择器 -->
<el-date-picker
v-if="timeType === 'week'"
v-model="dateRange"
type="date"
placeholder="选择周"
format="yyyy年第WW周"
value-format="yyyy-'W'WW"
picker-options="{ type: 'week' }"
:disabled-date="disableFutureDate"
></el-date-picker>
<!-- 2. 按月单月选择器 -->
<el-date-picker
v-else-if="timeType === 'month'"
v-model="dateRange"
type="date"
placeholder="选择月"
format="yyyy-MM"
value-format="yyyy-MM"
picker-options="{ type: 'month' }"
:disabled-date="disableFutureDate"
></el-date-picker>
<!-- 3. 按年单年选择器 -->
<el-date-picker
v-else-if="timeType === 'year'"
v-model="dateRange"
type="date"
placeholder="选择年"
format="yyyy"
value-format="yyyy"
picker-options="{ type: 'year' }"
:disabled-date="disableFutureDate"
></el-date-picker>
</el-col>
<!-- 查询/重置按钮无选择时禁用查询 -->
<el-col :span="6">
<el-button type="primary" @click="handleQuery" :disabled="!dateRange">查询</el-button>
<el-button type="text" @click="handleReset" style="margin-left: 10px;">重置</el-button>
</el-col>
</el-row>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<el-loading-spinner></el-loading-spinner>
<p>数据加载中...</p>
</div>
<!-- 环比指标卡区域保持原有展示逻辑 -->
<div v-else class="indicator-cards">
<el-row :gutter="20">
<!-- 当期值 -->
<el-col :span="6">
<div class="indicator-card">
<div class="card-header">
<span class="card-title">当期值</span>
<span class="card-period">{{ currentPeriodText }}</span>
</div>
<div class="card-value">{{ formatNumber(currentValue) }} {{ unit }}</div>
<div class="card-desc">当前{{ timeTypeMap[timeType] }}{{ indicatorName }}</div>
</div>
</el-col>
<!-- 上期值 -->
<el-col :span="6">
<div class="indicator-card">
<div class="card-header">
<span class="card-title">上期值</span>
<span class="card-period">{{ previousPeriodText }}</span>
</div>
<div class="card-value">{{ formatNumber(previousValue) }} {{ unit }}</div>
<div class="card-desc">上一个{{ timeTypeMap[timeType] }}{{ indicatorName }}</div>
</div>
</el-col>
<!-- 增加值 -->
<el-col :span="6">
<div class="indicator-card">
<div class="card-header">
<span class="card-title">增加值</span>
<span class="card-icon"><i class="el-icon-arrow-right"></i></span>
</div>
<div class="card-value" :class="increaseValue > 0 ? 'text-increase' : increaseValue < 0 ? 'text-decrease' : ''">
{{ formatNumber(increaseValue) }} {{ unit }}
</div>
<div class="card-desc">当期 - 上期</div>
</div>
</el-col>
<!-- 环比 -->
<el-col :span="6">
<div class="indicator-card">
<div class="card-header">
<span class="card-title">环比</span>
<span class="card-icon"><i class="el-icon-refresh"></i></span>
</div>
<div class="card-value" :class="monthOnMonth > 0 ? 'text-increase' : monthOnMonth < 0 ? 'text-decrease' : ''">
{{ monthOnMonth !== null ? (monthOnMonth * 100).toFixed(2) + '%' : '--' }}
</div>
<div class="card-desc">当期 - 上期/ 上期 × 100%</div>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
export default {
name: 'CycleAnalysis',
props: {
// 数据单位kWh、Nm³、元
unit: {
type: String,
default: 'kWh'
},
// 指标名称(如:能耗、费用、产量)
indicatorName: {
type: String,
default: '能耗'
}
},
data() {
return {
// 时间类型默认按周week
timeType: 'week',
// 单周期选择值格式week→yyyy-Wwwmonth→yyyy-MMyear→yyyy
dateRange: '',
// 加载状态
loading: false,
// 核心数据
currentValue: null, // 当期值
previousValue: null, // 上期值
increaseValue: null, // 增加值(当期-上期)
monthOnMonth: null, // 环比((当期-上期)/上期)
// 周期文本描述2025年第23周、2025年06月
currentPeriodText: '',
previousPeriodText: '',
// 时间类型映射(用于文案显示)
timeTypeMap: {
week: '周',
month: '月',
year: '年'
}
}
},
mounted() {
// 初始化默认周期(当前周/月/年)+ 自动查询一次
this.initDateRange()
this.handleQuery()
},
methods: {
/**
* 初始化默认周期(当前时间类型的当前周期)
*/
initDateRange() {
const now = new Date()
const year = now.getFullYear()
let currentPeriod
switch (this.timeType) {
case 'week':
// 周格式yyyy-Www两位周数
const [currentWeek] = this.getWeekInfo(now)
currentPeriod = `${year}-W${this.padZero(currentWeek)}`
break
case 'month':
// 月格式yyyy-MM两位月份
const currentMonth = now.getMonth() + 1
currentPeriod = `${year}-${this.padZero(currentMonth)}`
break
case 'year':
// 年格式yyyy
currentPeriod = `${year}`
break
}
this.dateRange = currentPeriod
// 同步更新周期文本(当期+上期)
this.updatePeriodText()
},
/**
* 获取周信息(本年周数 + 周起始日期)
* @param {Date} date - 日期对象
* @returns {Array} [周数, 周起始日期Date]
*/
getWeekInfo(date) {
const firstDayOfYear = new Date(date.getFullYear(), 0, 1)
const pastDaysOfYear = (date - firstDayOfYear) / (24 * 60 * 60 * 1000)
// 周日为一周第一天,计算本年周数
const weekNumber = Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7)
// 计算当前周的起始日期(周日)
const weekStart = new Date(date)
weekStart.setDate(date.getDate() - date.getDay())
return [weekNumber, weekStart]
},
/**
* 计算上期周期(根据当前周期自动推导)
* @param {String} currentPeriod - 当前周期2025-W23、2025-06、2025
* @param {String} timeType - 时间类型week/month/year
* @returns {String} 上期周期
*/
getPreviousPeriod(currentPeriod, timeType) {
const [year, part] = currentPeriod.split(
timeType === 'week' ? '-W' : timeType === 'month' ? '-' : ''
)
let prevYear = parseInt(year)
let prevPart
switch (timeType) {
case 'week':
const currentWeek = parseInt(part)
// 若当前是第1周上期为上一年最后一周
if (currentWeek === 1) {
prevYear -= 1
prevPart = this.getLastWeekOfYear(prevYear)
} else {
prevPart = currentWeek - 1
}
return `${prevYear}-W${this.padZero(prevPart)}`
case 'month':
const currentMonth = parseInt(part)
// 若当前是1月上期为上一年12月
if (currentMonth === 1) {
prevYear -= 1
prevPart = 12
} else {
prevPart = currentMonth - 1
}
return `${prevYear}-${this.padZero(prevPart)}`
case 'year':
// 上期为上一年
return `${prevYear - 1}`
}
},
/**
* 获取某一年的最后一周(处理跨年周场景)
* @param {Number} year - 年份
* @returns {Number} 最后一周的周数
*/
getLastWeekOfYear(year) {
const lastDayOfYear = new Date(year, 11, 31) // 12月31日
const [lastWeek] = this.getWeekInfo(lastDayOfYear)
return lastWeek
},
/**
* 将周期转换为时间段startDate ~ endDate格式yyyy-MM-dd
* @param {String} period - 周期2025-W23、2025-06、2025
* @param {String} timeType - 时间类型week/month/year
* @returns {Object} { start: 开始日期, end: 结束日期 }
*/
getPeriodTimeRange(period, timeType) {
let startDate, endDate
const [year, part] = period.split(
timeType === 'week' ? '-W' : timeType === 'month' ? '-' : ''
)
const targetYear = parseInt(year)
switch (timeType) {
case 'week':
const week = parseInt(part)
// 计算该年第N周的起始日期周日
const firstDayOfYear = new Date(targetYear, 0, 1)
const firstWeekStart = new Date(firstDayOfYear)
firstWeekStart.setDate(firstDayOfYear.getDate() - firstDayOfYear.getDay())
// 第N周起始 = 第一周起始 + (N-1)*7天
startDate = new Date(firstWeekStart)
startDate.setDate(firstWeekStart.getDate() + (week - 1) * 7)
// 第N周结束 = 起始 + 6天周六
endDate = new Date(startDate)
endDate.setDate(startDate.getDate() + 6)
break
case 'month':
const month = parseInt(part) - 1 // 月份从0开始
startDate = new Date(targetYear, month, 1) // 当月1号
endDate = new Date(targetYear, month + 1, 0) // 当月最后一天
break
case 'year':
startDate = new Date(targetYear, 0, 1) // 1月1号
endDate = new Date(targetYear, 11, 31) // 12月31号
break
}
// 格式化为 yyyy-MM-dd
const format = (date) => date.toISOString().split('T')[0]
return { start: format(startDate), end: format(endDate) }
},
/**
* 更新周期文本描述(当期+上期)
*/
updatePeriodText() {
if (!this.dateRange) return
const currentPeriod = this.dateRange
const previousPeriod = this.getPreviousPeriod(currentPeriod, this.timeType)
switch (this.timeType) {
case 'week':
const [currWYear, currW] = currentPeriod.split('-W')
const [prevWYear, prevW] = previousPeriod.split('-W')
this.currentPeriodText = `${currWYear}年第${currW}`
this.previousPeriodText = `${prevWYear}年第${prevW}`
break
case 'month':
const [currMYear, currM] = currentPeriod.split('-')
const [prevMYear, prevM] = previousPeriod.split('-')
this.currentPeriodText = `${currMYear}${currM}`
this.previousPeriodText = `${prevMYear}${prevM}`
break
case 'year':
this.currentPeriodText = `${currentPeriod}`
this.previousPeriodText = `${previousPeriod}`
break
}
},
/**
* 时间类型切换:重置周期 + 自动查询
*/
handleTimeTypeChange() {
this.dateRange = ''
this.initDateRange()
this.resetData()
this.handleQuery()
},
/**
* 查询按钮:构建参数 + 调用接口 + 计算环比
*/
handleQuery() {
this.loading = true
// 1. 构建接口参数(含当期/上期时间段)
const apiParams = this.buildApiParams()
// 2. 调用接口实际项目替换为axios
this.mockApiRequest(apiParams)
.then(({ currentValue, previousValue }) => {
// 3. 保存原始数据保留2位小数
this.currentValue = Number(currentValue.toFixed(2))
this.previousValue = Number(previousValue.toFixed(2))
// 4. 计算增加值和环比
this.calculateIndicators()
// 5. 更新周期文本
this.updatePeriodText()
})
.catch(err => {
this.$message.error(`数据获取失败:${err.message}`)
this.resetData()
})
.finally(() => {
this.loading = false
})
},
/**
* 构建接口参数(核心:传递当期/上期时间段)
* @returns {Object} 接口请求参数
*/
buildApiParams() {
if (!this.dateRange) return {}
// 当前周期 + 上期周期
const currentPeriod = this.dateRange
const previousPeriod = this.getPreviousPeriod(currentPeriod, this.timeType)
// 转换为时间段start/end: yyyy-MM-dd
const currentTimeRange = this.getPeriodTimeRange(currentPeriod, this.timeType)
const previousTimeRange = this.getPeriodTimeRange(previousPeriod, this.timeType)
return {
indicatorName: this.indicatorName, // 指标名称(如:能耗)
timeType: this.timeType, // 时间类型week/month/year
currentRange: currentTimeRange, // 当期时间段
previousRange: previousTimeRange // 上期时间段
}
},
/**
* 模拟接口请求实际项目替换为axios.post/get
* @param {Object} params - 接口参数
* @returns {Promise} 包含当期/上期值的Promise
*/
mockApiRequest(params) {
return new Promise((resolve) => {
// 模拟网络延迟
setTimeout(() => {
// 根据时间类型生成合理范围的随机数据(以上期为基础)
let baseValue = 0
switch (params.timeType) {
case 'week':
baseValue = 7000 + Math.random() * 2000 // 周数据7000-9000
break
case 'month':
baseValue = 30000 + Math.random() * 5000 // 月数据30000-35000
break
case 'year':
baseValue = 365000 + Math.random() * 50000 // 年数据36.5万-41.5万
break
}
// 当期值 = 上期值 × 随机波动(-5% ~ +15%
const previousValue = baseValue
const fluctuation = -0.05 + Math.random() * 0.2 // 波动范围:-5% ~ +15%
const currentValue = previousValue * (1 + fluctuation)
resolve({ currentValue, previousValue })
}, 800)
})
},
/**
* 计算增加值和环比处理上期为0的异常场景
*/
calculateIndicators() {
// 增加值 = 当期 - 上期
this.increaseValue = this.currentValue - this.previousValue
// 环比 = (当期 - 上期) / 上期上期为0时特殊处理
if (this.previousValue === 0) {
this.monthOnMonth = this.currentValue > 0 ? 1 : 0 // 上期为0时当期有值则环比100%
} else {
this.monthOnMonth = this.increaseValue / this.previousValue
}
},
/**
* 重置:恢复默认周期 + 清空数据 + 自动查询
*/
handleReset() {
this.initDateRange()
this.resetData()
this.handleQuery()
},
/**
* 清空核心数据
*/
resetData() {
this.currentValue = null
this.previousValue = null
this.increaseValue = null
this.monthOnMonth = null
this.currentPeriodText = ''
this.previousPeriodText = ''
},
/**
* 数字补零(确保周数/月份为两位)
* @param {Number} num - 需补零的数字
* @returns {String} 补零后的字符串
*/
padZero(num) {
return num < 10 ? `0${num}` : `${num}`
},
/**
* 格式化数字(千分位分隔 + 保留2位小数
* @param {Number} num - 需格式化的数字
* @returns {String} 格式化后的字符串
*/
formatNumber(num) {
if (num === null || isNaN(num)) return '--'
return num.toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
},
/**
* 禁用未来日期选择(无法选择未到的周/月/年)
* @param {Date} date - 待判断日期
* @returns {Boolean} 是否禁用
*/
disableFutureDate(date) {
return date > new Date()
},
refresh() {
this.handleQuery()
}
}
}
</script>
<style scoped>
.month-on-month-analysis {
padding: 20px;
background-color: #fff;
border-radius: 6px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
}
.filter-section {
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid #f0f0f0;
}
.indicator-cards {
margin-top: 10px;
}
.indicator-card {
background-color: #fff;
border-radius: 6px;
padding: 18px;
height: 100%;
border: 1px solid #f0f0f0;
transition: all 0.3s ease;
}
.indicator-card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transform: translateY(-2px);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.card-title {
font-size: 14px;
color: #666;
font-weight: 500;
}
.card-period {
font-size: 12px;
color: #999;
background-color: #f5f7fa;
padding: 2px 8px;
border-radius: 4px;
}
.card-icon {
color: #409eff;
font-size: 14px;
}
.card-value {
font-size: 26px;
font-weight: 600;
color: #333;
margin-bottom: 6px;
transition: color 0.3s ease;
}
.card-desc {
font-size: 12px;
color: #999;
}
/* 增加值/环比颜色:上升红、下降绿 */
.text-increase {
color: #f56c6c;
}
.text-decrease {
color: #67c23a;
}
/* 加载状态 */
.loading-container {
text-align: center;
padding: 60px 0;
color: #666;
}
.loading-container p {
margin-top: 12px;
font-size: 14px;
}
</style>