feat:前端做了一些改善,以及主页demo
This commit is contained in:
@@ -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)`
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user