617 lines
16 KiB
Vue
617 lines
16 KiB
Vue
<template>
|
|
<div class="sent-container">
|
|
<!-- 深空背景 -->
|
|
<div class="space-background">
|
|
<div class="stars" ref="stars"></div>
|
|
</div>
|
|
|
|
<!-- 顶部导航 -->
|
|
<div class="header glass-card">
|
|
<van-icon name="arrow-left" size="24" @click="goBack" />
|
|
<h2>发件箱</h2>
|
|
<van-icon name="sort" size="24" @click="showSort = true" />
|
|
</div>
|
|
|
|
<!-- 邮件列表 -->
|
|
<div class="mail-list">
|
|
<van-list
|
|
v-model:loading="loading"
|
|
:finished="finished"
|
|
finished-text="没有更多了"
|
|
@load="loadMore"
|
|
>
|
|
<div
|
|
v-for="mail in sortedMails"
|
|
:key="mail.mailId"
|
|
class="mail-item glass-card"
|
|
@click="openMail(mail)"
|
|
>
|
|
<div class="mail-icon">
|
|
<div class="time-capsule-small" :class="{'delivered': mail.status === 'DELIVERED'}"></div>
|
|
</div>
|
|
<div class="mail-content">
|
|
<div class="mail-header">
|
|
<h3 class="mail-title">{{ mail.title }}</h3>
|
|
<span class="mail-date">{{ formatDate(mail.sendTime) }}</span>
|
|
</div>
|
|
<p class="mail-preview">{{ mail.content.substring(0, 50) }}...</p>
|
|
<div class="mail-footer">
|
|
<span class="mail-recipient">收件人: {{ mail.recipient.username }}</span>
|
|
<van-tag :type="getStatusType(mail.status)" size="small">
|
|
{{ getStatusText(mail.status) }}
|
|
</van-tag>
|
|
</div>
|
|
<div class="mail-progress">
|
|
<div class="progress-info">
|
|
<span>投递进度</span>
|
|
<span>{{ getProgressText(mail) }}</span>
|
|
</div>
|
|
<van-progress
|
|
:percentage="getProgressPercentage(mail)"
|
|
stroke-width="4"
|
|
color="#00D4FF"
|
|
track-color="rgba(255, 255, 255, 0.1)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="mail-actions">
|
|
<van-icon name="eye" size="18" @click.stop="previewMail(mail)" />
|
|
<van-icon name="edit" size="18" @click.stop="editMail(mail)" v-if="mail.status === 'DRAFT'" />
|
|
<van-icon name="close" size="18" @click.stop="cancelMail(mail)" v-if="mail.status === 'PENDING'" />
|
|
</div>
|
|
</div>
|
|
</van-list>
|
|
|
|
<div v-if="mails.value.length === 0 && !loading" class="empty-state">
|
|
<van-empty description="暂无已发送的邮件" />
|
|
</div>
|
|
</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="showSort" position="bottom" :style="{ height: '30%' }">
|
|
<div class="sort-popup">
|
|
<h3>排序方式</h3>
|
|
<van-radio-group v-model="sortType">
|
|
<van-cell-group>
|
|
<van-cell title="按发送时间" clickable @click="sortType = 'sendDate'">
|
|
<template #right-icon>
|
|
<van-radio name="sendDate" />
|
|
</template>
|
|
</van-cell>
|
|
<van-cell title="按投递时间" clickable @click="sortType = 'deliveryDate'">
|
|
<template #right-icon>
|
|
<van-radio name="deliveryDate" />
|
|
</template>
|
|
</van-cell>
|
|
<van-cell title="按状态" clickable @click="sortType = 'status'">
|
|
<template #right-icon>
|
|
<van-radio name="status" />
|
|
</template>
|
|
</van-cell>
|
|
</van-cell-group>
|
|
</van-radio-group>
|
|
<div class="sort-actions">
|
|
<van-button block type="primary" @click="applySort">确定</van-button>
|
|
</div>
|
|
</div>
|
|
</van-popup>
|
|
|
|
<!-- 预览弹窗 -->
|
|
<van-popup v-model:show="showPreview" position="bottom" :style="{ height: '70%' }">
|
|
<div class="preview-popup">
|
|
<div class="preview-header">
|
|
<h3>{{ previewMailData.title }}</h3>
|
|
<van-icon name="cross" @click="showPreview = false" />
|
|
</div>
|
|
<div class="preview-content">
|
|
<div class="preview-info">
|
|
<p><strong>收件人:</strong> {{ previewMailData.recipient?.username || '未知' }}</p>
|
|
<p><strong>发送时间:</strong> {{ formatDate(previewMailData.sendTime) }}</p>
|
|
<p v-if="previewMailData.deliveryTime"><strong>投递时间:</strong> {{ formatDate(previewMailData.deliveryTime) }}</p>
|
|
<p><strong>状态:</strong> {{ getStatusText(previewMailData.status) }}</p>
|
|
</div>
|
|
<div class="preview-text">
|
|
{{ previewMailData.content }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</van-popup>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { showFailToast, showSuccessToast, Dialog } from 'vant'
|
|
import { mailActions, mailState } from '../store'
|
|
|
|
export default {
|
|
name: 'Sent',
|
|
setup() {
|
|
const router = useRouter()
|
|
const active = ref(2)
|
|
const stars = ref(null)
|
|
const sortType = ref('sendDate')
|
|
const showSort = ref(false)
|
|
const showPreview = ref(false)
|
|
const previewMailData = ref(null)
|
|
|
|
// 使用直接导入的状态和操作
|
|
const mails = computed(() => mailState.sentList)
|
|
const loading = computed(() => mailState.loading)
|
|
|
|
// 邮件数据
|
|
const finished = ref(false)
|
|
const page = ref(1)
|
|
const pageSize = ref(10)
|
|
|
|
// 排序后的邮件
|
|
const sortedMails = computed(() => {
|
|
const sorted = [...mails.value]
|
|
|
|
switch (sortType.value) {
|
|
case 'sendDate':
|
|
return sorted.sort((a, b) => new Date(b.sendTime) - new Date(a.sendTime))
|
|
case 'deliveryDate':
|
|
return sorted.sort((a, b) => {
|
|
if (!a.deliveryTime) return 1
|
|
if (!b.deliveryTime) return -1
|
|
return new Date(a.deliveryTime) - new Date(b.deliveryTime)
|
|
})
|
|
case 'status':
|
|
const statusOrder = { DRAFT: 0, PENDING: 1, DELIVERING: 2, DELIVERED: 3 }
|
|
return sorted.sort((a, b) => statusOrder[a.status] - statusOrder[b.status])
|
|
default:
|
|
return sorted
|
|
}
|
|
})
|
|
|
|
// 生成星空背景
|
|
const generateStars = () => {
|
|
if (!stars.value) return
|
|
|
|
const starsContainer = stars.value
|
|
const starCount = 100
|
|
|
|
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 fetchMails = async (reset = false) => {
|
|
if (loading.value || finished.value) return
|
|
|
|
try {
|
|
if (reset) {
|
|
page.value = 1
|
|
finished.value = false
|
|
}
|
|
|
|
const response = await mailActions.getMails({
|
|
type: 'SENT',
|
|
page: page.value,
|
|
size: pageSize.value
|
|
})
|
|
|
|
if (response.code === 200) {
|
|
// 判断是否加载完成
|
|
if (response.data.list.length < pageSize.value) {
|
|
finished.value = true
|
|
} else {
|
|
page.value += 1
|
|
}
|
|
} else {
|
|
showFailToast(response.message || '获取邮件列表失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('获取邮件列表失败:', error)
|
|
showFailToast('获取邮件列表失败')
|
|
}
|
|
}
|
|
|
|
// 加载更多
|
|
const loadMore = () => {
|
|
fetchMails()
|
|
}
|
|
|
|
// 返回上一页
|
|
const goBack = () => {
|
|
router.back()
|
|
}
|
|
|
|
// 格式化日期
|
|
const formatDate = (dateStr) => {
|
|
if (!dateStr) return '未设置'
|
|
|
|
const date = new Date(dateStr)
|
|
const now = new Date()
|
|
const diffTime = date - now
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
|
|
|
if (diffDays < 0) {
|
|
// 过去的时间
|
|
const pastDays = Math.abs(diffDays)
|
|
if (pastDays === 0) return '今天'
|
|
if (pastDays === 1) return '昨天'
|
|
if (pastDays < 7) return `${pastDays}天前`
|
|
if (pastDays < 30) return `${Math.floor(pastDays / 7)}周前`
|
|
if (pastDays < 365) return `${Math.floor(pastDays / 30)}个月前`
|
|
return `${Math.floor(pastDays / 365)}年前`
|
|
} else {
|
|
// 未来的时间
|
|
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 getStatusType = (status) => {
|
|
switch (status) {
|
|
case 'DRAFT': return 'warning'
|
|
case 'PENDING': return 'primary'
|
|
case 'DELIVERING': return 'primary'
|
|
case 'DELIVERED': return 'success'
|
|
default: return 'default'
|
|
}
|
|
}
|
|
|
|
// 获取状态文本
|
|
const getStatusText = (status) => {
|
|
switch (status) {
|
|
case 'DRAFT': return '草稿'
|
|
case 'PENDING': return '待投递'
|
|
case 'DELIVERING': return '投递中'
|
|
case 'DELIVERED': return '已送达'
|
|
default: return '未知'
|
|
}
|
|
}
|
|
|
|
// 获取进度文本
|
|
const getProgressText = (mail) => {
|
|
if (mail.status === 'DRAFT') return '未设置投递时间'
|
|
if (mail.status === 'DELIVERED') return '已完成'
|
|
|
|
const now = new Date()
|
|
const sendTime = new Date(mail.sendTime)
|
|
const deliveryTime = new Date(mail.deliveryTime)
|
|
const total = deliveryTime - sendTime
|
|
const elapsed = now - sendTime
|
|
const percentage = Math.min(100, Math.max(0, (elapsed / total) * 100))
|
|
|
|
return `${Math.round(percentage)}%`
|
|
}
|
|
|
|
// 获取进度百分比
|
|
const getProgressPercentage = (mail) => {
|
|
if (mail.status === 'DRAFT') return 0
|
|
if (mail.status === 'DELIVERED') return 100
|
|
|
|
const now = new Date()
|
|
const sendTime = new Date(mail.sendTime)
|
|
const deliveryTime = new Date(mail.deliveryTime)
|
|
const total = deliveryTime - sendTime
|
|
const elapsed = now - sendTime
|
|
return Math.min(100, Math.max(0, (elapsed / total) * 100))
|
|
}
|
|
|
|
// 打开邮件
|
|
const openMail = (mail) => {
|
|
router.push(`/capsule/${mail.mailId}`)
|
|
}
|
|
|
|
// 预览邮件
|
|
const previewMail = (mail) => {
|
|
previewMailData.value = mail
|
|
showPreview.value = true
|
|
}
|
|
|
|
// 编辑邮件
|
|
const editMail = (mail) => {
|
|
router.push(`/compose?edit=${mail.mailId}`)
|
|
}
|
|
|
|
// 取消邮件
|
|
const cancelMail = (mail) => {
|
|
Dialog.confirm({
|
|
title: '确认撤销',
|
|
message: '确定要撤销这封邮件的发送吗?撤销后将无法恢复。',
|
|
})
|
|
.then(async () => {
|
|
try {
|
|
const response = await mailActions.revokeMail(mail.mailId)
|
|
if (response.code === 200) {
|
|
showSuccessToast('邮件已撤销')
|
|
// 重新加载邮件列表
|
|
fetchMails(true)
|
|
} else {
|
|
showFailToast(response.message || '撤销失败')
|
|
}
|
|
} catch (error) {
|
|
console.error('撤销邮件失败:', error)
|
|
showFailToast('撤销失败')
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// 取消操作
|
|
})
|
|
}
|
|
|
|
// 应用排序
|
|
const applySort = () => {
|
|
showSort.value = false
|
|
showFailToast('排序已应用')
|
|
}
|
|
|
|
onMounted(() => {
|
|
generateStars()
|
|
fetchMails(true)
|
|
})
|
|
|
|
return {
|
|
active,
|
|
stars,
|
|
sortType,
|
|
showSort,
|
|
showPreview,
|
|
previewMailData,
|
|
sortedMails,
|
|
loading,
|
|
finished,
|
|
goBack,
|
|
formatDate,
|
|
getStatusType,
|
|
getStatusText,
|
|
getProgressText,
|
|
getProgressPercentage,
|
|
openMail,
|
|
previewMail,
|
|
editMail,
|
|
cancelMail,
|
|
applySort,
|
|
loadMore
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.sent-container {
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.mail-list {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 0 15px 15px;
|
|
}
|
|
|
|
.mail-item {
|
|
display: flex;
|
|
padding: 15px;
|
|
margin-bottom: 15px;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.mail-item:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 20px rgba(0, 212, 255, 0.2);
|
|
}
|
|
|
|
.mail-icon {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 50px;
|
|
height: 50px;
|
|
margin-right: 15px;
|
|
}
|
|
|
|
.time-capsule-small {
|
|
width: 30px;
|
|
height: 45px;
|
|
background: var(--gradient-color);
|
|
border-radius: 15px;
|
|
position: relative;
|
|
box-shadow: 0 5px 15px rgba(0, 212, 255, 0.3);
|
|
}
|
|
|
|
.time-capsule-small::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 8px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
width: 20px;
|
|
height: 20px;
|
|
background: radial-gradient(circle, var(--accent-color), transparent);
|
|
border-radius: 50%;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.time-capsule-small.delivered::before {
|
|
background: radial-gradient(circle, #4CAF50, transparent);
|
|
}
|
|
|
|
.mail-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.mail-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.mail-title {
|
|
margin: 0;
|
|
font-size: 16px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.mail-date {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.mail-preview {
|
|
margin: 0 0 10px;
|
|
font-size: 14px;
|
|
color: var(--text-secondary);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.mail-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.mail-recipient {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.mail-progress {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.progress-info {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 12px;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.progress-info span:first-child {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.progress-info span:last-child {
|
|
color: var(--accent-color);
|
|
}
|
|
|
|
.mail-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
margin-left: 10px;
|
|
}
|
|
|
|
.mail-actions .van-icon {
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: color 0.3s ease;
|
|
}
|
|
|
|
.mail-actions .van-icon:hover {
|
|
color: var(--accent-color);
|
|
}
|
|
|
|
.empty-state {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 200px;
|
|
}
|
|
|
|
.sort-popup {
|
|
padding: 20px;
|
|
height: 100%;
|
|
}
|
|
|
|
.sort-popup h3 {
|
|
margin: 0 0 15px;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.sort-actions {
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.preview-popup {
|
|
padding: 20px;
|
|
height: 100%;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.preview-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.preview-header h3 {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.preview-content {
|
|
background: rgba(255, 255, 255, 0.05);
|
|
border-radius: 8px;
|
|
padding: 15px;
|
|
}
|
|
|
|
.preview-info {
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.preview-info p {
|
|
margin: 5px 0;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.preview-body {
|
|
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
|
padding-top: 15px;
|
|
}
|
|
|
|
.preview-body p {
|
|
margin: 0;
|
|
line-height: 1.6;
|
|
}
|
|
</style> |