2026-07-17 09:35:32 +08:00
|
|
|
|
<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 { useAuthStore } from '@/store/modules/auth';
|
|
|
|
|
|
|
|
|
|
|
|
defineOptions({ name: 'Home' });
|
|
|
|
|
|
|
|
|
|
|
|
interface TodayModel {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
brand_name?: string;
|
|
|
|
|
|
name?: string;
|
|
|
|
|
|
create_at?: string | null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
interface TodayOta {
|
|
|
|
|
|
id: number;
|
|
|
|
|
|
verName?: string;
|
|
|
|
|
|
model?: string;
|
|
|
|
|
|
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[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
const modelCount = computed(() => todayModels.value.length);
|
|
|
|
|
|
const otaCount = computed(() => todayOtas.value.length);
|
|
|
|
|
|
|
|
|
|
|
|
const quickLinks: QuickLink[] = [
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'headphone_brand',
|
|
|
|
|
|
title: '耳机品牌管理',
|
|
|
|
|
|
desc: '维护耳机品牌信息',
|
2026-07-20 15:45:52 +08:00
|
|
|
|
icon: 'mdi:tag-multiple-outline'
|
2026-07-17 09:35:32 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'headphone_model',
|
|
|
|
|
|
title: '耳机型号管理',
|
|
|
|
|
|
desc: '维护耳机型号与配置',
|
2026-07-20 15:45:52 +08:00
|
|
|
|
icon: 'mdi:earbuds'
|
2026-07-17 09:35:32 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'upgrade_ota',
|
|
|
|
|
|
title: 'OTA 管理',
|
|
|
|
|
|
desc: '固件升级包管理',
|
2026-07-20 15:45:52 +08:00
|
|
|
|
icon: 'mdi:cellphone-arrow-down'
|
2026-07-17 09:35:32 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'share-code_log',
|
|
|
|
|
|
title: '分享日志',
|
|
|
|
|
|
desc: '查看分享码使用记录',
|
2026-07-20 15:45:52 +08:00
|
|
|
|
icon: 'mdi:file-document-outline'
|
2026-07-17 09:35:32 +08:00
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
key: 'toolbox_luxsin-controller',
|
|
|
|
|
|
title: 'Luxsin 控制器',
|
|
|
|
|
|
desc: '局域网设备数据同步',
|
2026-07-20 15:45:52 +08:00
|
|
|
|
icon: 'mdi:tune-vertical'
|
2026-07-17 09:35:32 +08:00
|
|
|
|
}
|
|
|
|
|
|
];
|
2026-06-18 17:37:08 +08:00
|
|
|
|
|
2026-07-17 09:35:32 +08:00
|
|
|
|
function formatTime(isoStr?: string | null) {
|
|
|
|
|
|
if (!isoStr) return '';
|
|
|
|
|
|
const d = new Date(isoStr);
|
|
|
|
|
|
const pad = (n: number) => String(n).padStart(2, '0');
|
|
|
|
|
|
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
2026-06-30 10:41:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function loadTodayStats() {
|
2026-07-17 09:35:32 +08:00
|
|
|
|
loading.value = true;
|
|
|
|
|
|
const { data, error } = await fetchGetDashboardToday();
|
|
|
|
|
|
if (!error && data) {
|
|
|
|
|
|
todayModels.value = (data.models as TodayModel[]) || [];
|
|
|
|
|
|
todayOtas.value = (data.otas as TodayOta[]) || [];
|
2026-06-30 10:41:03 +08:00
|
|
|
|
}
|
2026-07-17 09:35:32 +08:00
|
|
|
|
loading.value = false;
|
2026-06-30 10:41:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 09:35:32 +08:00
|
|
|
|
function go(key: RouteKey) {
|
|
|
|
|
|
routerPushByKey(key);
|
2026-06-18 17:37:08 +08:00
|
|
|
|
}
|
2026-06-30 10:41:03 +08:00
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-07-17 09:35:32 +08:00
|
|
|
|
loadTodayStats();
|
|
|
|
|
|
});
|
2026-06-18 17:37:08 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
2026-07-17 09:35:32 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<NSpace vertical :size="16">
|
|
|
|
|
|
<NCard :bordered="false" class="card-wrapper">
|
|
|
|
|
|
<NThing>
|
|
|
|
|
|
<template #header>欢迎回来,{{ authStore.userInfo.userName || '用户' }}</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">
|
|
|
|
|
|
<NGi>
|
|
|
|
|
|
<NCard :bordered="false" class="card-wrapper" title="今日新增型号">
|
|
|
|
|
|
<template #header-extra>
|
|
|
|
|
|
<NTag type="info" size="small">{{ modelCount }}</NTag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<NEmpty v-if="!todayModels.length" description="暂无新增" />
|
|
|
|
|
|
<NList v-else>
|
|
|
|
|
|
<NListItem v-for="item in todayModels" :key="item.id">
|
|
|
|
|
|
<div class="flex-y-center justify-between">
|
|
|
|
|
|
<span>{{ item.brand_name }} {{ item.name }}</span>
|
|
|
|
|
|
<NText depth="3">{{ formatTime(item.create_at) }}</NText>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</NListItem>
|
|
|
|
|
|
</NList>
|
|
|
|
|
|
</NCard>
|
|
|
|
|
|
</NGi>
|
|
|
|
|
|
<NGi>
|
|
|
|
|
|
<NCard :bordered="false" class="card-wrapper" title="今日新增 OTA">
|
|
|
|
|
|
<template #header-extra>
|
|
|
|
|
|
<NTag type="success" size="small">{{ otaCount }}</NTag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<NEmpty v-if="!todayOtas.length" description="暂无新增" />
|
|
|
|
|
|
<NList v-else>
|
|
|
|
|
|
<NListItem v-for="item in todayOtas" :key="item.id">
|
|
|
|
|
|
<div class="flex-y-center justify-between">
|
|
|
|
|
|
<span>{{ item.verName }} · {{ item.model }}</span>
|
|
|
|
|
|
<NText depth="3">{{ formatTime(item.create_at) }}</NText>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</NListItem>
|
|
|
|
|
|
</NList>
|
|
|
|
|
|
</NCard>
|
|
|
|
|
|
</NGi>
|
|
|
|
|
|
</NGrid>
|
|
|
|
|
|
</NSpin>
|
|
|
|
|
|
</NSpace>
|
|
|
|
|
|
</template>
|
2026-06-30 10:41:03 +08:00
|
|
|
|
|
2026-07-17 09:35:32 +08:00
|
|
|
|
<style scoped>
|
|
|
|
|
|
.quick-link-card {
|
|
|
|
|
|
transition: transform 0.2s ease;
|
2026-06-30 10:41:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 09:35:32 +08:00
|
|
|
|
.quick-link-card:hover {
|
|
|
|
|
|
transform: translateY(-2px);
|
2026-06-30 10:41:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-17 09:35:32 +08:00
|
|
|
|
.quick-link-icon {
|
|
|
|
|
|
width: 44px;
|
|
|
|
|
|
height: 44px;
|
|
|
|
|
|
border-radius: 12px;
|
2026-06-30 10:41:03 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-07-17 09:35:32 +08:00
|
|
|
|
background: rgba(125, 211, 252, 0.12);
|
|
|
|
|
|
color: #38bdf8;
|
2026-06-18 17:37:08 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|