首页新增新增&日活报表

This commit is contained in:
eafonyang
2026-07-20 18:39:33 +08:00
parent d2a8ef2f2d
commit 3a0108dc54
26 changed files with 990 additions and 613 deletions
+16
View File
@@ -6,3 +6,19 @@ export function fetchGetDashboardToday() {
method: 'get'
});
}
export function fetchGetDeviceDaily(days: number = 30) {
return request<Api.Dashboard.DeviceDailyStats>({
url: '/dashboard/device-daily',
method: 'get',
params: { days }
});
}
export function fetchGetActiveDaily(days: number = 30) {
return request<Api.Dashboard.ActiveDailyStats>({
url: '/dashboard/active-daily',
method: 'get',
params: { days }
});
}
+20
View File
@@ -37,6 +37,26 @@ declare namespace Api {
}>;
[key: string]: unknown;
}
interface DeviceDailySeries {
name: string;
data: number[];
}
interface DeviceDailyStats {
dates: string[];
series: DeviceDailySeries[];
total: number;
}
interface ActiveDailyStats {
dates: string[];
series: DeviceDailySeries[];
latest: {
date: string;
total: number;
};
}
}
namespace User {
+4
View File
@@ -67,6 +67,8 @@ declare module 'vue' {
NModal: typeof import('naive-ui')['NModal']
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
NPagination: typeof import('naive-ui')['NPagination']
NRadioButton: typeof import('naive-ui')['NRadioButton']
NRadioGroup: typeof import('naive-ui')['NRadioGroup']
NScrollbar: typeof import('naive-ui')['NScrollbar']
NSelect: typeof import('naive-ui')['NSelect']
NSpace: typeof import('naive-ui')['NSpace']
@@ -152,6 +154,8 @@ declare global {
const NModal: typeof import('naive-ui')['NModal']
const NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
const NPagination: typeof import('naive-ui')['NPagination']
const NRadioButton: typeof import('naive-ui')['NRadioButton']
const NRadioGroup: typeof import('naive-ui')['NRadioGroup']
const NScrollbar: typeof import('naive-ui')['NScrollbar']
const NSelect: typeof import('naive-ui')['NSelect']
const NSpace: typeof import('naive-ui')['NSpace']
+186 -84
View File
@@ -1,9 +1,9 @@
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue';
import type { RouteKey } from '@elegant-router/types';
import { fetchGetDashboardToday } from '@/service/api';
import { useRouterPush } from '@/hooks/common/router';
import { computed, onMounted, ref, watch } from 'vue';
import { fetchGetDashboardToday, fetchGetDeviceDaily, fetchGetActiveDaily } from '@/service/api';
import { useAuthStore } from '@/store/modules/auth';
import { useEcharts } from '@/hooks/common/echarts';
import type { ECOption } from '@/hooks/common/echarts';
defineOptions({ name: 'Home' });
@@ -21,15 +21,7 @@ interface TodayOta {
create_at?: string | null;
}
interface QuickLink {
key: RouteKey;
title: string;
desc: string;
icon: string;
}
const authStore = useAuthStore();
const { routerPushByKey } = useRouterPush();
const loading = ref(false);
const todayModels = ref<TodayModel[]>([]);
const todayOtas = ref<TodayOta[]>([]);
@@ -37,38 +29,6 @@ const todayOtas = ref<TodayOta[]>([]);
const modelCount = computed(() => todayModels.value.length);
const otaCount = computed(() => todayOtas.value.length);
const quickLinks: QuickLink[] = [
{
key: 'headphone_brand',
title: '耳机品牌管理',
desc: '维护耳机品牌信息',
icon: 'mdi:tag-multiple-outline'
},
{
key: 'headphone_model',
title: '耳机型号管理',
desc: '维护耳机型号与配置',
icon: 'mdi:earbuds'
},
{
key: 'upgrade_ota',
title: 'OTA 管理',
desc: '固件升级包管理',
icon: 'mdi:cellphone-arrow-down'
},
{
key: 'share-code_log',
title: '分享日志',
desc: '查看分享码使用记录',
icon: 'mdi:file-document-outline'
},
{
key: 'toolbox_luxsin-controller',
title: 'Luxsin 控制器',
desc: '局域网设备数据同步',
icon: 'mdi:tune-vertical'
}
];
function formatTime(isoStr?: string | null) {
if (!isoStr) return '';
@@ -87,12 +47,149 @@ async function loadTodayStats() {
loading.value = false;
}
function go(key: RouteKey) {
routerPushByKey(key);
// ===== 共享:tooltip 悬浮时追加总计行(可自定义标签)=====
function createTooltipFormatter(totalLabel: string) {
return (params: any): string => {
const list = Array.isArray(params) ? params : [params];
if (!list.length) return '';
const header = `${list[0].axisValueLabel || list[0].name}<br/>`;
let total = 0;
const items = list
.map(p => {
const val = typeof p.value === 'number' ? p.value : 0;
total += val;
return `${p.marker} ${p.seriesName}: <b>${val}</b>`;
})
.join('<br/>');
return `${header}${items}<br/>${totalLabel}: <b>${total}</b>`;
};
}
// ===== 设备每日新增趋势 =====
const deviceDays = ref(30);
const deviceLoading = ref(false);
const deviceTotal = ref(0);
function buildDeviceChartOptions(dates: string[], series: Array<{ name: string; data: number[] }>): ECOption {
const labelInterval = deviceDays.value <= 7 ? 0 : deviceDays.value <= 30 ? 2 : 6;
return {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: createTooltipFormatter('总新增')
},
legend: {
top: 'bottom'
},
grid: {
left: '3%',
right: '4%',
bottom: '12%',
containLabel: true
},
xAxis: {
type: 'category',
data: dates,
axisLabel: {
formatter: (val: string) => val.slice(5),
interval: labelInterval,
rotate: 45
}
},
yAxis: {
type: 'value',
minInterval: 1
},
series: series.map(s => ({
name: s.name,
type: 'bar' as const,
stack: 'total',
data: s.data
}))
};
}
const { domRef: deviceChartRef, updateOptions: updateDeviceChart } = useEcharts(() => buildDeviceChartOptions([], []));
async function loadDeviceDaily() {
deviceLoading.value = true;
const { data, error } = await fetchGetDeviceDaily(deviceDays.value);
if (!error && data) {
deviceTotal.value = data.total;
await updateDeviceChart(() => buildDeviceChartOptions(data.dates, data.series));
}
deviceLoading.value = false;
}
watch(deviceDays, () => {
loadDeviceDaily();
});
// ===== 设备日活趋势 =====
const activeDays = ref(30);
const activeLoading = ref(false);
const activeLatest = ref({ date: '', total: 0 });
function buildActiveChartOptions(dates: string[], series: Array<{ name: string; data: number[] }>): ECOption {
const labelInterval = activeDays.value <= 7 ? 0 : activeDays.value <= 30 ? 2 : 6;
return {
tooltip: {
trigger: 'axis',
formatter: createTooltipFormatter('总日活')
},
legend: {
top: 'bottom'
},
grid: {
left: '3%',
right: '4%',
bottom: '12%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates,
axisLabel: {
formatter: (val: string) => val.slice(5),
interval: labelInterval,
rotate: 45
}
},
yAxis: {
type: 'value',
minInterval: 1
},
series: series.map(s => ({
name: s.name,
type: 'line' as const,
smooth: true,
symbolSize: 6,
data: s.data
}))
};
}
const { domRef: activeChartRef, updateOptions: updateActiveChart } = useEcharts(() => buildActiveChartOptions([], []));
async function loadActiveDaily() {
activeLoading.value = true;
const { data, error } = await fetchGetActiveDaily(activeDays.value);
if (!error && data) {
activeLatest.value = data.latest;
await updateActiveChart(() => buildActiveChartOptions(data.dates, data.series));
}
activeLoading.value = false;
}
watch(activeDays, () => {
loadActiveDaily();
});
onMounted(() => {
loadTodayStats();
loadDeviceDaily();
loadActiveDaily();
});
</script>
@@ -101,30 +198,10 @@ onMounted(() => {
<NCard :bordered="false" class="card-wrapper">
<NThing>
<template #header>欢迎回来{{ authStore.userInfo.userName || '用户' }}</template>
<template #description>Luxsin CMS · 下方快捷入口或左侧菜单开始操作</template>
<template #description>Luxsin CMS · 从左侧菜单开始操作</template>
</NThing>
</NCard>
<NGrid cols="1 s:2 m:3" responsive="screen" :x-gap="16" :y-gap="16">
<NGi v-for="item in quickLinks" :key="item.key">
<NCard
:bordered="false"
class="card-wrapper quick-link-card cursor-pointer"
hoverable
@click="go(item.key)"
>
<div class="flex-y-center gap-12px">
<div class="quick-link-icon">
<SvgIcon :icon="item.icon" class="text-24px" />
</div>
<div>
<div class="text-16px font-medium">{{ item.title }}</div>
<NText depth="3" class="text-13px">{{ item.desc }}</NText>
</div>
</div>
</NCard>
</NGi>
</NGrid>
<NSpin :show="loading">
<NGrid cols="1 s:2" responsive="screen" :x-gap="16" :y-gap="16">
@@ -162,26 +239,51 @@ onMounted(() => {
</NGi>
</NGrid>
</NSpin>
<NGrid cols="1 s:2" responsive="screen" :x-gap="16" :y-gap="16">
<NGi>
<NCard :bordered="false" class="card-wrapper">
<template #header>设备每日新增趋势</template>
<template #header-extra>
<NSpace align="center" :size="12">
<NTag type="info" size="small">合计 {{ deviceTotal }}</NTag>
<NRadioGroup v-model:value="deviceDays" size="small">
<NRadioButton :value="7">近7天</NRadioButton>
<NRadioButton :value="30">近1月</NRadioButton>
<NRadioButton :value="90">近3月</NRadioButton>
</NRadioGroup>
</NSpace>
</template>
<NSpin :show="deviceLoading">
<div ref="deviceChartRef" class="device-chart-container"></div>
</NSpin>
</NCard>
</NGi>
<NGi>
<NCard :bordered="false" class="card-wrapper">
<template #header>设备日活趋势</template>
<template #header-extra>
<NSpace align="center" :size="12">
<NTag type="success" size="small">{{ activeLatest.date.slice(5) }} 总日活 {{ activeLatest.total }}</NTag>
<NRadioGroup v-model:value="activeDays" size="small">
<NRadioButton :value="7">近7天</NRadioButton>
<NRadioButton :value="30">近1月</NRadioButton>
<NRadioButton :value="90">近3月</NRadioButton>
</NRadioGroup>
</NSpace>
</template>
<NSpin :show="activeLoading">
<div ref="activeChartRef" class="device-chart-container"></div>
</NSpin>
</NCard>
</NGi>
</NGrid>
</NSpace>
</template>
<style scoped>
.quick-link-card {
transition: transform 0.2s ease;
}
.quick-link-card:hover {
transform: translateY(-2px);
}
.quick-link-icon {
width: 44px;
height: 44px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(125, 211, 252, 0.12);
color: #38bdf8;
.device-chart-container {
width: 100%;
height: 350px;
}
</style>