Files
dashboard/frontend/src/views/home/index.vue
T

335 lines
7.8 KiB
Vue
Raw Normal View History

2026-06-18 17:37:08 +08:00
<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>
2026-06-30 10:41:03 +08:00
<!-- 快捷入口 -->
2026-06-18 17:37:08 +08:00
<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">
2026-07-10 15:00:49 +08:00
<el-icon class="home-link-icon" :class="item.colorClass"><component :is="item.icon" /></el-icon>
2026-06-18 17:37:08 +08:00
<div>
<div class="home-link-title">{{ item.title }}</div>
<div class="home-link-desc">{{ item.desc }}</div>
</div>
</div>
</el-card>
</div>
2026-06-30 10:41:03 +08:00
<!-- 今日新增数据 -->
<el-card class="home-today-card-wrapper">
<template #header>
<h2 class="home-today-title">今日新增</h2>
</template>
<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"><Loading /></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>
<!-- 今日新增 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"><Loading /></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>
2026-06-18 17:37:08 +08:00
</div>
</template>
<script setup>
2026-06-30 10:41:03 +08:00
import { computed, ref, onMounted } from 'vue'
2026-06-18 17:37:08 +08:00
import { useRouter } from 'vue-router'
2026-07-10 16:51:05 +08:00
import { House, Collection, List, Document, Share, Upload, Loading, Tools } from '@element-plus/icons-vue'
2026-06-18 17:37:08 +08:00
import { getUser } from '@/utils/auth'
2026-06-30 10:41:03 +08:00
import { getTodayStats } from '@/api/dashboard'
2026-06-18 17:37:08 +08:00
defineOptions({ name: 'Home' })
const router = useRouter()
const username = computed(() => getUser()?.username || '')
2026-06-30 10:41:03 +08:00
const todayLoading = ref(false)
const todayModels = ref([])
const todayOtas = ref([])
2026-06-18 17:37:08 +08:00
const quickLinks = [
2026-07-10 15:00:49 +08:00
{ path: '/brand', title: '耳机品牌管理', desc: '维护耳机品牌信息', icon: Collection, colorClass: 'icon-cyan' },
{ path: '/model', title: '耳机型号管理', desc: '维护耳机型号与配置', icon: List, colorClass: 'icon-coral' },
{ path: '/ota', title: 'OTA 管理', desc: '固件升级包管理', icon: Document, colorClass: 'icon-cyan' },
2026-07-10 16:51:05 +08:00
{ path: '/share-code/log', title: '分享日志', desc: '查看分享码使用记录', icon: Share, colorClass: 'icon-coral' },
{ path: '/toolbox/luxsin-controller', title: 'Luxsin 控制器', desc: '局域网设备数据同步', icon: Tools, colorClass: 'icon-cyan' }
2026-06-18 17:37:08 +08:00
]
2026-06-30 10:41:03 +08:00
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())}`
}
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
}
}
2026-06-18 17:37:08 +08:00
function go(path) {
router.push(path)
}
2026-06-30 10:41:03 +08:00
onMounted(() => {
loadTodayStats()
})
2026-06-18 17:37:08 +08:00
</script>
<style scoped>
.home-page {
display: flex;
flex-direction: column;
gap: 16px;
min-height: 100%;
}
.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;
2026-07-10 15:00:49 +08:00
color: #fb7185;
2026-06-18 17:37:08 +08:00
background: linear-gradient(145deg, rgba(15, 23, 42, 0.9), rgba(30, 41, 59, 0.9));
2026-07-10 15:00:49 +08:00
box-shadow: 0 0 0 1px rgba(251, 113, 133, 0.2) inset;
2026-06-18 17:37:08 +08:00
}
.home-title {
margin: 0;
font-size: 24px;
font-weight: 700;
color: #e2e8f0;
}
.home-subtitle {
margin: 8px 0 0;
font-size: 14px;
color: #94a3b8;
}
2026-06-30 10:41:03 +08:00
/* 今日新增 */
.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;
}
/* 快捷入口 */
2026-06-18 17:37:08 +08:00
.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 {
transform: translateY(-2px);
}
.home-link-inner {
display: flex;
align-items: flex-start;
gap: 14px;
}
.home-link-icon {
font-size: 22px;
margin-top: 2px;
}
2026-07-10 15:00:49 +08:00
.home-link-icon.icon-cyan {
color: #7dd3fc;
}
.home-link-icon.icon-coral {
color: #fda4af;
}
2026-06-18 17:37:08 +08:00
.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;
}
</style>