全新的 ui,soybean admin
This commit is contained in:
+159
-306
@@ -1,334 +1,187 @@
|
||||
<template>
|
||||
<div class="home-page">
|
||||
<el-card class="home-hero">
|
||||
<div class="home-hero-inner">
|
||||
<div class="home-hero-icon">
|
||||
<el-icon :size="28"><House /></el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="home-title">欢迎回来{{ username ? `,${username}` : '' }}</h1>
|
||||
<p class="home-subtitle">Luxsin CMS · 从左侧菜单或下方快捷入口开始操作</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<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';
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<div class="home-links">
|
||||
<el-card
|
||||
v-for="item in quickLinks"
|
||||
:key="item.path"
|
||||
class="home-link-card"
|
||||
shadow="hover"
|
||||
@click="go(item.path)"
|
||||
>
|
||||
<div class="home-link-inner">
|
||||
<el-icon class="home-link-icon" :class="item.colorClass"><component :is="item.icon" /></el-icon>
|
||||
<div>
|
||||
<div class="home-link-title">{{ item.title }}</div>
|
||||
<div class="home-link-desc">{{ item.desc }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
defineOptions({ name: 'Home' });
|
||||
|
||||
<!-- 今日新增数据 -->
|
||||
<el-card class="home-today-card-wrapper">
|
||||
<template #header>
|
||||
<h2 class="home-today-title">今日新增</h2>
|
||||
</template>
|
||||
interface TodayModel {
|
||||
id: number;
|
||||
brand_name?: string;
|
||||
name?: string;
|
||||
create_at?: string | null;
|
||||
}
|
||||
|
||||
<div class="home-today-grid">
|
||||
<!-- 今日新增型号 -->
|
||||
<el-card class="home-today-card">
|
||||
<template #header>
|
||||
<div class="home-card-header">
|
||||
<span class="home-card-title">
|
||||
<el-icon><List /></el-icon>
|
||||
新增型号
|
||||
</span>
|
||||
<el-tag size="small" type="primary">{{ todayModels.length }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="todayLoading" class="home-today-loading">
|
||||
<el-icon class="is-loading"><LoaderCircle /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="todayModels.length === 0" class="home-today-empty">
|
||||
暂无新增
|
||||
</div>
|
||||
<ul v-else class="home-today-list">
|
||||
<li v-for="item in todayModels" :key="item.id" class="home-today-item">
|
||||
<span class="home-today-item-text">{{ item.brand_name }} {{ item.name }}</span>
|
||||
<span class="home-today-item-time">{{ formatTime(item.create_at) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
interface TodayOta {
|
||||
id: number;
|
||||
verName?: string;
|
||||
model?: string;
|
||||
create_at?: string | null;
|
||||
}
|
||||
|
||||
<!-- 今日新增 OTA -->
|
||||
<el-card class="home-today-card">
|
||||
<template #header>
|
||||
<div class="home-card-header">
|
||||
<span class="home-card-title">
|
||||
<el-icon><Upload /></el-icon>
|
||||
新增 OTA
|
||||
</span>
|
||||
<el-tag size="small" type="success">{{ todayOtas.length }}</el-tag>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="todayLoading" class="home-today-loading">
|
||||
<el-icon class="is-loading"><LoaderCircle /></el-icon>
|
||||
</div>
|
||||
<div v-else-if="todayOtas.length === 0" class="home-today-empty">
|
||||
暂无新增
|
||||
</div>
|
||||
<ul v-else class="home-today-list">
|
||||
<li v-for="item in todayOtas" :key="item.id" class="home-today-item">
|
||||
<span class="home-today-item-text">{{ item.verName }} · {{ item.model }}</span>
|
||||
<span class="home-today-item-time">{{ formatTime(item.create_at) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-card>
|
||||
interface QuickLink {
|
||||
key: RouteKey;
|
||||
title: string;
|
||||
desc: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
const authStore = useAuthStore();
|
||||
const { routerPushByKey } = useRouterPush();
|
||||
const loading = ref(false);
|
||||
const todayModels = ref<TodayModel[]>([]);
|
||||
const todayOtas = ref<TodayOta[]>([]);
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { House, Layers, List, FileText, Share2, Upload, LoaderCircle, ToolCase } from '@lucide/vue'
|
||||
import { getUser } from '@/utils/auth'
|
||||
import { getTodayStats } from '@/api/dashboard'
|
||||
const modelCount = computed(() => todayModels.value.length);
|
||||
const otaCount = computed(() => todayOtas.value.length);
|
||||
|
||||
defineOptions({ name: 'Home' })
|
||||
const quickLinks: QuickLink[] = [
|
||||
{
|
||||
key: 'headphone_brand',
|
||||
title: '耳机品牌管理',
|
||||
desc: '维护耳机品牌信息',
|
||||
icon: 'mdi:layers-triple'
|
||||
},
|
||||
{
|
||||
key: 'headphone_model',
|
||||
title: '耳机型号管理',
|
||||
desc: '维护耳机型号与配置',
|
||||
icon: 'mdi:format-list-bulleted'
|
||||
},
|
||||
{
|
||||
key: 'upgrade_ota',
|
||||
title: 'OTA 管理',
|
||||
desc: '固件升级包管理',
|
||||
icon: 'mdi:file-document-outline'
|
||||
},
|
||||
{
|
||||
key: 'share-code_log',
|
||||
title: '分享日志',
|
||||
desc: '查看分享码使用记录',
|
||||
icon: 'mdi:share-variant'
|
||||
},
|
||||
{
|
||||
key: 'toolbox_luxsin-controller',
|
||||
title: 'Luxsin 控制器',
|
||||
desc: '局域网设备数据同步',
|
||||
icon: 'mdi:tools'
|
||||
}
|
||||
];
|
||||
|
||||
const router = useRouter()
|
||||
const username = computed(() => getUser()?.username || '')
|
||||
|
||||
const todayLoading = ref(false)
|
||||
const todayModels = ref([])
|
||||
const todayOtas = ref([])
|
||||
|
||||
const quickLinks = [
|
||||
{ path: '/brand', title: '耳机品牌管理', desc: '维护耳机品牌信息', icon: Layers, colorClass: 'icon-cyan' },
|
||||
{ path: '/model', title: '耳机型号管理', desc: '维护耳机型号与配置', icon: List, colorClass: 'icon-coral' },
|
||||
{ path: '/ota', title: 'OTA 管理', desc: '固件升级包管理', icon: FileText, colorClass: 'icon-cyan' },
|
||||
{ path: '/share-code/log', title: '分享日志', desc: '查看分享码使用记录', icon: Share2, colorClass: 'icon-coral' },
|
||||
{ path: '/toolbox/luxsin-controller', title: 'Luxsin 控制器', desc: '局域网设备数据同步', icon: ToolCase, colorClass: 'icon-cyan' }
|
||||
]
|
||||
|
||||
function formatTime(isoStr) {
|
||||
if (!isoStr) return ''
|
||||
const d = new Date(isoStr)
|
||||
const pad = (n) => String(n).padStart(2, '0')
|
||||
return `${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
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() {
|
||||
todayLoading.value = true
|
||||
try {
|
||||
const res = await getTodayStats()
|
||||
if (res.code === 1 && res.data) {
|
||||
todayModels.value = res.data.models || []
|
||||
todayOtas.value = res.data.otas || []
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取今日数据失败:', e)
|
||||
} finally {
|
||||
todayLoading.value = false
|
||||
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(path) {
|
||||
router.push(path)
|
||||
function go(key: RouteKey) {
|
||||
routerPushByKey(key);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTodayStats()
|
||||
})
|
||||
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>
|
||||
.home-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
min-height: 100%;
|
||||
.quick-link-card {
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.home-hero :deep(.el-card__body) {
|
||||
padding: 28px 32px;
|
||||
}
|
||||
|
||||
.home-hero-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.home-hero-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fb7185;
|
||||
background: linear-gradient(145deg, rgba(15, 23, 42, 0.9), rgba(30, 41, 59, 0.9));
|
||||
box-shadow: 0 0 0 1px rgba(251, 113, 133, 0.2) inset;
|
||||
}
|
||||
|
||||
.home-title {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.home-subtitle {
|
||||
margin: 8px 0 0;
|
||||
font-size: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 今日新增 */
|
||||
.home-today-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.home-today-card-wrapper {
|
||||
max-width: 760px;
|
||||
}
|
||||
|
||||
.home-today-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.home-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.home-card-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.home-card-title .el-icon {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.home-today-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 32px 0;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.home-today-loading .el-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.home-today-empty {
|
||||
text-align: center;
|
||||
padding: 24px 0;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.home-today-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
max-height: 280px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.home-today-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
|
||||
.home-today-item:hover {
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
}
|
||||
|
||||
.home-today-item-text {
|
||||
font-size: 14px;
|
||||
color: #cbd5e1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.home-today-item-time {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* 快捷入口 */
|
||||
.home-links {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.home-link-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.home-link-card:hover {
|
||||
.quick-link-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.home-link-inner {
|
||||
.quick-link-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.home-link-icon {
|
||||
font-size: 22px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.home-link-icon.icon-cyan {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
|
||||
.home-link-icon.icon-coral {
|
||||
color: #fda4af;
|
||||
}
|
||||
|
||||
.home-link-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.home-link-desc {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(125, 211, 252, 0.12);
|
||||
color: #38bdf8;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user