Files
dashboard/frontend/src/views/home/index.vue
T
2026-07-20 15:45:52 +08:00

188 lines
5.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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: '维护耳机品牌信息',
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 '';
const d = new Date(isoStr);
const pad = (n: number) => String(n).padStart(2, '0');
return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
async function loadTodayStats() {
loading.value = true;
const { data, error } = await fetchGetDashboardToday();
if (!error && data) {
todayModels.value = (data.models as TodayModel[]) || [];
todayOtas.value = (data.otas as TodayOta[]) || [];
}
loading.value = false;
}
function go(key: RouteKey) {
routerPushByKey(key);
}
onMounted(() => {
loadTodayStats();
});
</script>
<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>
<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;
}
</style>