<template>
|
<div class="screen-page">
|
<el-row :gutter="10" class="kpi-row">
|
<el-col v-for="item in kpis" :key="item.label" :lg="4" :sm="12" :xs="24">
|
<kpi-card v-bind="item" />
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="10" class="mid-row">
|
<el-col :lg="14" :xs="24">
|
<panel-card title="参与趋势" sub="可在人数与投递次数之间切换">
|
<template #extra>
|
<cl-select-button v-model="trendKind" :options="trendOptions" small />
|
</template>
|
<v-chart :option="trendOption" autoresize class="chart" />
|
</panel-card>
|
</el-col>
|
<el-col :lg="10" :xs="24">
|
<panel-card title="碳积分流转" sub="第一版用流向,不做 Sankey">
|
<div class="flow">
|
<div class="flow__node is-in">
|
<span>发放</span>
|
<strong>{{ data.pointFlow.issued.toLocaleString() }}</strong>
|
</div>
|
<div class="flow__arrow">↓</div>
|
<div class="flow__split">
|
<div>
|
<span>转赠</span>
|
<strong>{{ data.pointFlow.transfer.toLocaleString() }}</strong>
|
</div>
|
<div>
|
<span>捐赠</span>
|
<strong>{{ data.pointFlow.donate.toLocaleString() }}</strong>
|
</div>
|
<div>
|
<span>商城消费</span>
|
<strong>{{ data.pointFlow.mall.toLocaleString() }}</strong>
|
</div>
|
</div>
|
<div class="flow__arrow">↓</div>
|
<div class="flow__node is-out">
|
<span>当前余额</span>
|
<strong>{{ data.pointFlow.balance.toLocaleString() }}</strong>
|
</div>
|
</div>
|
</panel-card>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="10">
|
<el-col :span="24">
|
<panel-card title="组织排行榜" sub="人均按参与人数;院系不统计参与率">
|
<template #extra>
|
<div class="rank-tools">
|
<cl-select-button v-model="rankOrg" :options="orgOptions" small />
|
<cl-select-button v-model="rankMetric" :options="metricOptions" small />
|
</div>
|
</template>
|
<v-chart :option="rankOption" autoresize class="chart chart--rank" />
|
</panel-card>
|
</el-col>
|
</el-row>
|
</div>
|
</template>
|
|
<script lang="ts" setup>
|
defineOptions({
|
name: 'report-screen-participation'
|
});
|
|
import { computed, ref } from 'vue';
|
import KpiCard from '../../components/kpi-card.vue';
|
import PanelCard from '../../components/panel-card.vue';
|
import { type RankMetric } from '../../data/screen';
|
import { useChartTheme, useScreenData } from '../../hooks/use-screen';
|
|
const { data } = useScreenData();
|
const { textColor, mutedColor, splitColor, primary, baseGrid, axisStyle, tooltip } = useChartTheme();
|
|
const trendKind = ref<'people' | 'times'>('people');
|
const rankOrg = ref<'class' | 'major' | 'dept'>('class');
|
const rankMetric = ref<RankMetric>('people');
|
|
const trendOptions = [
|
{ label: '每日参与人数', value: 'people' },
|
{ label: '每日投递次数', value: 'times' }
|
];
|
|
const orgOptions = [
|
{ label: '班级', value: 'class' },
|
{ label: '专业', value: 'major' },
|
{ label: '院系', value: 'dept' }
|
];
|
|
const metricOptions: { label: string; value: RankMetric }[] = [
|
{ label: '参与人数', value: 'people' },
|
{ label: '回收重量', value: 'weight' },
|
{ label: '人均回收量', value: 'weightPerCapita' },
|
{ label: '人均积分', value: 'pointPerCapita' },
|
{ label: '碳减排', value: 'carbon' }
|
];
|
|
const kpis = computed(() => [
|
{ label: '参与师生', value: data.value.kpis.totalParticipants, suffix: '人', fixed: 0 },
|
{ label: '参与率', value: data.value.kpis.participateRate, suffix: '%', fixed: 1, hint: '参与人数 / 参数 student.count.{部门ID}' },
|
{ label: '本区间新增参与', value: data.value.kpis.newParticipants, suffix: '人', fixed: 0, hint: '区间内出现过的去重用户' },
|
{ label: '碳积分发放', value: data.value.kpis.pointsIssued, suffix: '分', fixed: 0 },
|
{ label: '碳积分消费', value: data.value.kpis.pointsConsumed, suffix: '分', fixed: 0 },
|
{ label: '碳积分余额', value: data.value.kpis.pointsBalance, suffix: '分', fixed: 0 }
|
]);
|
|
const trendOption = computed(() => ({
|
grid: baseGrid(),
|
tooltip: { trigger: 'axis', ...tooltip() },
|
xAxis: { type: 'category', data: data.value.axis, ...axisStyle() },
|
yAxis: {
|
type: 'value',
|
splitLine: { lineStyle: { color: splitColor.value, type: 'dashed' } },
|
axisLabel: { color: mutedColor.value },
|
axisLine: { show: false },
|
axisTick: { show: false }
|
},
|
series: [
|
{
|
type: 'line',
|
smooth: true,
|
showSymbol: false,
|
data: trendKind.value === 'people' ? data.value.participateTrend : data.value.deliverTrend,
|
itemStyle: { color: primary.value },
|
areaStyle: { color: primary.value, opacity: 0.12 }
|
}
|
]
|
}));
|
|
const rankRows = computed(() => {
|
if (rankOrg.value === 'major') return data.value.majorRank;
|
if (rankOrg.value === 'dept') return data.value.deptRank;
|
return data.value.classRank;
|
});
|
|
const rankOption = computed(() => {
|
const rows = [...rankRows.value].reverse();
|
return {
|
grid: { containLabel: true, left: 8, right: 24, top: 12, bottom: 8 },
|
tooltip: { trigger: 'axis', ...tooltip() },
|
xAxis: {
|
type: 'value',
|
splitLine: { lineStyle: { color: splitColor.value, type: 'dashed' } },
|
axisLabel: { color: mutedColor.value },
|
axisLine: { show: false },
|
axisTick: { show: false }
|
},
|
yAxis: {
|
type: 'category',
|
data: rows.map(e => e.name),
|
axisLabel: { color: textColor.value },
|
axisLine: { show: false },
|
axisTick: { show: false }
|
},
|
series: [
|
{
|
type: 'bar',
|
barWidth: 14,
|
data: rows.map(e => Number((e as Record<string, number>)[rankMetric.value] || 0)),
|
itemStyle: { color: primary.value, borderRadius: [0, 8, 8, 0] }
|
}
|
]
|
};
|
});
|
</script>
|
|
<style lang="scss" scoped>
|
.screen-page {
|
.kpi-row,
|
.mid-row {
|
margin-bottom: 10px;
|
}
|
|
.kpi-row {
|
:deep(.el-col) {
|
margin-bottom: 10px;
|
}
|
}
|
|
.mid-row {
|
:deep(.panel-card) {
|
min-height: 360px;
|
}
|
}
|
|
.chart {
|
height: 300px;
|
width: 100%;
|
|
&--rank {
|
height: 340px;
|
}
|
}
|
|
.rank-tools {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 8px;
|
justify-content: flex-end;
|
}
|
|
.flow {
|
display: flex;
|
flex-direction: column;
|
align-items: stretch;
|
justify-content: center;
|
height: 300px;
|
padding: 8px 16px;
|
gap: 8px;
|
|
&__node,
|
&__split > div {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 12px 14px;
|
border-radius: 10px;
|
background: var(--el-fill-color-lighter);
|
font-size: 13px;
|
color: var(--el-text-color-secondary);
|
|
strong {
|
font-size: 20px;
|
color: var(--el-text-color-primary);
|
}
|
}
|
|
&__node.is-in strong {
|
color: var(--el-color-success);
|
}
|
|
&__node.is-out strong {
|
color: var(--el-color-primary);
|
}
|
|
&__split {
|
display: grid;
|
grid-template-columns: repeat(3, 1fr);
|
gap: 8px;
|
}
|
|
&__arrow {
|
text-align: center;
|
color: var(--el-text-color-secondary);
|
}
|
}
|
}
|
</style>
|