feat:总览-能源模块
parent
c4ff5f807c
commit
ed30527d2d
@ -0,0 +1,33 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
export function getEnergyTypeList(params = {}) {
|
||||
return request({
|
||||
url: '/admin-api/mes/energy-type/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getOrganizationList(params = {}) {
|
||||
return request({
|
||||
url: '/admin-api/mes/organization/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function getEnergyDeviceList(params = {}) {
|
||||
return request({
|
||||
url: '/admin-api/mes/energy-device/getList',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function queryEnergyOverviewData(params = {}) {
|
||||
return request({
|
||||
url: '/admin-api/mes/energy-device/queryOverviewData',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,939 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<NavBar title="能源总览" />
|
||||
|
||||
<view class="page-body">
|
||||
<view class="filter-bar">
|
||||
<view class="filter-row quick-row">
|
||||
<picker :range="orgLabels" :value="orgIndex" @change="onOrgChange">
|
||||
<view class="picker-field">
|
||||
<text class="picker-text">{{ selectedOrgLabel }}</text>
|
||||
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
<picker :range="energyTypeLabels" :value="energyTypeIndex" @change="onEnergyTypeChange">
|
||||
<view class="picker-field">
|
||||
<text class="picker-text">{{ selectedEnergyTypeName }}</text>
|
||||
<uni-icons type="bottom" size="14" color="#a8adb7"></uni-icons>
|
||||
</view>
|
||||
</picker>
|
||||
<view class="icon-filter-btn" @click="refreshData">
|
||||
<uni-icons type="refresh" size="24" color="#7b8491"></uni-icons>
|
||||
</view>
|
||||
<view class="icon-filter-btn" @click="openFilterDrawer">
|
||||
<uni-icons type="settings" size="24" color="#7b8491"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="content-wrap">
|
||||
<view v-if="!hasEnergyTypes && !loading" class="empty-card">暂无能源类型</view>
|
||||
|
||||
<template v-else>
|
||||
<view class="metric-grid">
|
||||
<view v-for="item in metricCards" :key="item.key" class="metric-card">
|
||||
<view class="metric-icon" :class="item.theme">
|
||||
<uni-icons :type="item.icon" size="24" :color="item.color"></uni-icons>
|
||||
</view>
|
||||
<view class="metric-content">
|
||||
<text class="metric-label">{{ item.label }}</text>
|
||||
<view class="metric-value-row">
|
||||
<text class="metric-value">{{ item.value }}</text>
|
||||
<text v-if="item.unit" class="metric-unit">{{ item.unit }}</text>
|
||||
</view>
|
||||
<view class="metric-sub">
|
||||
<text class="sub-label">{{ item.subLabel }}</text>
|
||||
<text v-if="item.change" :class="['metric-change', item.down ? 'down' : 'up']">
|
||||
{{ item.down ? '↓' : '↑' }} {{ item.change }}
|
||||
</text>
|
||||
<text v-else class="sub-value">{{ item.subValue }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-card">
|
||||
<view class="panel-title">能源用量趋势</view>
|
||||
<view class="chart-box">
|
||||
<qiun-data-charts type="line" :chartData="trendChartData" :canvas2d="false" :opts="trendChartOpts" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-card">
|
||||
<view class="panel-title">区域能耗占比</view>
|
||||
<view class="region-layout">
|
||||
<view class="ring-box">
|
||||
<qiun-data-charts type="ring" :chartData="regionChartData" :canvas2d="false" :opts="regionChartOpts" />
|
||||
</view>
|
||||
<view class="region-legend">
|
||||
<view v-for="item in regionItems" :key="item.name" class="region-row">
|
||||
<view class="region-left">
|
||||
<view class="region-dot" :style="{ background: item.color }"></view>
|
||||
<text class="region-name">{{ item.name }}</text>
|
||||
</view>
|
||||
<text class="region-value">{{ item.value }} {{ selectedEnergyUnit }} ({{ item.percent }}%)</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel-card">
|
||||
<view class="panel-title">能耗排行 TOP5</view>
|
||||
<view v-if="!rankList.length" class="empty-text">暂无排行数据</view>
|
||||
<view v-for="(item, index) in rankList" :key="item.id || item.name" class="rank-row">
|
||||
<view :class="['rank-index', `rank-${index + 1}`]">{{ index + 1 }}</view>
|
||||
<view class="rank-main">
|
||||
<text class="rank-name">{{ textValue(item.name) }}</text>
|
||||
<text class="rank-region">{{ textValue(item.region) }}</text>
|
||||
</view>
|
||||
<text class="rank-value">{{ textValue(item.value) }} {{ selectedEnergyUnit }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<view v-if="loading" class="loading-mask">{{ t('functionCommon.loading') }}</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<uni-popup ref="filterPopupRef" class="energy-filter-popup" type="right" background-color="transparent" :animation="false">
|
||||
<view class="filter-drawer">
|
||||
<view class="drawer-header">
|
||||
<text class="drawer-title">更多筛选</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="drawer-body">
|
||||
<view class="drawer-section drawer-fields">
|
||||
<view class="drawer-field drawer-field-wide">
|
||||
<text class="drawer-label">时间范围</text>
|
||||
<view class="drawer-date">
|
||||
<uni-datetime-picker
|
||||
v-model="timeRange"
|
||||
type="datetimerange"
|
||||
:clear-icon="true"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="drawer-actions">
|
||||
<view class="drawer-action reset" @click="resetFilters">重置</view>
|
||||
<view class="drawer-action confirm" @click="confirmFilterDrawer">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</uni-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import NavBar from '@/components/common/NavBar.vue'
|
||||
import {
|
||||
getEnergyDeviceList,
|
||||
getEnergyTypeList,
|
||||
getOrganizationList,
|
||||
queryEnergyOverviewData
|
||||
} from '@/api/mes/energyOverview'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const loading = ref(false)
|
||||
const organizationOptions = ref([])
|
||||
const selectedOrgId = ref('')
|
||||
const timeRange = ref(getTodayTimeRange())
|
||||
const filterPopupRef = ref(null)
|
||||
const energyTypeOptions = ref([])
|
||||
const energyDeviceList = ref([])
|
||||
const selectedEnergyTypeId = ref('')
|
||||
const metricCards = ref([])
|
||||
const rankList = ref([])
|
||||
const regionItems = ref([])
|
||||
|
||||
const trendChartData = reactive({
|
||||
categories: [],
|
||||
series: [{ name: '用量', data: [] }]
|
||||
})
|
||||
|
||||
const regionChartData = reactive({
|
||||
series: [{ data: [] }]
|
||||
})
|
||||
|
||||
const trendChartOpts = {
|
||||
color: ['#2f7df6'],
|
||||
dataLabel: false,
|
||||
dataPointShape: true,
|
||||
legend: { show: false },
|
||||
xAxis: { disableGrid: true, labelCount: 5 },
|
||||
yAxis: { gridType: 'dash', dashLength: 2 },
|
||||
extra: { line: { type: 'curve', width: 2, activeType: 'hollow' } }
|
||||
}
|
||||
|
||||
const regionChartOpts = {
|
||||
color: ['#2f7df6', '#52c41a', '#faad14', '#13c2c2', '#fa8c16'],
|
||||
dataLabel: false,
|
||||
legend: { show: false },
|
||||
title: { name: '总用量', fontSize: 12, color: '#64748b' },
|
||||
subtitle: { name: '', fontSize: 13, color: '#0f172a' },
|
||||
extra: {
|
||||
ring: {
|
||||
ringWidth: 22,
|
||||
activeOpacity: 0.5,
|
||||
activeRadius: 6,
|
||||
offsetAngle: 0,
|
||||
labelWidth: 15,
|
||||
border: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const metricConfig = {
|
||||
total: { icon: 'fire-filled', theme: 'blue', color: '#2f7df6' },
|
||||
deviceCount: { icon: 'gear-filled', theme: 'green', color: '#22c55e' },
|
||||
topDevice: { icon: 'flag-filled', theme: 'purple', color: '#8b5cf6' },
|
||||
topRegion: { icon: 'map-filled', theme: 'cyan', color: '#06b6d4' },
|
||||
range: { icon: 'calendar-filled', theme: 'orange', color: '#f59e0b' }
|
||||
}
|
||||
|
||||
const orgPickerOptions = computed(() => [
|
||||
{ id: '', name: '\u5168\u90e8\u533a\u57df' },
|
||||
...organizationOptions.value
|
||||
])
|
||||
const orgLabels = computed(() => orgPickerOptions.value.map((item) => item.name || '-'))
|
||||
const orgIndex = computed(() => {
|
||||
const idx = orgPickerOptions.value.findIndex((item) => String(item.id) === String(selectedOrgId.value))
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
const selectedOrgLabel = computed(() => {
|
||||
const found = orgPickerOptions.value.find((item) => String(item.id) === String(selectedOrgId.value))
|
||||
return found?.name || '\u5168\u90e8\u533a\u57df'
|
||||
})
|
||||
const hasEnergyTypes = computed(() => energyTypeOptions.value.length > 0)
|
||||
const selectedEnergyType = computed(() =>
|
||||
energyTypeOptions.value.find((item) => String(item.id) === String(selectedEnergyTypeId.value))
|
||||
)
|
||||
const selectedEnergyUnit = computed(() => selectedEnergyType.value?.unit || 'kWh')
|
||||
const selectedEnergyTypeName = computed(() => selectedEnergyType.value?.name || '请选择能源类型')
|
||||
const energyTypeLabels = computed(() => energyTypeOptions.value.map((item) => item.name || '-'))
|
||||
const energyTypeIndex = computed(() => {
|
||||
const idx = energyTypeOptions.value.findIndex((item) => String(item.id) === String(selectedEnergyTypeId.value))
|
||||
return idx >= 0 ? idx : 0
|
||||
})
|
||||
|
||||
onLoad(async () => {
|
||||
await initPage()
|
||||
})
|
||||
|
||||
onPullDownRefresh(async () => {
|
||||
await refreshData()
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
|
||||
async function initPage() {
|
||||
loading.value = true
|
||||
try {
|
||||
await loadOrganizations()
|
||||
await loadEnergyTypes()
|
||||
await refreshData(false)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshData(showLoading = true) {
|
||||
if (showLoading) loading.value = true
|
||||
try {
|
||||
if (!hasEnergyTypes.value) {
|
||||
resetOverview()
|
||||
return
|
||||
}
|
||||
await loadEnergyDevices()
|
||||
const range = normalizeTimeRange(timeRange.value)
|
||||
const res = await queryEnergyOverviewData({
|
||||
orgId: selectedOrgId.value || undefined,
|
||||
energyTypeId: selectedEnergyTypeId.value,
|
||||
startTime: range[0],
|
||||
endTime: range[1],
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
const data = normalizeOverviewData(res)
|
||||
setMetricCards(data.metrics)
|
||||
updateTrendChart(data.trendChart)
|
||||
updateRegionChart(data.regionChart)
|
||||
rankList.value = (data.rankList || []).slice(0, 5)
|
||||
} catch (error) {
|
||||
uni.showToast({ title: t('functionCommon.loadFailed'), icon: 'none' })
|
||||
setFallbackOverview()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOrganizations() {
|
||||
try {
|
||||
const res = await getOrganizationList({})
|
||||
organizationOptions.value = normalizeList(res)
|
||||
} catch (error) {
|
||||
organizationOptions.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEnergyTypes() {
|
||||
try {
|
||||
const res = await getEnergyTypeList({ orgId: selectedOrgId.value || undefined })
|
||||
const list = normalizeList(res)
|
||||
energyTypeOptions.value = list
|
||||
const currentExists = list.some((item) => String(item.id) === String(selectedEnergyTypeId.value))
|
||||
if (!currentExists) {
|
||||
selectedEnergyTypeId.value = list[0]?.id || ''
|
||||
}
|
||||
} catch (error) {
|
||||
energyTypeOptions.value = []
|
||||
selectedEnergyTypeId.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function loadEnergyDevices() {
|
||||
try {
|
||||
const res = await getEnergyDeviceList({ orgId: selectedOrgId.value || undefined })
|
||||
energyDeviceList.value = normalizeList(res)
|
||||
} catch (error) {
|
||||
energyDeviceList.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeOverviewData(res) {
|
||||
const root = res && res.data !== undefined ? res.data : res
|
||||
return {
|
||||
metrics: Array.isArray(root?.metrics) ? root.metrics : [],
|
||||
trendChart: root?.trendChart || {},
|
||||
regionChart: root?.regionChart || {},
|
||||
rankList: Array.isArray(root?.rankList) ? root.rankList : []
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeList(res) {
|
||||
const root = res && res.data !== undefined ? res.data : res
|
||||
if (Array.isArray(root)) return root
|
||||
if (Array.isArray(root?.data)) return root.data
|
||||
if (Array.isArray(root?.list)) return root.list
|
||||
if (Array.isArray(root?.records)) return root.records
|
||||
return []
|
||||
}
|
||||
|
||||
function setMetricCards(metrics = []) {
|
||||
if (metrics.length) {
|
||||
metricCards.value = metrics
|
||||
.filter((item) => ['total', 'deviceCount', 'topDevice', 'topRegion'].includes(item.key))
|
||||
.map((item) => normalizeMetricCard(item))
|
||||
return
|
||||
}
|
||||
metricCards.value = buildFallbackMetricCards()
|
||||
}
|
||||
|
||||
function normalizeMetricCard(item) {
|
||||
const config = metricConfig[item.key] || metricConfig.total
|
||||
return {
|
||||
key: item.key,
|
||||
label: item.label || metricLabel(item.key),
|
||||
value: textValue(item.value),
|
||||
unit: item.unit || '',
|
||||
icon: config.icon,
|
||||
theme: config.theme,
|
||||
color: config.color,
|
||||
subLabel: item.subLabel || '',
|
||||
subValue: item.subValue || '',
|
||||
change: item.change || '',
|
||||
down: Boolean(item.down)
|
||||
}
|
||||
}
|
||||
|
||||
function buildFallbackMetricCards() {
|
||||
const unit = selectedEnergyUnit.value
|
||||
const deviceCount = energyDeviceList.value.length
|
||||
return [
|
||||
normalizeMetricCard({ key: 'total', label: '总用量', value: '0', unit, subLabel: '今日累计', subValue: '0' }),
|
||||
normalizeMetricCard({ key: 'deviceCount', label: '统计设备', value: String(deviceCount), unit: '台', subLabel: '当前能源类型', subValue: selectedEnergyTypeName.value }),
|
||||
normalizeMetricCard({ key: 'topDevice', label: '最高能耗设备', value: '-', unit: '', subLabel: '设备', subValue: '-' }),
|
||||
normalizeMetricCard({ key: 'topRegion', label: '最高能耗区域', value: '-', unit: '', subLabel: '区域', subValue: '-' })
|
||||
]
|
||||
}
|
||||
|
||||
function updateTrendChart(trend = {}) {
|
||||
const categories = Array.isArray(trend.xAxis) && trend.xAxis.length ? trend.xAxis : buildTodayHours()
|
||||
const data = Array.isArray(trend.data) && trend.data.length
|
||||
? trend.data.map((item) => Number(item) || 0)
|
||||
: categories.map(() => 0)
|
||||
trendChartData.categories = categories
|
||||
trendChartData.series = [{ name: '用量', data }]
|
||||
}
|
||||
|
||||
function updateRegionChart(region = {}) {
|
||||
const colors = regionChartOpts.color
|
||||
const items = Array.isArray(region.items) ? region.items : []
|
||||
regionItems.value = items.map((item, index) => ({
|
||||
name: item.name || '-',
|
||||
value: textValue(item.value),
|
||||
percent: textValue(item.percent),
|
||||
color: colors[index % colors.length]
|
||||
}))
|
||||
regionChartData.series = [{
|
||||
data: regionItems.value.map((item) => ({
|
||||
name: item.name,
|
||||
value: Number(item.value) || 0
|
||||
}))
|
||||
}]
|
||||
regionChartOpts.subtitle.name = `${textValue(region.totalValue || 0)} ${selectedEnergyUnit.value}`
|
||||
}
|
||||
|
||||
function setFallbackOverview() {
|
||||
metricCards.value = buildFallbackMetricCards()
|
||||
updateTrendChart({})
|
||||
updateRegionChart({})
|
||||
rankList.value = []
|
||||
}
|
||||
|
||||
function resetOverview() {
|
||||
metricCards.value = []
|
||||
rankList.value = []
|
||||
regionItems.value = []
|
||||
trendChartData.categories = []
|
||||
trendChartData.series = [{ name: '用量', data: [] }]
|
||||
regionChartData.series = [{ data: [] }]
|
||||
}
|
||||
|
||||
async function onOrgChange(e) {
|
||||
const item = orgPickerOptions.value[Number(e?.detail?.value || 0)]
|
||||
if (!item) return
|
||||
selectedOrgId.value = item.id || ''
|
||||
selectedEnergyTypeId.value = ''
|
||||
await loadEnergyTypes()
|
||||
await refreshData()
|
||||
}
|
||||
|
||||
async function onEnergyTypeChange(e) {
|
||||
const item = energyTypeOptions.value[Number(e?.detail?.value || 0)]
|
||||
if (!item) return
|
||||
selectedEnergyTypeId.value = item.id
|
||||
await refreshData()
|
||||
}
|
||||
|
||||
function openFilterDrawer() {
|
||||
filterPopupRef.value?.open()
|
||||
}
|
||||
|
||||
function closeFilterDrawer() {
|
||||
filterPopupRef.value?.close()
|
||||
}
|
||||
|
||||
async function confirmFilterDrawer() {
|
||||
closeFilterDrawer()
|
||||
await refreshData()
|
||||
}
|
||||
|
||||
async function resetFilters() {
|
||||
selectedOrgId.value = ''
|
||||
selectedEnergyTypeId.value = ''
|
||||
timeRange.value = getTodayTimeRange()
|
||||
await loadEnergyTypes()
|
||||
closeFilterDrawer()
|
||||
await refreshData()
|
||||
}
|
||||
|
||||
function 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`]
|
||||
}
|
||||
|
||||
function normalizeTimeRange(value) {
|
||||
return Array.isArray(value) && value.length === 2 && value[0] && value[1]
|
||||
? value
|
||||
: getTodayTimeRange()
|
||||
}
|
||||
|
||||
function buildTodayHours() {
|
||||
return Array.from({ length: 24 }).map((_, index) => `${String(index).padStart(2, '0')}:00`)
|
||||
}
|
||||
|
||||
function metricLabel(key) {
|
||||
const map = {
|
||||
total: '总用量',
|
||||
deviceCount: '统计设备',
|
||||
topDevice: '最高能耗设备',
|
||||
topRegion: '最高能耗区域'
|
||||
}
|
||||
return map[key] || '-'
|
||||
}
|
||||
|
||||
function textValue(value) {
|
||||
if (value === 0) return '0'
|
||||
if (value === null || value === undefined) return '-'
|
||||
const text = String(value).trim()
|
||||
return text || '-'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-container {
|
||||
min-height: 100vh;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
padding: 18rpx 20rpx;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.quick-row>picker {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.picker-field {
|
||||
height: 66rpx;
|
||||
padding: 0 18rpx 0 24rpx;
|
||||
border: 1rpx solid #d9dde5;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.picker-text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: #374151;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.icon-filter-btn {
|
||||
width: 66rpx;
|
||||
height: 66rpx;
|
||||
flex: 0 0 66rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1rpx solid transparent;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.page-body {
|
||||
padding-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 18rpx;
|
||||
}
|
||||
|
||||
.metric-card,
|
||||
.panel-card,
|
||||
.empty-card {
|
||||
border-radius: 8rpx;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 6rpx 20rpx rgba(15, 23, 42, 0.06);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
min-width: 0;
|
||||
min-height: 178rpx;
|
||||
padding: 20rpx;
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.metric-icon {
|
||||
width: 62rpx;
|
||||
height: 62rpx;
|
||||
border-radius: 8rpx;
|
||||
flex: 0 0 62rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.metric-icon.blue {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.metric-icon.green {
|
||||
background: #ecfdf3;
|
||||
}
|
||||
|
||||
.metric-icon.purple {
|
||||
background: #f5f3ff;
|
||||
}
|
||||
|
||||
.metric-icon.cyan {
|
||||
background: #ecfeff;
|
||||
}
|
||||
|
||||
.metric-icon.orange {
|
||||
background: #fffbeb;
|
||||
}
|
||||
|
||||
.metric-content {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.25;
|
||||
color: #475569;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.metric-value-row {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
min-width: 0;
|
||||
font-size: 34rpx;
|
||||
line-height: 1;
|
||||
color: #0f172a;
|
||||
font-weight: 800;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metric-unit {
|
||||
margin-left: 5rpx;
|
||||
font-size: 22rpx;
|
||||
color: #475569;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.metric-sub {
|
||||
margin-top: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sub-label,
|
||||
.sub-value,
|
||||
.metric-change {
|
||||
font-size: 22rpx;
|
||||
line-height: 1.2;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.sub-value {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.metric-change.up {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.metric-change.down {
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
.panel-card {
|
||||
margin-top: 18rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 30rpx;
|
||||
line-height: 1.3;
|
||||
color: #0f172a;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.chart-box {
|
||||
height: 360rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
|
||||
.region-layout {
|
||||
margin-top: 18rpx;
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ring-box {
|
||||
width: 260rpx;
|
||||
height: 260rpx;
|
||||
flex: 0 0 260rpx;
|
||||
}
|
||||
|
||||
.region-legend {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
}
|
||||
|
||||
.region-row {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10rpx;
|
||||
}
|
||||
|
||||
.region-left {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9rpx;
|
||||
}
|
||||
|
||||
.region-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
flex: 0 0 16rpx;
|
||||
}
|
||||
|
||||
.region-name {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.region-value {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 23rpx;
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rank-row {
|
||||
min-width: 0;
|
||||
min-height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18rpx;
|
||||
border-bottom: 1rpx solid #edf0f5;
|
||||
}
|
||||
|
||||
.rank-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.rank-index {
|
||||
width: 42rpx;
|
||||
height: 42rpx;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb;
|
||||
color: #475569;
|
||||
font-size: 23rpx;
|
||||
font-weight: 800;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 42rpx;
|
||||
}
|
||||
|
||||
.rank-1 {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.rank-2 {
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
.rank-3 {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.rank-main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-size: 26rpx;
|
||||
color: #111827;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rank-region {
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rank-value {
|
||||
max-width: 210rpx;
|
||||
text-align: right;
|
||||
font-size: 25rpx;
|
||||
color: #0f172a;
|
||||
font-weight: 800;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.empty-card,
|
||||
.empty-text,
|
||||
.loading-mask {
|
||||
padding: 34rpx 0;
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.filter-drawer {
|
||||
width: 630rpx;
|
||||
height: calc(100vh - var(--status-bar-height));
|
||||
margin-top: var(--status-bar-height);
|
||||
background: #f5f5f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border-radius: 28rpx 0 0 28rpx;
|
||||
}
|
||||
|
||||
:deep(.energy-filter-popup.right .uni-popup__content-transition) {
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.drawer-header {
|
||||
height: 104rpx;
|
||||
padding: 18rpx 34rpx 0;
|
||||
background: #ffffff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drawer-title {
|
||||
color: #1f2937;
|
||||
font-size: 34rpx;
|
||||
line-height: 1.3;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.drawer-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding-bottom: 40rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drawer-section {
|
||||
margin-bottom: 18rpx;
|
||||
padding: 8rpx 28rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drawer-field {
|
||||
min-width: 0;
|
||||
min-height: 118rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
border-bottom: 1rpx solid #eceff3;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drawer-field:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.drawer-label {
|
||||
width: 150rpx;
|
||||
flex: 0 0 150rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.3;
|
||||
color: #4b5563;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.drawer-date {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.drawer-actions {
|
||||
padding: 18rpx 28rpx 34rpx;
|
||||
display: flex;
|
||||
gap: 18rpx;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drawer-action {
|
||||
flex: 1;
|
||||
height: 78rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.drawer-action.reset {
|
||||
background: #f3f4f6;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.drawer-action.confirm {
|
||||
background: #22486e;
|
||||
color: #ffffff;
|
||||
}
|
||||
</style>
|
||||
Loading…
Reference in New Issue