feat:前端做了一些改善,以及主页demo

This commit is contained in:
Losita
2026-05-05 23:31:24 +08:00
parent 2204fac84e
commit 816a29c062
16 changed files with 3281 additions and 60 deletions

View File

@@ -4,16 +4,18 @@ import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
interface SidebarItem {
key: 'home' | 'task' | 'calendar' | 'ai'
key: 'home' | 'task' | 'calendar' | 'ai' | 'forum' | 'store'
label: string
short: string
to?: '/dashboard' | '/assistant' | '/schedule'
to?: '/dashboard' | '/assistant' | '/schedule' | '/forum' | '/store'
}
const sidebarItems: SidebarItem[] = [
{ key: 'home', label: '总览', short: '总', to: '/dashboard' },
{ key: 'calendar', label: '日程', short: '程', to: '/schedule' },
{ key: 'ai', label: '助手', short: 'AI', to: '/assistant' },
{ key: 'forum', label: '社区', short: '区', to: '/forum' },
{ key: 'store', label: '商店', short: '商', to: '/store' },
]
const route = useRoute()
@@ -22,6 +24,8 @@ const router = useRouter()
const activeSidebarKey = computed<SidebarItem['key']>(() => {
if (route.path.startsWith('/assistant')) return 'ai'
if (route.path.startsWith('/schedule')) return 'calendar'
if (route.path.startsWith('/forum')) return 'forum'
if (route.path.startsWith('/store')) return 'store'
return 'home'
})
@@ -30,6 +34,7 @@ const activeSidebarIndex = computed(() => {
})
const activeIndicatorStyle = computed(() => {
// 每个项高度 60px + 间隔 12px = 72px
return {
transform: `translateY(${activeSidebarIndex.value * 72}px)`
}

View File

@@ -333,6 +333,11 @@ const visibleTasks = computed(() => props.tasks)
.action-btn.delete:hover { background: #fee2e2; transform: scale(1.1); }
/* --- 骨架屏 --- */
.quadrant-card__skeleton {
display: grid;
gap: 12px;
}
.quadrant-card__skeleton-item {
border-radius: 18px;
min-height: 72px;

View File

@@ -109,35 +109,38 @@ const renderSlots = computed<RenderSlot[]>(() =>
</div>
</header>
<transition name="grid-pop" mode="out-in">
<div v-if="loading" key="loading" class="pastel-grid">
<div v-for="n in 8" :key="n" class="skeleton-pill" />
</div>
<div v-if="loading" key="loading" class="pastel-grid">
<div
v-for="slot in slotBlueprint"
:key="slot.key"
class="skeleton-pill"
:class="{ 'is-pause': slot.kind === 'pause' }"
/>
</div>
<div v-else key="content" class="pastel-grid">
<template v-for="slot in renderSlots" :key="slot.key">
<article
v-if="slot.kind === 'event'"
class="pastel-item"
:class="[`tone--${slot.tone}`]"
>
<div class="item-time">{{ slot.timeText }}</div>
<strong class="item-title">{{ slot.title }}</strong>
<div class="item-footer">
<svg class="location-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<span class="location-text">{{ slot.locationText }}</span>
</div>
</article>
<div v-else key="content" class="pastel-grid">
<template v-for="slot in renderSlots" :key="slot.key">
<article
v-if="slot.kind === 'event'"
class="pastel-item"
:class="[`tone--${slot.tone}`]"
>
<div class="item-time">{{ slot.timeText }}</div>
<strong class="item-title">{{ slot.title }}</strong>
<div class="item-footer">
<svg class="location-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<span class="location-text">{{ slot.locationText }}</span>
</div>
</article>
<article v-else class="pause-item">
<span class="pause-tag">{{ slot.title }}</span>
</article>
</template>
</div>
</transition>
<article v-else class="pause-item">
<span class="pause-tag">{{ slot.title }}</span>
</article>
</template>
</div>
</section>
</template>
@@ -187,16 +190,9 @@ const renderSlots = computed<RenderSlot[]>(() =>
flex-direction: column;
justify-content: space-between;
min-height: 140px;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
cursor: default;
}
.pastel-item:hover {
transform: scale(1.03) translateY(-4px);
box-shadow: 0 15px 30px -10px rgba(0, 0, 0, 0.1);
z-index: 10;
}
.item-time {
font-size: 12px;
font-weight: 800;
@@ -266,20 +262,16 @@ const renderSlots = computed<RenderSlot[]>(() =>
animation: pill-shimmer 1.5s infinite linear;
}
.skeleton-pill.is-pause {
min-height: 80px;
}
@keyframes pill-shimmer {
0% { opacity: 0.5; }
50% { opacity: 1; }
100% { opacity: 0.5; }
}
/* 动画效果 */
.grid-pop-enter-active {
transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.grid-pop-enter-from {
opacity: 0;
transform: scale(0.9);
}
@media (max-width: 1200px) {
.pastel-grid { grid-template-columns: repeat(4, 1fr); }