444 lines
10 KiB
Vue
444 lines
10 KiB
Vue
|
|
<template>
|
|||
|
|
<div class="home-container">
|
|||
|
|
<!-- 深空背景 -->
|
|||
|
|
<div class="space-background">
|
|||
|
|
<div class="stars" ref="stars"></div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 顶部欢迎语 -->
|
|||
|
|
<div class="header glass-card">
|
|||
|
|
<div class="welcome-text">
|
|||
|
|
<h2>欢迎回来,{{ userName }}</h2>
|
|||
|
|
<p>{{ greetingText }}</p>
|
|||
|
|
</div>
|
|||
|
|
<div class="header-actions">
|
|||
|
|
<van-icon name="search" size="24" @click="showSearch = true" />
|
|||
|
|
<van-icon name="bell" size="24" @click="showNotifications = true" />
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 时光胶囊视图 -->
|
|||
|
|
<div class="capsules-container">
|
|||
|
|
<div class="capsules-space" ref="capsulesSpace">
|
|||
|
|
<!-- 时间胶囊 -->
|
|||
|
|
<div
|
|||
|
|
v-for="capsule in capsules"
|
|||
|
|
:key="capsule.id"
|
|||
|
|
class="capsule-wrapper"
|
|||
|
|
:style="getCapsuleStyle(capsule)"
|
|||
|
|
@click="openCapsule(capsule)"
|
|||
|
|
>
|
|||
|
|
<div class="time-capsule" :class="{'glowing': capsule.isGlowing}">
|
|||
|
|
<div class="capsule-info">
|
|||
|
|
<p class="capsule-title">{{ capsule.title }}</p>
|
|||
|
|
<p class="capsule-date">{{ formatDate(capsule.deliveryDate) }}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 悬浮按钮 -->
|
|||
|
|
<div class="fab-container">
|
|||
|
|
<van-button
|
|||
|
|
icon="plus"
|
|||
|
|
type="primary"
|
|||
|
|
round
|
|||
|
|
class="fab-button"
|
|||
|
|
@click="goToCompose"
|
|||
|
|
>
|
|||
|
|
撰写邮件
|
|||
|
|
</van-button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- 底部导航 -->
|
|||
|
|
<van-tabbar v-model="active" class="custom-tabbar">
|
|||
|
|
<van-tabbar-item icon="home-o" to="/home">时光胶囊</van-tabbar-item>
|
|||
|
|
<van-tabbar-item icon="envelop-o" to="/inbox">收件箱</van-tabbar-item>
|
|||
|
|
<van-tabbar-item icon="send-o" to="/sent">发件箱</van-tabbar-item>
|
|||
|
|
<van-tabbar-item icon="user-o" to="/profile">个人中心</van-tabbar-item>
|
|||
|
|
</van-tabbar>
|
|||
|
|
|
|||
|
|
<!-- 搜索弹窗 -->
|
|||
|
|
<van-popup v-model:show="showSearch" position="top" :style="{ height: '30%' }">
|
|||
|
|
<div class="search-popup">
|
|||
|
|
<van-search
|
|||
|
|
v-model="searchValue"
|
|||
|
|
placeholder="搜索邮件"
|
|||
|
|
@search="onSearch"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</van-popup>
|
|||
|
|
|
|||
|
|
<!-- 通知弹窗 -->
|
|||
|
|
<van-popup v-model:show="showNotifications" position="top" :style="{ height: '40%' }">
|
|||
|
|
<div class="notifications-popup">
|
|||
|
|
<h3>通知</h3>
|
|||
|
|
<div v-if="notifications.length === 0" class="empty-notifications">
|
|||
|
|
<p>暂无新通知</p>
|
|||
|
|
</div>
|
|||
|
|
<div v-else>
|
|||
|
|
<div
|
|||
|
|
v-for="notification in notifications"
|
|||
|
|
:key="notification.id"
|
|||
|
|
class="notification-item"
|
|||
|
|
>
|
|||
|
|
<p>{{ notification.message }}</p>
|
|||
|
|
<span class="notification-time">{{ formatTime(notification.time) }}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</van-popup>
|
|||
|
|
</div>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
import { ref, onMounted, computed } from 'vue'
|
|||
|
|
import { useRouter } from 'vue-router'
|
|||
|
|
import { showFailToast } from 'vant'
|
|||
|
|
import { userState, mailState, mailActions } from '../store'
|
|||
|
|
|
|||
|
|
export default {
|
|||
|
|
name: 'Home',
|
|||
|
|
setup() {
|
|||
|
|
const router = useRouter()
|
|||
|
|
const active = ref(0)
|
|||
|
|
const stars = ref(null)
|
|||
|
|
const capsulesSpace = ref(null)
|
|||
|
|
const showSearch = ref(false)
|
|||
|
|
const showNotifications = ref(false)
|
|||
|
|
const searchValue = ref('')
|
|||
|
|
|
|||
|
|
// 使用直接导入的状态和操作
|
|||
|
|
const userName = computed(() => userState.userInfo.username || '时光旅人')
|
|||
|
|
const capsules = computed(() => mailState.sentList || [])
|
|||
|
|
const notifications = ref([]) // 暂时使用空数组,可以后续添加通知功能
|
|||
|
|
|
|||
|
|
// 根据时间获取问候语
|
|||
|
|
const greetingText = computed(() => {
|
|||
|
|
const hour = new Date().getHours()
|
|||
|
|
if (hour < 6) return '夜深了,注意休息'
|
|||
|
|
if (hour < 12) return '早上好,美好的一天开始了'
|
|||
|
|
if (hour < 18) return '下午好,继续加油'
|
|||
|
|
return '晚上好,今天过得怎么样'
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 生成星空背景
|
|||
|
|
const generateStars = () => {
|
|||
|
|
if (!stars.value) return
|
|||
|
|
|
|||
|
|
const starsContainer = stars.value
|
|||
|
|
const starCount = 150
|
|||
|
|
|
|||
|
|
for (let i = 0; i < starCount; i++) {
|
|||
|
|
const star = document.createElement('div')
|
|||
|
|
star.className = 'star'
|
|||
|
|
|
|||
|
|
// 随机位置
|
|||
|
|
const left = Math.random() * 100
|
|||
|
|
const top = Math.random() * 100
|
|||
|
|
|
|||
|
|
// 随机大小
|
|||
|
|
const size = Math.random() * 3 + 1
|
|||
|
|
|
|||
|
|
// 随机动画延迟
|
|||
|
|
const delay = Math.random() * 4
|
|||
|
|
|
|||
|
|
star.style.left = `${left}%`
|
|||
|
|
star.style.top = `${top}%`
|
|||
|
|
star.style.width = `${size}px`
|
|||
|
|
star.style.height = `${size}px`
|
|||
|
|
star.style.animationDelay = `${delay}s`
|
|||
|
|
|
|||
|
|
starsContainer.appendChild(star)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取胶囊样式
|
|||
|
|
const getCapsuleStyle = (capsule) => {
|
|||
|
|
return {
|
|||
|
|
left: `${capsule.position.x}%`,
|
|||
|
|
top: `${capsule.position.y}%`,
|
|||
|
|
transform: `scale(${0.5 + capsule.position.z})`,
|
|||
|
|
opacity: 0.5 + capsule.position.z * 0.5,
|
|||
|
|
zIndex: Math.floor(capsule.position.z * 10)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化日期
|
|||
|
|
const formatDate = (date) => {
|
|||
|
|
const now = new Date()
|
|||
|
|
const targetDate = new Date(date)
|
|||
|
|
const diffTime = targetDate - now
|
|||
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|||
|
|
|
|||
|
|
if (diffDays < 0) return '已送达'
|
|||
|
|
if (diffDays === 0) return '今天'
|
|||
|
|
if (diffDays === 1) return '明天'
|
|||
|
|
if (diffDays < 7) return `${diffDays}天后`
|
|||
|
|
if (diffDays < 30) return `${Math.floor(diffDays / 7)}周后`
|
|||
|
|
if (diffDays < 365) return `${Math.floor(diffDays / 30)}个月后`
|
|||
|
|
return `${Math.floor(diffDays / 365)}年后`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 格式化时间
|
|||
|
|
const formatTime = (time) => {
|
|||
|
|
const now = new Date()
|
|||
|
|
const targetDate = new Date(time)
|
|||
|
|
const diffTime = now - targetDate
|
|||
|
|
const diffMinutes = Math.floor(diffTime / (1000 * 60))
|
|||
|
|
|
|||
|
|
if (diffMinutes < 1) return '刚刚'
|
|||
|
|
if (diffMinutes < 60) return `${diffMinutes}分钟前`
|
|||
|
|
|
|||
|
|
const diffHours = Math.floor(diffMinutes / 60)
|
|||
|
|
if (diffHours < 24) return `${diffHours}小时前`
|
|||
|
|
|
|||
|
|
const diffDays = Math.floor(diffHours / 24)
|
|||
|
|
if (diffDays < 7) return `${diffDays}天前`
|
|||
|
|
|
|||
|
|
return targetDate.toLocaleDateString()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 打开胶囊详情
|
|||
|
|
const openCapsule = (capsule) => {
|
|||
|
|
router.push(`/capsule/${capsule.id}`)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 跳转到撰写页面
|
|||
|
|
const goToCompose = () => {
|
|||
|
|
router.push('/compose')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 搜索处理
|
|||
|
|
const onSearch = (value) => {
|
|||
|
|
if (!value) {
|
|||
|
|
showFailToast('请输入搜索内容')
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
router.push(`/search?q=${encodeURIComponent(value)}`)
|
|||
|
|
showSearch.value = false
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取时光胶囊数据
|
|||
|
|
const fetchCapsules = async () => {
|
|||
|
|
try {
|
|||
|
|
await mailActions.getCapsules()
|
|||
|
|
} catch (error) {
|
|||
|
|
showFailToast('获取时光胶囊数据失败')
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 获取通知数据
|
|||
|
|
const fetchNotifications = async () => {
|
|||
|
|
try {
|
|||
|
|
await mailActions.getNotifications()
|
|||
|
|
} catch (error) {
|
|||
|
|
console.error('获取通知失败:', error)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(async () => {
|
|||
|
|
generateStars()
|
|||
|
|
await fetchCapsules()
|
|||
|
|
await fetchNotifications()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
return {
|
|||
|
|
active,
|
|||
|
|
userName,
|
|||
|
|
greetingText,
|
|||
|
|
stars,
|
|||
|
|
capsulesSpace,
|
|||
|
|
capsules,
|
|||
|
|
showSearch,
|
|||
|
|
showNotifications,
|
|||
|
|
searchValue,
|
|||
|
|
notifications,
|
|||
|
|
getCapsuleStyle,
|
|||
|
|
formatDate,
|
|||
|
|
formatTime,
|
|||
|
|
openCapsule,
|
|||
|
|
goToCompose,
|
|||
|
|
onSearch
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style scoped>
|
|||
|
|
.home-container {
|
|||
|
|
height: 100vh;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-text h2 {
|
|||
|
|
margin: 0;
|
|||
|
|
font-size: 18px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.welcome-text p {
|
|||
|
|
margin: 5px 0 0;
|
|||
|
|
font-size: 14px;
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.header-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 15px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capsules-container {
|
|||
|
|
flex: 1;
|
|||
|
|
position: relative;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capsules-space {
|
|||
|
|
position: relative;
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capsule-wrapper {
|
|||
|
|
position: absolute;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capsule-wrapper:hover {
|
|||
|
|
transform: scale(1.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.time-capsule {
|
|||
|
|
width: 60px;
|
|||
|
|
height: 90px;
|
|||
|
|
background: var(--gradient-color);
|
|||
|
|
border-radius: 30px;
|
|||
|
|
position: relative;
|
|||
|
|
box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
|
|||
|
|
transition: all 0.3s ease;
|
|||
|
|
animation: float 6s ease-in-out infinite;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.time-capsule::before {
|
|||
|
|
content: '';
|
|||
|
|
position: absolute;
|
|||
|
|
top: 10px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
width: 40px;
|
|||
|
|
height: 40px;
|
|||
|
|
background: radial-gradient(circle, var(--accent-color), transparent);
|
|||
|
|
border-radius: 50%;
|
|||
|
|
opacity: 0.7;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.time-capsule.glowing::before {
|
|||
|
|
animation: pulse 2s infinite alternate;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capsule-info {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: -40px;
|
|||
|
|
left: 50%;
|
|||
|
|
transform: translateX(-50%);
|
|||
|
|
width: 100px;
|
|||
|
|
text-align: center;
|
|||
|
|
opacity: 0;
|
|||
|
|
transition: opacity 0.3s ease;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capsule-wrapper:hover .capsule-info {
|
|||
|
|
opacity: 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capsule-title {
|
|||
|
|
font-size: 12px;
|
|||
|
|
font-weight: bold;
|
|||
|
|
margin: 0;
|
|||
|
|
white-space: nowrap;
|
|||
|
|
overflow: hidden;
|
|||
|
|
text-overflow: ellipsis;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.capsule-date {
|
|||
|
|
font-size: 10px;
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
margin: 2px 0 0;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@keyframes pulse {
|
|||
|
|
0% {
|
|||
|
|
opacity: 0.4;
|
|||
|
|
transform: translateX(-50%) scale(0.8);
|
|||
|
|
}
|
|||
|
|
100% {
|
|||
|
|
opacity: 1;
|
|||
|
|
transform: translateX(-50%) scale(1.2);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fab-container {
|
|||
|
|
position: absolute;
|
|||
|
|
bottom: 80px;
|
|||
|
|
right: 20px;
|
|||
|
|
z-index: 10;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.fab-button {
|
|||
|
|
width: 60px;
|
|||
|
|
height: 60px;
|
|||
|
|
border-radius: 50%;
|
|||
|
|
background: linear-gradient(135deg, #00D4FF, #1D3B5A);
|
|||
|
|
border: none;
|
|||
|
|
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.4);
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.search-popup {
|
|||
|
|
padding: 20px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notifications-popup {
|
|||
|
|
padding: 20px;
|
|||
|
|
height: 100%;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notifications-popup h3 {
|
|||
|
|
margin: 0 0 15px;
|
|||
|
|
font-size: 18px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.empty-notifications {
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: center;
|
|||
|
|
align-items: center;
|
|||
|
|
height: 100px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-item {
|
|||
|
|
padding: 10px 0;
|
|||
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-item:last-child {
|
|||
|
|
border-bottom: none;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-item p {
|
|||
|
|
margin: 0 0 5px;
|
|||
|
|
font-size: 14px;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
.notification-time {
|
|||
|
|
font-size: 12px;
|
|||
|
|
color: var(--text-secondary);
|
|||
|
|
}
|
|||
|
|
</style>
|