diff --git a/src/api/mes/energydevice/index.ts b/src/api/mes/energydevice/index.ts index 0ea5247f..feccb85a 100644 --- a/src/api/mes/energydevice/index.ts +++ b/src/api/mes/energydevice/index.ts @@ -42,6 +42,61 @@ export interface EnergyDeviceDataRecordVO { latestDataTime?: string } +export interface EnergyOverviewMetricVO { + key: string + label: string + value: string + unit?: string + subLabel?: string + subValue?: string + change?: string + down?: boolean +} + +export interface EnergyOverviewTrendVO { + unit?: string + xAxis: string[] + data: string[] +} + +export interface EnergyOverviewRegionItemVO { + name: string + value: string + percent: string +} + +export interface EnergyOverviewRegionVO { + unit?: string + totalValue: string + items: EnergyOverviewRegionItemVO[] +} + +export interface EnergyOverviewRankVO { + id: number + name: string + region: string + value: string +} + +export interface EnergyOverviewDetailVO { + id: number + name: string + energyType: string + region: string + value: string + startTime: string + endTime: string +} + +export interface EnergyOverviewRespVO { + metrics: EnergyOverviewMetricVO[] + trendChart: EnergyOverviewTrendVO + regionChart: EnergyOverviewRegionVO + rankList: EnergyOverviewRankVO[] + total: number + detailList: EnergyOverviewDetailVO[] +} + // 能源设备 API export const EnergyDeviceApi = { // 查询能源设备分页 @@ -66,6 +121,10 @@ export const EnergyDeviceApi = { return await request.get({ url: `/mes/energy-device/queryDataRecords`, params }) }, + queryOverviewData: async (params: any) => { + return await request.get({ url: `/mes/energy-device/queryOverviewData`, params }) + }, + exportQueryDataRecords: async (params: any) => { return await request.download({ url: `/mes/energy-device/record-export-excel`, params }) }, diff --git a/src/locales/en.ts b/src/locales/en.ts index 0558b2c3..a5e948d5 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -4727,5 +4727,42 @@ export default { warningNoPlanData: 'No plan data to save', saveSuccess: 'Schedule saved successfully' } + }, + EnergyOverview: { + messages: { + singleDayOnly: 'The time range can only query data within a single day' + }, + filters: { + org: 'Region', + orgPlaceholder: 'Please select region', + energyType: 'Energy Type', + energyTypePlaceholder: 'Please select energy type', + timeRange: 'Time Range' + }, + panels: { + trend: 'Energy Usage Trend', + region: 'Regional Energy Share', + top5: 'Top 5 Energy Consumers', + detail: 'Energy Details' + }, + table: { + rank: 'Rank', + name: 'Name', + region: 'Region', + usage: 'Usage', + meterName: 'Meter Name', + energyType: 'Energy Type', + energyUsage: 'Energy Usage', + startTime: 'Start Time', + endTime: 'End Time' + }, + chart: { + usage: 'Usage', + time: 'Time', + regionEnergy: 'Regional Energy', + totalUsage: 'Total Usage' + }, + empty: 'Please configure energy types first', + exportSuccess: 'Export started' } } diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index abe15b57..ee469d84 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -4938,5 +4938,42 @@ export default { warningNoPlanData: '暂无可保存的计划数据', saveSuccess: '排产计划保存成功' } + }, + EnergyOverview: { + messages: { + singleDayOnly: '时间范围只能查询同一天的数据' + }, + filters: { + org: '所属区域', + orgPlaceholder: '请选择所属区域', + energyType: '能源类型', + energyTypePlaceholder: '请选择能源类型', + timeRange: '时间范围' + }, + panels: { + trend: '能源用量趋势', + region: '区域能耗占比', + top5: '能耗排行 TOP5', + detail: '能源明细表' + }, + table: { + rank: '排名', + name: '名称', + region: '所属区域', + usage: '用量', + meterName: '表名称', + energyType: '能源类型', + energyUsage: '能源用量', + startTime: '开始时间', + endTime: '结束时间' + }, + chart: { + usage: '用量', + time: '时间', + regionEnergy: '区域能耗', + totalUsage: '总用量' + }, + empty: '请先配置能源类型', + exportSuccess: '导出已发起' } } diff --git a/src/views/mes/energyOverview/index.vue b/src/views/mes/energyOverview/index.vue index ea7d7c2f..f525cba0 100644 --- a/src/views/mes/energyOverview/index.vue +++ b/src/views/mes/energyOverview/index.vue @@ -8,29 +8,35 @@ :inline="true" label-width="auto" > - - + + - + - + - 查询 + {{ t('common.query') }} - 重置 + {{ t('common.reset') }} - 导出 + {{ t('action.export') }} @@ -63,7 +69,7 @@
{{ item.label }}
{{ item.value }} - {{ item.unit }} + {{ item.unit }}
{{ item.subLabel }} @@ -82,17 +88,9 @@
- 能源用量趋势 + {{ t('EnergyOverview.panels.trend') }}
-
- - - - - - -
@@ -101,17 +99,9 @@
- 区域能耗占比 + {{ t('EnergyOverview.panels.region') }}
-
- - - - - - -
@@ -123,19 +113,24 @@
- 能耗排行 TOP5 + {{ t('EnergyOverview.panels.top5') }}
- + - - - + + +
@@ -143,17 +138,22 @@
- 能源明细表 + {{ t('EnergyOverview.panels.detail') }}
- - - - - - + + + + + + - +
@@ -176,6 +176,14 @@ import { EChartsOption } from 'echarts' import { Echart } from '@/components/Echart' import { EnergyTypeApi, EnergyTypeVO } from '@/api/mes/energytype' +import { + EnergyDeviceApi, + EnergyOverviewDetailVO, + EnergyOverviewMetricVO, + EnergyOverviewRankVO, + EnergyOverviewRespVO +} from '@/api/mes/energydevice' +import { OrganizationApi, OrganizationVO } from '@/api/mes/organization' defineOptions({ name: 'EnergyOverview' }) @@ -192,24 +200,40 @@ interface MetricCard { subValue?: string } -interface DetailRow { - id: number - name: string - energyType: string - region: string - value: string - startTime: string - endTime: string -} +type DetailRow = EnergyOverviewDetailVO +const { t } = useI18n() const message = useMessage() -const defaultTimeRange = ['2026-05-04 00:00:00', '2026-05-05 23:59:59'] -const orgOptions = [ - { label: '成型系统', value: 1 }, - { label: '蛋托线01', value: 2 }, - { label: '烘干系统', value: 3 } -] +const getTodayTimeRange = () => { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const date = `${year}-${month}-${day}` + return [`${date} 00:00:00`, `${date} 23:59:59`] +} + +const parseRangeTime = (value?: string) => { + if (!value) return undefined + const date = new Date(value.replace(/-/g, '/')) + return Number.isNaN(date.getTime()) ? undefined : date +} + +const isSingleDayRange = (timeRange?: string[]) => { + if (!timeRange || timeRange.length !== 2) return false + const start = parseRangeTime(timeRange[0]) + const end = parseRangeTime(timeRange[1]) + if (!start || !end) return false + return ( + start.getFullYear() === end.getFullYear() && + start.getMonth() === end.getMonth() && + start.getDate() === end.getDate() + ) +} + +const defaultTimeRange = getTodayTimeRange() +const orgOptions = ref<{ label: string; value: string | number; raw?: OrganizationVO }[]>([]) const loading = ref(false) const queryFormRef = ref() @@ -224,55 +248,19 @@ const queryParams = reactive({ }) const metricCards = ref([]) -const rankList = ref([ - { name: '蛋托成型柜用电量', region: '成型系统', value: '2,139.09' }, - { name: '蛋托线真空柜用电量', region: '蛋托线01', value: '0' }, - { name: '蛋托干燥柜用电量', region: '烘干系统', value: '0' }, - { name: '蛋托线其它用电量', region: '蛋托线01', value: '0' }, - { name: '烘干系统其它用电量', region: '烘干系统', value: '0' } -]) -const detailList = ref([]) +const rankList = ref([]) const pageDetailList = ref([]) const total = ref(0) const hasEnergyTypes = computed(() => energyTypeOptions.value.length > 0) const selectedEnergyType = computed(() => energyTypeOptions.value.find((item) => item.id === queryParams.energyTypeId) ) -const selectedEnergyName = computed(() => selectedEnergyType.value?.name || '电') const selectedEnergyUnit = computed(() => selectedEnergyType.value?.unit || 'kWh') -const selectedUnitConsumptionUnit = computed(() => `${selectedEnergyUnit.value}/件`) - -const hours = [ - '00:00', - '01:00', - '02:00', - '03:00', - '04:00', - '05:00', - '06:00', - '07:00', - '08:00', - '09:00', - '10:00', - '11:00', - '12:00', - '13:00', - '14:00', - '15:00', - '16:00', - '17:00', - '18:00', - '19:00', - '20:00', - '21:00', - '22:00' -] -const trendData = [90, 80, 72, 75, 74, 85, 105, 116, 120, 112, 123, 127, 122, 146, 156.8, 147, 116, 94, 88, 102, 103, 116, 106] -const regionData = [ - { name: '成型系统', value: 2139.09 }, - { name: '蛋托线01', value: 0 }, - { name: '烘干系统', value: 0 } -] + +const trendXAxis = ref([]) +const trendSeries = ref([]) +const regionItems = ref<{ name: string; value: number; percent: string }[]>([]) +const regionTotal = ref('0') const trendChartOptions = reactive({ color: ['#2f7df6'], @@ -280,12 +268,12 @@ const trendChartOptions = reactive({ trigger: 'axis', formatter: (params: any) => { const item = params?.[0] - return `${item.axisValue}
用电量:${item.data} kWh` + return `${item?.axisValue || ''}
${t('EnergyOverview.chart.usage')}:${item?.data || 0} ${selectedEnergyUnit.value}` } }, legend: { top: 4, - data: ['用电量 (kWh)'] + data: [t('EnergyOverview.chart.usage')] }, grid: { left: 24, @@ -297,25 +285,23 @@ const trendChartOptions = reactive({ xAxis: { type: 'category', boundaryGap: false, - name: '时间(小时)', + name: t('EnergyOverview.chart.time'), nameLocation: 'middle', nameGap: 28, - data: hours, + data: trendXAxis.value, axisLine: { lineStyle: { color: '#dcdfe6' } }, axisLabel: { color: '#606266' } }, yAxis: { type: 'value', - name: 'kWh', + name: selectedEnergyUnit.value, min: 0, - max: 180, - interval: 30, axisLabel: { color: '#606266' }, splitLine: { lineStyle: { color: '#e8edf5' } } }, series: [ { - name: '用电量 (kWh)', + name: t('EnergyOverview.chart.usage'), type: 'line', smooth: true, symbol: 'circle', @@ -334,21 +320,16 @@ const trendChartOptions = reactive({ ] } }, - data: trendData, - markPoint: { - symbolSize: 48, - label: { color: '#2f7df6', formatter: '{c}' }, - data: [{ type: 'max', name: '峰值' }] - } + data: trendSeries.value } ] -}) as EChartsOption +}) const regionPieOptions = reactive({ - color: ['#2f7df6', '#52c41a', '#faad14'], + color: ['#2f7df6', '#52c41a', '#faad14', '#13c2c2', '#fa8c16'], tooltip: { trigger: 'item', - formatter: '{b}
{c} kWh ({d}%)' + formatter: `{b}
{c} ${selectedEnergyUnit.value} ({d}%)` }, legend: { orient: 'vertical', @@ -357,15 +338,15 @@ const regionPieOptions = reactive({ itemWidth: 12, itemHeight: 12, formatter: (name: string) => { - const item = regionData.find((row) => row.name === name) + const item = regionItems.value.find((row) => row.name === name) const value = item?.value ?? 0 - const percent = value > 0 ? '100.00' : '0.00' - return `${name} ${value.toLocaleString()} kWh (${percent}%)` + const percent = item?.percent ?? '0.00' + return `${name} ${value.toLocaleString()} ${selectedEnergyUnit.value} (${percent}%)` } }, series: [ { - name: '区域能耗', + name: t('EnergyOverview.chart.regionEnergy'), type: 'pie', radius: ['58%', '76%'], center: ['34%', '50%'], @@ -373,7 +354,7 @@ const regionPieOptions = reactive({ label: { show: true, position: 'center', - formatter: '总用电量\n{total|2,139.09 kWh}', + formatter: `${t('EnergyOverview.chart.totalUsage')}\n{total|${regionTotal.value} ${selectedEnergyUnit.value}}`, color: '#303133', rich: { total: { @@ -385,116 +366,145 @@ const regionPieOptions = reactive({ } }, labelLine: { show: false }, - data: regionData + data: regionItems.value } ] -}) as EChartsOption +}) -const buildMetrics = () => { - metricCards.value = [ - { - key: 'total', - label: '总用电量', - value: '2,139.09', - unit: selectedEnergyUnit.value, - icon: 'ep:lightning', - theme: 'blue', - subLabel: '较昨日', - change: '12.53%' - }, - { - key: 'today', - label: '今日用电量', - value: '860.32', - unit: selectedEnergyUnit.value, - icon: 'ep:calendar', - theme: 'green', - subLabel: '较昨日', - change: '12.15%' - }, - { - key: 'peak', - label: '峰值用电', - value: '156.8', - unit: selectedEnergyUnit.value, - icon: 'ep:histogram', - theme: 'purple', - subLabel: '时间:', - subValue: '2026-05-04 14:00 ~ 15:00' - }, - { - key: 'unit', - label: '单位产量能耗', - value: '0.38', - unit: selectedUnitConsumptionUnit.value, - icon: 'ep:orange', - theme: 'orange', - subLabel: '较昨日', - change: '5.21%', - down: true - }, - { - key: 'region', - label: '最大耗能区域', - value: '成型系统', - unit: '', - icon: 'ep:pie-chart', - theme: 'cyan', - subLabel: '占比:', - subValue: '100.00%' - } - ] +const metricThemeMap: Record = { + total: { icon: 'ep:lightning', theme: 'blue' }, + deviceCount: { icon: 'ep:cpu', theme: 'green' }, + topDevice: { icon: 'ep:histogram', theme: 'purple' }, + topRegion: { icon: 'ep:pie-chart', theme: 'cyan' }, + range: { icon: 'ep:calendar', theme: 'orange' } } -const buildDetails = () => { +const buildFallbackTrendXAxis = () => { const timeRange = queryParams.timeRange?.length === 2 ? queryParams.timeRange : defaultTimeRange - detailList.value = [ - { - id: 1, - name: '蛋托线真空柜用电量', - energyType: selectedEnergyName.value, - region: '蛋托线01', - value: '0', - startTime: timeRange[0], - endTime: timeRange[1] - }, - { - id: 2, - name: '蛋托成型柜用电量', - energyType: selectedEnergyName.value, - region: '成型系统', - value: '2,139.09', - startTime: timeRange[0], - endTime: timeRange[1] - }, - { - id: 3, - name: '蛋托干燥柜用电量', - energyType: selectedEnergyName.value, - region: '烘干系统', - value: '0', - startTime: timeRange[0], - endTime: timeRange[1] + const [startTime, endTime] = timeRange + const start = new Date(startTime.replace(/-/g, '/')) + const end = new Date(endTime.replace(/-/g, '/')) + if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime()) || start > end) { + return [] + } + + const sameDay = + start.getFullYear() === end.getFullYear() && + start.getMonth() === end.getMonth() && + start.getDate() === end.getDate() + + const axis: string[] = [] + if (sameDay) { + const current = new Date(start) + current.setMinutes(0, 0, 0) + const limit = new Date(end) + limit.setMinutes(0, 0, 0) + while (current <= limit) { + axis.push(`${String(current.getHours()).padStart(2, '0')}:00`) + current.setHours(current.getHours() + 1) } - ] - total.value = detailList.value.length + return axis + } + + const current = new Date(start) + current.setHours(0, 0, 0, 0) + const limit = new Date(end) + limit.setHours(0, 0, 0, 0) + while (current <= limit) { + const year = current.getFullYear() + const month = String(current.getMonth() + 1).padStart(2, '0') + const day = String(current.getDate()).padStart(2, '0') + axis.push(`${year}-${month}-${day}`) + current.setDate(current.getDate() + 1) + } + return axis +} + +const normalizeMetricCards = (metrics: EnergyOverviewMetricVO[]) => { + metricCards.value = metrics.map((item) => ({ + key: item.key, + label: item.label, + value: item.value, + unit: item.unit || '', + icon: metricThemeMap[item.key]?.icon || 'ep:data-line', + theme: metricThemeMap[item.key]?.theme || 'blue', + subLabel: item.subLabel || '', + subValue: item.subValue || '', + change: item.change, + down: item.down + })) +} + +const updateCharts = (data: EnergyOverviewRespVO) => { + const fallbackXAxis = buildFallbackTrendXAxis() + trendXAxis.value = data.trendChart?.xAxis?.length ? data.trendChart.xAxis : fallbackXAxis + const responseSeries = (data.trendChart?.data || []).map((item) => Number(item) || 0) + trendSeries.value = + responseSeries.length > 0 + ? responseSeries + : new Array(trendXAxis.value.length).fill(0) + regionItems.value = (data.regionChart?.items || []).map((item) => ({ + name: item.name, + value: Number(item.value) || 0, + percent: item.percent + })) + regionTotal.value = data.regionChart?.totalValue || '0' + + trendChartOptions.xAxis = { + ...(trendChartOptions.xAxis as any), + data: trendXAxis.value, + name: t('EnergyOverview.chart.time') + } + trendChartOptions.yAxis = { + ...(trendChartOptions.yAxis as any), + name: selectedEnergyUnit.value + } + if (Array.isArray(trendChartOptions.legend?.data)) { + trendChartOptions.legend.data = [t('EnergyOverview.chart.usage')] + } + if (Array.isArray(trendChartOptions.series) && trendChartOptions.series[0]) { + ;(trendChartOptions.series[0] as any).name = t('EnergyOverview.chart.usage') + ;(trendChartOptions.series[0] as any).data = trendSeries.value + } + if (Array.isArray(regionPieOptions.series) && regionPieOptions.series[0]) { + ;(regionPieOptions.series[0] as any).name = t('EnergyOverview.chart.regionEnergy') + ;(regionPieOptions.series[0] as any).data = regionItems.value + ;(regionPieOptions.series[0] as any).label = { + ...(regionPieOptions.series[0] as any).label, + formatter: `${t('EnergyOverview.chart.totalUsage')}\n{total|${regionTotal.value} ${selectedEnergyUnit.value}}` + } + } } -const getList = () => { +const getList = async () => { if (!hasEnergyTypes.value) { metricCards.value = [] - detailList.value = [] + rankList.value = [] pageDetailList.value = [] total.value = 0 return } loading.value = true - buildMetrics() - buildDetails() - const start = (queryParams.pageNo - 1) * queryParams.pageSize - pageDetailList.value = detailList.value.slice(start, start + queryParams.pageSize) - loading.value = false + try { + const timeRange = queryParams.timeRange?.length === 2 ? queryParams.timeRange : defaultTimeRange + const res = await EnergyDeviceApi.queryOverviewData({ + orgId: queryParams.orgId, + energyTypeId: queryParams.energyTypeId, + startTime: timeRange[0], + endTime: timeRange[1], + pageNo: queryParams.pageNo, + pageSize: queryParams.pageSize + }) + const data = ((res as any)?.data || res) as EnergyOverviewRespVO + normalizeMetricCards(data.metrics || []) + rankList.value = data.rankList || [] + pageDetailList.value = data.detailList || [] + total.value = data.total || 0 + updateCharts(data) + } finally { + loading.value = false + } } const normalizeEnergyTypeList = (data: unknown): EnergyTypeVO[] => { @@ -508,31 +518,33 @@ const normalizeEnergyTypeList = (data: unknown): EnergyTypeVO[] => { return [] } -const getDefaultEnergyType = (list: EnergyTypeVO[]) => { - return ( - list.find((item) => item.name === '电') || - list.find((item) => item.name === '水') || - list.find((item) => item.name === '气') || - list[0] - ) -} +const getDefaultEnergyType = (list: EnergyTypeVO[]) => list[0] const getEnergyTypes = async () => { try { - const data = await EnergyTypeApi.getEnergyTypeList() + const data = await EnergyTypeApi.getEnergyTypeList({ orgId: queryParams.orgId }) energyTypeOptions.value = normalizeEnergyTypeList(data) } catch (error) { energyTypeOptions.value = [] } - const defaultEnergyType = getDefaultEnergyType(energyTypeOptions.value) + let defaultEnergyType = getDefaultEnergyType(energyTypeOptions.value) + try { + const deviceRes = await EnergyDeviceApi.getList({ orgId: queryParams.orgId }) + const devices = ((deviceRes as any)?.data || deviceRes || []) as Array<{ deviceTypeId?: number }> + const deviceTypeIds = new Set(devices.map((item) => item.deviceTypeId).filter(Boolean)) + defaultEnergyType = + energyTypeOptions.value.find((item) => deviceTypeIds.has(item.id)) || defaultEnergyType + } catch (error) { + // ignore and use the first energy type + } defaultEnergyTypeId.value = defaultEnergyType?.id queryParams.energyTypeId = defaultEnergyType?.id } const handleQuery = () => { - if (!hasEnergyTypes.value) { - getList() + if (!isSingleDayRange(queryParams.timeRange)) { + message.warning(t('EnergyOverview.messages.singleDayOnly')) return } queryParams.pageNo = 1 @@ -543,20 +555,51 @@ const resetQuery = () => { queryFormRef.value?.resetFields() queryParams.orgId = undefined queryParams.energyTypeId = defaultEnergyTypeId.value - queryParams.timeRange = [...defaultTimeRange] + queryParams.timeRange = getTodayTimeRange() handleQuery() } -const handleExport = () => { - message.info('示例页暂未接入导出接口') +const handleExport = async () => { + if (!isSingleDayRange(queryParams.timeRange)) { + message.warning(t('EnergyOverview.messages.singleDayOnly')) + return + } + const timeRange = queryParams.timeRange?.length === 2 ? queryParams.timeRange : defaultTimeRange + await EnergyDeviceApi.exportQueryDataRecords({ + orgId: queryParams.orgId, + startTime: timeRange[0], + endTime: timeRange[1] + }) + message.success(t('EnergyOverview.exportSuccess')) +} + +const loadOrgOptions = async () => { + if (orgOptions.value.length) return + const data: any = await OrganizationApi.getOrganizationList({}) + const rows = (Array.isArray(data) ? data : data?.list ?? data?.data ?? []) as OrganizationVO[] + orgOptions.value = rows + .filter((r) => r?.id !== undefined && r?.name) + .map((r) => ({ label: r.name, value: r.id, raw: r })) } onMounted(async () => { + await loadOrgOptions() await getEnergyTypes() if (hasEnergyTypes.value) { getList() } }) + +watch( + () => queryParams.orgId, + async () => { + await getEnergyTypes() + queryParams.pageNo = 1 + if (hasEnergyTypes.value) { + getList() + } + } +)