188 lines
5.1 KiB
Vue
188 lines
5.1 KiB
Vue
<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>
|