This commit is contained in:
2025-10-18 16:18:47 +08:00
parent 2b634ed4d4
commit ccc20f9abf
27 changed files with 5017 additions and 573 deletions

View File

@@ -1,3 +1,4 @@
# 开发环境配置
VUE_APP_API_BASE_URL=/api/v1
VUE_APP_TITLE=ChronoMail - 未来邮箱
VUE_APP_USE_MOCK_API=false

39
dist/report.html vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,13 +1,26 @@
<template>
<div id="app">
<GlobalBackground />
<router-view />
<ThemeSwitcher />
</div>
</template>
<script>
import GlobalBackground from '@/components/GlobalBackground.vue'
import ThemeSwitcher from '@/components/ThemeSwitcher.vue'
import { getCurrentTheme, applyTheme } from '@/utils/theme'
export default {
name: 'App',
components: {
GlobalBackground,
ThemeSwitcher
},
mounted() {
// 初始化主题
applyTheme(getCurrentTheme());
// 隐藏加载动画
setTimeout(() => {
const loadingContainer = document.querySelector('.loading-container');

View File

@@ -1,24 +1,40 @@
import api from './request'
import mockAPI from './mock'
// 检查是否使用模拟API
const useMockAPI = process.env.VUE_APP_USE_MOCK_API === 'true'
// 用户认证相关API
export const authAPI = {
// 用户注册
register(data) {
if (useMockAPI) {
return mockAPI.auth.register(data)
}
return api.post('/auth/register', data)
},
// 用户登录
login(data) {
if (useMockAPI) {
return mockAPI.auth.login(data)
}
return api.post('/auth/login', data)
},
// 刷新token
refreshToken(refreshToken) {
if (useMockAPI) {
return mockAPI.auth.refreshToken(refreshToken)
}
return api.post('/auth/refresh', { refreshToken })
},
// 退出登录
logout() {
if (useMockAPI) {
return mockAPI.auth.logout()
}
return api.post('/auth/logout')
}
}
@@ -27,32 +43,63 @@ export const authAPI = {
export const mailAPI = {
// 创建邮件
createMail(data) {
if (useMockAPI) {
return mockAPI.mail.createMail(data)
}
return api.post('/mails', data)
},
// 获取邮件列表
getMails(params) {
if (useMockAPI) {
return mockAPI.mail.getMails(params)
}
return api.get('/mails', { params })
},
// 获取邮件详情
getMailDetail(mailId) {
if (useMockAPI) {
return mockAPI.mail.getMailDetail(mailId)
}
return api.get(`/mails/${mailId}`)
},
// 更新邮件
updateMail(mailId, data) {
if (useMockAPI) {
return mockAPI.mail.updateMail(mailId, data)
}
return api.put(`/mails/${mailId}`, data)
},
// 删除邮件
deleteMail(mailId) {
if (useMockAPI) {
return mockAPI.mail.deleteMail(mailId)
}
return api.delete(`/mails/${mailId}`)
},
// 撤销发送
revokeMail(mailId) {
if (useMockAPI) {
return mockAPI.mail.revokeMail(mailId)
}
return api.post(`/mails/${mailId}/revoke`)
},
// 发送至未来
sendToFuture(mailId, sendTime, triggerType = 'TIME', triggerCondition = {}) {
if (useMockAPI) {
return mockAPI.mail.sendToFuture(mailId, sendTime, triggerType, triggerCondition)
}
return api.post(`/mails/send-to-future`, {
mailId,
sendTime,
triggerType,
triggerCondition
})
}
}
@@ -60,12 +107,42 @@ export const mailAPI = {
export const capsuleAPI = {
// 获取胶囊视图
getCapsules() {
if (useMockAPI) {
return mockAPI.capsule.getCapsules()
}
return api.get('/capsules')
},
// 更新胶囊样式
updateCapsuleStyle(capsuleId, style) {
if (useMockAPI) {
return mockAPI.capsule.updateCapsuleStyle(capsuleId, style)
}
return api.put(`/capsules/${capsuleId}/style`, { style })
},
// 存入胶囊 - 将邮件转换为胶囊
saveToCapsule(mailId, capsuleData) {
if (useMockAPI) {
return mockAPI.capsule.saveToCapsule(mailId, capsuleData)
}
return api.post(`/capsules/save/${mailId}`, capsuleData)
},
// 从胶囊中取出邮件
removeFromCapsule(capsuleId) {
if (useMockAPI) {
return mockAPI.capsule.removeFromCapsule(capsuleId)
}
return api.post(`/capsules/remove/${capsuleId}`)
},
// 获取胶囊详情
getCapsuleDetail(capsuleId) {
if (useMockAPI) {
return mockAPI.capsule.getCapsuleDetail(capsuleId)
}
return api.get(`/capsules/${capsuleId}`)
}
}
@@ -73,16 +150,25 @@ export const capsuleAPI = {
export const aiAPI = {
// 写作辅助
writingAssistant(data) {
if (useMockAPI) {
return mockAPI.ai.writingAssistant(data)
}
return api.post('/ai/writing-assistant', data)
},
// 情感分析
sentimentAnalysis(content) {
if (useMockAPI) {
return mockAPI.ai.sentimentAnalysis(content)
}
return api.post('/ai/sentiment-analysis', { content })
},
// 未来预测
futurePrediction(data) {
if (useMockAPI) {
return mockAPI.ai.futurePrediction(data)
}
return api.post('/ai/future-prediction', data)
}
}
@@ -91,26 +177,41 @@ export const aiAPI = {
export const userAPI = {
// 获取时间线
getTimeline(params) {
if (useMockAPI) {
return mockAPI.user.getTimeline(params)
}
return api.get('/timeline', { params })
},
// 获取统计数据
getStatistics() {
if (useMockAPI) {
return mockAPI.user.getStatistics()
}
return api.get('/statistics')
},
// 获取用户信息
getUserProfile() {
if (useMockAPI) {
return mockAPI.user.getUserProfile()
}
return api.get('/user/profile')
},
// 更新用户信息
updateUserProfile(data) {
if (useMockAPI) {
return mockAPI.user.updateUserProfile(data)
}
return api.put('/user/profile', data)
},
// 获取用户订阅信息
getSubscription() {
if (useMockAPI) {
return mockAPI.user.getSubscription()
}
return api.get('/user/subscription')
}
}
@@ -119,6 +220,9 @@ export const userAPI = {
export const uploadAPI = {
// 上传附件
uploadAttachment(file) {
if (useMockAPI) {
return mockAPI.upload.uploadAttachment(file)
}
const formData = new FormData()
formData.append('file', file)
@@ -131,6 +235,9 @@ export const uploadAPI = {
// 上传头像
uploadAvatar(file) {
if (useMockAPI) {
return mockAPI.upload.uploadAvatar(file)
}
const formData = new FormData()
formData.append('file', file)
@@ -146,16 +253,25 @@ export const uploadAPI = {
export const notificationAPI = {
// 注册设备
registerDevice(data) {
if (useMockAPI) {
return mockAPI.notification.registerDevice(data)
}
return api.post('/notification/device', data)
},
// 获取通知设置
getNotificationSettings() {
if (useMockAPI) {
return mockAPI.notification.getNotificationSettings()
}
return api.get('/notification/settings')
},
// 更新通知设置
updateNotificationSettings(data) {
if (useMockAPI) {
return mockAPI.notification.updateNotificationSettings(data)
}
return api.put('/notification/settings', data)
}
}

951
src/api/mock.js Normal file
View File

@@ -0,0 +1,951 @@
// API模拟服务用于在没有后端服务器的情况下模拟API响应
import { showFailToast } from 'vant'
// 模拟数据存储
const mockData = {
mails: [],
capsules: [],
users: []
}
// 初始化默认测试用户
const initDefaultUser = () => {
if (mockData.users.length === 0) {
mockData.users.push({
userId: 'test-user-1',
username: 'testuser',
email: 'test@example.com',
avatar: 'https://picsum.photos/seed/testuser/200/200.jpg',
createdAt: new Date().toISOString()
})
}
}
// 初始化默认用户
initDefaultUser()
// 生成唯一ID
const generateId = () => Date.now().toString() + Math.random().toString(36).substr(2, 9)
// 模拟API响应
const mockResponse = (data, message = 'success', code = 200) => {
return Promise.resolve({
code,
message,
data
})
}
// 模拟错误响应
const mockError = (message = '请求失败', code = 500) => {
showFailToast(message)
return Promise.reject({
code,
message
})
}
// 模拟延迟
const delay = (ms = 1000) => new Promise(resolve => setTimeout(resolve, ms))
// 模拟邮件API
export const mockMailAPI = {
// 创建邮件
async createMail(data) {
await delay(1000)
const mail = {
mailId: generateId(),
capsuleId: generateId(),
status: data.status || 'PENDING',
createdAt: new Date().toISOString(),
...data
}
mockData.mails.push(mail)
return mockResponse({
mailId: mail.mailId,
capsuleId: mail.capsuleId,
status: mail.status,
createdAt: mail.createdAt
})
},
// 获取邮件列表
async getMails(params = {}) {
await delay(800)
const { type = 'INBOX', status, page = 1, size = 10 } = params
let filteredMails = [...mockData.mails]
// 根据类型筛选
if (type === 'SENT') {
// 在实际应用中这里应该根据当前用户ID筛选发送的邮件
filteredMails = filteredMails.filter(mail => mail.recipientType === 'SELF' || mail.recipientType === 'PUBLIC')
} else if (type === 'DRAFT') {
filteredMails = filteredMails.filter(mail => mail.status === 'DRAFT')
}
// 根据状态筛选
if (status) {
filteredMails = filteredMails.filter(mail => mail.status === status)
}
// 分页
const total = filteredMails.length
const start = (page - 1) * size
const end = start + size
const list = filteredMails.slice(start, end)
// 格式化返回数据
const formattedList = list.map(mail => ({
mailId: mail.mailId,
title: mail.title,
sender: {
userId: '1',
username: '当前用户',
avatar: 'https://picsum.photos/seed/user1/100/100.jpg'
},
recipient: {
userId: mail.recipientType === 'SELF' ? '1' : '2',
username: mail.recipientType === 'SELF' ? '自己' : '收件人',
avatar: 'https://picsum.photos/seed/user2/100/100.jpg'
},
sendTime: mail.sendTime,
deliveryTime: mail.deliveryTime,
status: mail.status,
hasAttachments: mail.attachments && mail.attachments.length > 0,
isEncrypted: mail.isEncrypted,
capsuleStyle: mail.capsuleStyle,
countdown: mail.status === 'PENDING' ? Math.floor(Math.random() * 86400) : undefined
}))
return mockResponse({
list: formattedList,
total,
page,
size
})
},
// 获取邮件详情
async getMailDetail(mailId) {
await delay(500)
const mail = mockData.mails.find(m => m.mailId === mailId)
if (!mail) {
return mockError('邮件不存在', 404)
}
return mockResponse({
mailId: mail.mailId,
title: mail.title,
content: mail.content,
sender: {
userId: '1',
username: '当前用户',
avatar: 'https://picsum.photos/seed/user1/100/100.jpg',
email: 'current@example.com'
},
recipient: {
userId: mail.recipientType === 'SELF' ? '1' : '2',
username: mail.recipientType === 'SELF' ? '自己' : '收件人',
avatar: 'https://picsum.photos/seed/user2/100/100.jpg',
email: mail.recipientEmail || 'recipient@example.com'
},
sendTime: mail.sendTime,
createdAt: mail.createdAt,
deliveryTime: mail.deliveryTime,
status: mail.status,
triggerType: mail.triggerType,
triggerCondition: mail.triggerCondition,
attachments: mail.attachments || [],
isEncrypted: mail.isEncrypted,
capsuleStyle: mail.capsuleStyle,
canEdit: mail.status === 'DRAFT',
canRevoke: mail.status === 'PENDING'
})
},
// 更新邮件
async updateMail(mailId, data) {
await delay(800)
const index = mockData.mails.findIndex(m => m.mailId === mailId)
if (index === -1) {
return mockError('邮件不存在', 404)
}
mockData.mails[index] = {
...mockData.mails[index],
...data
}
return mockResponse({
mailId,
status: mockData.mails[index].status
})
},
// 删除邮件
async deleteMail(mailId) {
await delay(500)
const index = mockData.mails.findIndex(m => m.mailId === mailId)
if (index === -1) {
return mockError('邮件不存在', 404)
}
mockData.mails.splice(index, 1)
return mockResponse({ mailId })
},
// 撤销发送
async revokeMail(mailId) {
await delay(800)
const index = mockData.mails.findIndex(m => m.mailId === mailId)
if (index === -1) {
return mockError('邮件不存在', 404)
}
if (mockData.mails[index].status !== 'PENDING') {
return mockError('只能撤销待发送的邮件', 400)
}
mockData.mails[index].status = 'DRAFT'
return mockResponse({
mailId,
status: 'DRAFT'
})
},
// 发送至未来
async sendToFuture(mailId, sendTime, triggerType = 'TIME', triggerCondition = {}) {
await delay(1000)
// 查找邮件
const mail = mockData.mails.find(m => m.mailId === mailId)
if (!mail) {
return mockError('邮件不存在', 404)
}
// 检查邮件状态
if (mail.status !== 'DRAFT') {
return mockError('只能将草稿状态的邮件发送至未来', 400)
}
// 对于时间触发,检查发送时间是否在未来
if (triggerType === 'TIME' && sendTime) {
const now = new Date()
const futureTime = new Date(sendTime)
if (futureTime <= now) {
return mockError('发送时间必须晚于当前时间', 400)
}
}
// 对于条件触发如果没有发送时间设置为默认值1年后
if (triggerType !== 'TIME' && !sendTime) {
const futureDate = new Date()
futureDate.setFullYear(futureDate.getFullYear() + 1)
sendTime = futureDate.toISOString()
}
// 计算倒计时秒数(仅对时间触发有效)
let countdown
if (triggerType === 'TIME' && sendTime) {
const now = new Date()
const futureTime = new Date(sendTime)
countdown = Math.floor((futureTime.getTime() - now.getTime()) / 1000)
}
// 更新邮件状态为待发送
mail.status = 'PENDING'
mail.sendTime = sendTime
mail.triggerType = triggerType
mail.triggerCondition = triggerCondition
// 如果没有胶囊ID生成一个
if (!mail.capsuleId) {
mail.capsuleId = generateId()
}
// 添加到胶囊列表
const capsule = {
capsuleId: mail.capsuleId,
mailId: mail.mailId,
title: mail.title,
sendTime: mail.sendTime,
deliveryTime: mail.deliveryTime,
progress: 0,
position: {
x: Math.random(),
y: Math.random(),
z: Math.random()
},
style: mail.capsuleStyle || 'default',
glowIntensity: Math.random(),
createdAt: new Date().toISOString()
}
// 检查胶囊是否已存在
const existingIndex = mockData.capsules.findIndex(c => c.capsuleId === capsule.capsuleId)
if (existingIndex === -1) {
mockData.capsules.push(capsule)
} else {
mockData.capsules[existingIndex] = capsule
}
return mockResponse({
mailId: mail.mailId,
capsuleId: mail.capsuleId,
status: mail.status,
sendTime: mail.sendTime,
countdown,
updatedAt: new Date().toISOString()
})
}
}
// 模拟胶囊API
export const mockCapsuleAPI = {
// 获取胶囊视图
async getCapsules() {
await delay(800)
const capsules = mockData.mails
.filter(mail => mail.status === 'PENDING')
.map(mail => {
// 计算进度
const now = new Date()
const sendTime = new Date(mail.sendTime || now)
const createdTime = new Date(mail.createdAt)
const totalTime = sendTime.getTime() - createdTime.getTime()
const elapsedTime = now.getTime() - createdTime.getTime()
const progress = Math.min(1, Math.max(0, elapsedTime / totalTime))
return {
capsuleId: mail.capsuleId,
mailId: mail.mailId,
title: mail.title,
sendTime: mail.sendTime,
deliveryTime: mail.deliveryTime,
progress,
position: {
x: Math.random(),
y: Math.random(),
z: Math.random()
},
style: mail.capsuleStyle || 'default',
glowIntensity: Math.random()
}
})
return mockResponse({
capsules,
scene: 'SPACE',
background: 'space'
})
},
// 更新胶囊样式
async updateCapsuleStyle(capsuleId, style) {
await delay(500)
const mail = mockData.mails.find(m => m.capsuleId === capsuleId)
if (!mail) {
return mockError('胶囊不存在', 404)
}
mail.capsuleStyle = style
return mockResponse({ capsuleId, style })
},
// 存入胶囊 - 将邮件转换为胶囊
async saveToCapsule(mailId, capsuleData = {}) {
await delay(1000)
// 查找邮件
const mail = mockData.mails.find(m => m.mailId === mailId)
if (!mail) {
return mockError('邮件不存在', 404)
}
// 检查邮件状态
if (mail.status !== 'DRAFT') {
return mockError('只能将草稿状态的邮件存入胶囊', 400)
}
// 更新邮件状态为待发送
mail.status = 'PENDING'
// 更新胶囊相关数据
if (capsuleData.capsuleStyle) {
mail.capsuleStyle = capsuleData.capsuleStyle
}
if (capsuleData.position) {
mail.position = capsuleData.position
}
if (capsuleData.glowIntensity) {
mail.glowIntensity = capsuleData.glowIntensity
}
// 如果没有发送时间设置为默认值30天后
if (!mail.sendTime) {
const futureDate = new Date()
futureDate.setDate(futureDate.getDate() + 30)
mail.sendTime = futureDate.toISOString()
}
// 添加到胶囊列表
const capsule = {
capsuleId: mail.capsuleId,
mailId: mail.mailId,
title: mail.title,
sendTime: mail.sendTime,
deliveryTime: mail.deliveryTime,
progress: 0,
position: mail.position || {
x: Math.random(),
y: Math.random(),
z: Math.random()
},
style: mail.capsuleStyle || 'default',
glowIntensity: mail.glowIntensity || Math.random(),
createdAt: new Date().toISOString()
}
// 检查胶囊是否已存在
const existingIndex = mockData.capsules.findIndex(c => c.capsuleId === capsule.capsuleId)
if (existingIndex === -1) {
mockData.capsules.push(capsule)
} else {
mockData.capsules[existingIndex] = capsule
}
return mockResponse({
capsuleId: capsule.capsuleId,
mailId: capsule.mailId,
status: mail.status,
message: '邮件已成功存入胶囊'
})
},
// 从胶囊中取出邮件
async removeFromCapsule(capsuleId) {
await delay(800)
// 查找胶囊
const capsuleIndex = mockData.capsules.findIndex(c => c.capsuleId === capsuleId)
if (capsuleIndex === -1) {
return mockError('胶囊不存在', 404)
}
const capsule = mockData.capsules[capsuleIndex]
// 查找对应的邮件
const mail = mockData.mails.find(m => m.mailId === capsule.mailId)
if (mail) {
// 将邮件状态改回草稿
mail.status = 'DRAFT'
}
// 从胶囊列表中移除
mockData.capsules.splice(capsuleIndex, 1)
return mockResponse({
capsuleId,
status: 'DRAFT',
message: '邮件已从胶囊中取出'
})
},
// 获取胶囊详情
async getCapsuleDetail(capsuleId) {
await delay(500)
// 查找胶囊
const capsule = mockData.capsules.find(c => c.capsuleId === capsuleId)
if (!capsule) {
return mockError('胶囊不存在', 404)
}
// 查找对应的邮件
const mail = mockData.mails.find(m => m.mailId === capsule.mailId)
if (!mail) {
return mockError('关联邮件不存在', 404)
}
// 计算进度
const now = new Date()
const sendTime = new Date(mail.sendTime || now)
const createdTime = new Date(mail.createdAt)
const totalTime = sendTime.getTime() - createdTime.getTime()
const elapsedTime = now.getTime() - createdTime.getTime()
const progress = Math.min(1, Math.max(0, elapsedTime / totalTime))
return mockResponse({
capsuleId: capsule.capsuleId,
mailId: capsule.mailId,
title: mail.title,
content: mail.content,
sendTime: mail.sendTime,
createdAt: mail.createdAt,
deliveryTime: mail.deliveryTime,
status: mail.status,
triggerType: mail.triggerType,
triggerCondition: mail.triggerCondition,
attachments: mail.attachments || [],
isEncrypted: mail.isEncrypted,
capsuleStyle: mail.capsuleStyle,
progress,
position: capsule.position,
glowIntensity: capsule.glowIntensity,
canEdit: mail.status === 'DRAFT',
canRevoke: mail.status === 'PENDING'
})
}
}
// 模拟用户API
export const mockUserAPI = {
// 获取用户订阅信息
async getSubscription() {
await delay(500)
return mockResponse({
plan: 'FREE',
remainingMails: 10,
maxAttachmentSize: 10485760, // 10MB
features: {
advancedTriggers: false,
customCapsules: false,
aiAssistant: true
},
expireDate: null
})
},
// 获取统计数据
async getStatistics() {
await delay(500)
const sentMails = mockData.mails.filter(mail => mail.status !== 'DRAFT')
const receivedMails = [] // 在实际应用中,这里应该获取接收到的邮件
return mockResponse({
totalSent: sentMails.length,
totalReceived: receivedMails.length,
timeTravelDuration: 30, // 天
mostFrequentRecipient: '自己',
mostCommonYear: new Date().getFullYear(),
keywordCloud: [
{ word: '未来', count: 5, size: 20 },
{ word: '希望', count: 3, size: 16 },
{ word: '梦想', count: 2, size: 14 }
],
monthlyStats: [
{ month: '2023-12', sent: 3, received: 1 },
{ month: '2024-01', sent: 5, received: 2 }
]
})
}
}
// 模拟AI API
export const mockAiAPI = {
// 情感分析
async sentimentAnalysis(content) {
await delay(1500)
const sentiments = ['POSITIVE', 'NEUTRAL', 'NEGATIVE', 'MIXED']
const emotionTypes = ['HAPPY', 'SAD', 'HOPEFUL', 'NOSTALGIC', 'EXCITED']
const randomSentiment = sentiments[Math.floor(Math.random() * sentiments.length)]
const emotions = emotionTypes.slice(0, 3).map(type => ({
type,
score: Math.random()
}))
const summaries = [
"这封信充满了对未来的期待和希望,表达了积极向上的情感。",
"文字中透露出对过去时光的怀念和对未来的思考。",
"这封信情感真挚,表达了内心深处的感受和想法。",
"文字中既有对现实的思考,也有对未来的憧憬和规划。"
]
const randomSummary = summaries[Math.floor(Math.random() * summaries.length)]
return mockResponse({
sentiment: randomSentiment,
confidence: Math.random(),
emotions,
keywords: ['未来', '希望', '梦想'],
summary: randomSummary
})
}
}
// 模拟认证API
export const mockAuthAPI = {
// 用户注册
async register(data) {
await delay(1000)
const { username, email, password } = data
// 检查用户是否已存在
const existingUser = mockData.users.find(u => u.email === email)
if (existingUser) {
return mockError('用户已存在', 400)
}
const user = {
userId: generateId(),
username,
email,
avatar: null,
createdAt: new Date().toISOString()
}
mockData.users.push(user)
// 生成token
const token = 'mock-token-' + generateId()
const refreshToken = 'mock-refresh-token-' + generateId()
localStorage.setItem('token', token)
localStorage.setItem('refreshToken', refreshToken)
localStorage.setItem('userInfo', JSON.stringify(user))
return mockResponse({
userId: user.userId,
username: user.username,
email: user.email,
avatar: user.avatar,
token,
refreshToken
})
},
// 用户登录
async login(data) {
await delay(1000)
const { usernameOrEmail, password } = data
// 查找用户(支持用户名或邮箱登录)
const user = mockData.users.find(u =>
u.email === usernameOrEmail || u.username === usernameOrEmail
)
if (!user) {
return mockError('用户不存在', 404)
}
// 生成token
const token = 'mock-token-' + generateId()
const refreshToken = 'mock-refresh-token-' + generateId()
localStorage.setItem('token', token)
localStorage.setItem('refreshToken', refreshToken)
localStorage.setItem('userInfo', JSON.stringify(user))
return mockResponse({
userId: user.userId,
username: user.username,
email: user.email,
avatar: user.avatar,
token,
refreshToken
})
},
// 刷新token
async refreshToken(refreshToken) {
await delay(500)
if (!refreshToken) {
return mockError('刷新令牌无效', 401)
}
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
// 生成新token
const token = 'mock-token-' + generateId()
const newRefreshToken = 'mock-refresh-token-' + generateId()
localStorage.setItem('token', token)
localStorage.setItem('refreshToken', newRefreshToken)
return mockResponse({
token,
refreshToken: newRefreshToken
})
},
// 退出登录
async logout() {
await delay(500)
localStorage.removeItem('token')
localStorage.removeItem('refreshToken')
localStorage.removeItem('userInfo')
return mockResponse({})
}
}
// 模拟上传API
export const mockUploadAPI = {
// 上传附件
async uploadAttachment(file) {
await delay(1500)
// 模拟文件上传
const fileId = generateId()
const url = `https://picsum.photos/seed/${fileId}/800/600.jpg`
return mockResponse({
id: fileId,
url,
type: file.type.startsWith('image/') ? 'IMAGE' : 'FILE',
size: file.size,
name: file.name
})
},
// 上传头像
async uploadAvatar(file) {
await delay(1000)
// 模拟文件上传
const url = `https://picsum.photos/seed/avatar-${generateId()}/200/200.jpg`
return mockResponse({
url
})
}
}
// 模拟通知API
export const mockNotificationAPI = {
// 注册设备
async registerDevice(data) {
await delay(500)
return mockResponse({
deviceId: generateId(),
registered: true
})
},
// 获取通知设置
async getNotificationSettings() {
await delay(500)
return mockResponse({
emailNotifications: true,
pushNotifications: true,
deliveryNotifications: true,
reminderNotifications: false
})
},
// 更新通知设置
async updateNotificationSettings(data) {
await delay(500)
return mockResponse({
...data,
updated: true
})
}
}
// 补充用户API中缺少的方法
mockUserAPI.getUserProfile = async () => {
await delay(500)
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
if (!userInfo.userId) {
return mockError('用户未登录', 401)
}
return mockResponse({
userId: userInfo.userId,
username: userInfo.username,
email: userInfo.email,
avatar: userInfo.avatar || 'https://picsum.photos/seed/avatar/200/200.jpg',
bio: '这是我的个人简介',
joinDate: userInfo.createdAt || new Date().toISOString()
})
}
mockUserAPI.updateUserProfile = async (data) => {
await delay(800)
const userInfo = JSON.parse(localStorage.getItem('userInfo') || '{}')
if (!userInfo.userId) {
return mockError('用户未登录', 401)
}
// 更新用户信息
const updatedUser = {
...userInfo,
...data
}
// 更新本地存储
localStorage.setItem('userInfo', JSON.stringify(updatedUser))
// 更新模拟数据
const index = mockData.users.findIndex(u => u.userId === userInfo.userId)
if (index !== -1) {
mockData.users[index] = updatedUser
}
return mockResponse(updatedUser)
}
mockUserAPI.getTimeline = async (params = {}) => {
await delay(800)
const { startDate, endDate, type = 'ALL' } = params
// 获取邮件列表
let mails = [...mockData.mails]
// 根据类型筛选
if (type === 'SENT') {
// 在实际应用中这里应该根据当前用户ID筛选发送的邮件
mails = mails.filter(mail => mail.recipientType === 'SELF' || mail.recipientType === 'PUBLIC')
} else if (type === 'RECEIVED') {
// 在实际应用中这里应该根据当前用户ID筛选接收的邮件
mails = mails.filter(mail => mail.recipientType === 'SPECIFIC')
}
// 根据日期范围筛选
if (startDate) {
mails = mails.filter(mail => new Date(mail.sendTime || mail.createdAt) >= new Date(startDate))
}
if (endDate) {
mails = mails.filter(mail => new Date(mail.sendTime || mail.createdAt) <= new Date(endDate))
}
// 按日期分组
const timeline = {}
mails.forEach(mail => {
const date = new Date(mail.sendTime || mail.createdAt).toISOString().split('T')[0]
if (!timeline[date]) {
timeline[date] = []
}
timeline[date].push({
type: mail.recipientType === 'SELF' ? 'SENT' : 'RECEIVED',
mailId: mail.mailId,
title: mail.title,
time: mail.sendTime || mail.createdAt,
withUser: {
userId: mail.recipientType === 'SELF' ? '1' : '2',
username: mail.recipientType === 'SELF' ? '自己' : '收件人',
avatar: 'https://picsum.photos/seed/user2/100/100.jpg'
},
emotion: 'POSITIVE' // 模拟情感
})
})
// 转换为数组格式
const timelineArray = Object.keys(timeline).map(date => ({
date,
events: timeline[date]
}))
return mockResponse({
timeline: timelineArray
})
}
// 补充AI API中缺少的方法
mockAiAPI.writingAssistant = async (data) => {
await delay(2000)
const { prompt, type = 'DRAFT', tone = 'CASUAL', length = 'MEDIUM' } = data
// 模拟写作辅助结果
let content = ''
if (type === 'OUTLINE') {
content = `1. 引言\n - 表达对${prompt}的看法\n - 提出主要观点\n\n2. 主体\n - 详细阐述观点\n - 举例说明\n\n3. 结论\n - 总结观点\n - 展望未来`
} else if (type === 'DRAFT') {
content = `关于${prompt},我想说的是...\n\n这是我想表达的主要内容。通过这封信,我希望能够传达我的想法和感受。\n\n在未来,我相信...`
} else {
content = `亲爱的未来的自己,\n\n当你读到这封信时,我希望你能够回想起现在关于${prompt}的想法。\n\n现在的我,对未来充满了期待和希望。我相信通过努力,我们能够实现自己的梦想。\n\n愿你一切都好。\n\n过去的你`
}
const suggestions = [
'可以添加更多个人情感',
'考虑加入具体事例',
'可以调整语气使其更加亲切',
'建议增加对未来的具体规划'
]
return mockResponse({
content,
suggestions: suggestions.slice(0, 2),
estimatedTime: Math.floor(Math.random() * 10) + 5
})
}
mockAiAPI.futurePrediction = async (data) => {
await delay(2000)
const { keywords, timeframe } = data
const predictions = [
`在未来${timeframe}内,关于${keywords}的发展将会超出我们的想象。`,
`根据当前趋势,${timeframe}${keywords}将成为社会的重要组成部分。`,
`预测${timeframe}后,${keywords}将带来革命性的变化。`
]
const randomPrediction = predictions[Math.floor(Math.random() * predictions.length)]
return mockResponse({
prediction: randomPrediction,
confidence: Math.random() * 0.5 + 0.5, // 0.5-1.0
relatedTopics: ['科技发展', '社会变迁', '个人成长']
})
}
export default {
auth: mockAuthAPI,
mail: mockMailAPI,
capsule: mockCapsuleAPI,
ai: mockAiAPI,
user: mockUserAPI,
upload: mockUploadAPI,
notification: mockNotificationAPI
}

View File

@@ -30,7 +30,7 @@ api.interceptors.response.use(
response => {
const res = response.data
// 如果响应中的success不是true,则判断为错误
// 如果响应中的code不是200,则判断为错误
if (res.success !== true) {
showFailToast(res.message || '请求失败')

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,121 @@
<template>
<div class="bottom-tabbar-container">
<!-- 底部导航 -->
<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>
<!-- 中心发布邮件按钮 -->
<div class="center-publish-button" @click="goToCompose">
<van-icon name="plus" size="24" />
</div>
<van-tabbar-item icon="send-o" to="/sent">发件箱</van-tabbar-item>
<van-tabbar-item icon="user-o" to="/profile">个人中心</van-tabbar-item>
</van-tabbar>
</div>
</template>
<script>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { Icon } from 'vant'
export default {
name: 'BottomTabbar',
components: {
[Icon.name]: Icon
},
props: {
currentActive: {
type: Number,
default: 0
}
},
setup(props, { emit }) {
const router = useRouter()
const active = ref(props.currentActive)
// 跳转到撰写页面
const goToCompose = () => {
router.push('/compose')
}
return {
active,
goToCompose
}
},
watch: {
currentActive(newVal) {
this.active = newVal
},
active(newVal) {
this.$emit('update:currentActive', newVal)
}
}
}
</script>
<style scoped>
.bottom-tabbar-container {
position: relative;
width: 100%;
}
.custom-tabbar {
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(10px);
border-top: 1px solid rgba(255, 255, 255, 0.2);
}
/* 中心发布按钮样式 */
.center-publish-button {
position: relative;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
margin-top: -20px;
transition: all 0.3s ease;
z-index: 10;
}
.center-publish-button:active {
transform: scale(0.95);
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.6);
}
/* 调整van-tabbar-item的样式为中间按钮留出空间 */
.custom-tabbar :deep(.van-tabbar-item) {
flex: 1;
}
/* 确保中间位置没有van-tabbar-item的背景 */
.custom-tabbar :deep(.van-tabbar-item:nth-child(3)) {
position: relative;
}
/* 为中心按钮创建一个圆形背景 */
.custom-tabbar :deep(.van-tabbar) {
position: relative;
}
.custom-tabbar :deep(.van-tabbar)::after {
content: '';
position: absolute;
top: -15px;
left: 50%;
transform: translateX(-50%);
width: 70px;
height: 35px;
background: rgba(255, 255, 255, 0.85);
border-radius: 35px 35px 0 0;
z-index: 1;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div class="global-background">
<div class="stars" ref="stars"></div>
</div>
</template>
<script>
import { onMounted, ref, watch } from 'vue'
import { getCurrentTheme } from '../utils/theme'
export default {
name: 'GlobalBackground',
setup() {
const stars = ref(null)
const currentTheme = ref(getCurrentTheme())
// 生成星空背景
const generateStars = () => {
if (!stars.value) return
// 清空现有星星
stars.value.innerHTML = ''
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)
}
}
// 监听主题变化
watch(currentTheme, () => {
generateStars()
})
onMounted(() => {
generateStars()
})
return {
stars
}
}
}
</script>
<style scoped>
.global-background {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--gradient-color);
z-index: -1;
overflow: hidden;
}
.stars {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.star {
position: absolute;
background-color: var(--star-color);
border-radius: 50%;
animation: twinkle 4s infinite;
}
</style>

122
src/components/README.md Normal file
View File

@@ -0,0 +1,122 @@
# 搜索组件 (SearchComponent)
## 概述
SearchComponent 是一个基于 Vant UI 组件库开发的搜索组件,已从 Home.vue 页面中提取出来,实现了搜索功能的模块化和复用性。
## 功能特性
1. **全屏搜索界面** - 提供沉浸式的搜索体验
2. **搜索建议** - 显示热门搜索标签,快速选择搜索内容
3. **搜索历史** - 自动保存和显示搜索历史记录
4. **搜索结果** - 展示搜索结果,支持不同类型的内容
5. **搜索筛选** - 支持按内容类型筛选搜索结果
6. **响应式设计** - 适配移动端和桌面端
## 使用方法
### 基本用法
```vue
<template>
<div>
<!-- 其他内容 -->
<SearchComponent v-model="showSearch" />
</div>
</template>
<script>
import { ref } from 'vue'
import SearchComponent from '@/components/SearchComponent.vue'
export default {
components: {
SearchComponent
},
setup() {
const showSearch = ref(false)
return {
showSearch
}
}
}
</script>
```
### 组件属性
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| modelValue | Boolean | false | 控制搜索组件的显示/隐藏 |
### 组件事件
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| update:modelValue | 显示状态变化时触发 | (value: boolean) |
| search | 执行搜索时触发 | (query: string, filters: string[]) |
| result-click | 点击搜索结果时触发 | (result: SearchResult) |
### 搜索结果类型
```typescript
interface SearchResult {
id: string
type: 'capsule' | 'sent' | 'received' | 'draft'
title: string
description: string
date: string
capsuleId?: string
mailId?: string
}
```
## 自定义配置
### 修改搜索建议
在 SearchComponent.vue 中修改 `searchSuggestions` 数组:
```javascript
const searchSuggestions = ref(['新年愿望', '生日祝福', '未来寄语', '毕业纪念', '爱情宣言'])
```
### 修改搜索历史存储键名
在 SearchComponent.vue 中修改 `HISTORY_STORAGE_KEY` 常量:
```javascript
const HISTORY_STORAGE_KEY = 'myApp_searchHistory'
```
### 修改模拟搜索结果
`generateMockSearchResults` 函数中自定义搜索结果的生成逻辑。
## 样式定制
组件使用了 Vant 的主题变量,可以通过修改 CSS 变量来自定义样式:
```css
.search-popup-container {
--primary-color: #1989fa;
--background-color: #1a1a2e;
--text-color: #ffffff;
}
```
## 注意事项
1. 组件依赖 Vant UI 组件库,使用前请确保已正确安装和配置
2. 搜索历史使用 localStorage 存储,注意浏览器兼容性
3. 当前搜索结果为模拟数据,实际使用时需要替换为真实的 API 调用
4. 组件使用了 Vue 3 的 Composition API需要 Vue 3 环境
## 更新日志
### v1.0.0
- 从 Home.vue 中提取搜索功能为独立组件
- 使用 Vant UI 组件重写样式
- 实现搜索建议、历史记录和结果展示功能
- 添加搜索筛选器功能

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
<template>
<div class="theme-switcher">
<van-popup
v-model:show="showThemePicker"
position="bottom"
round
:style="{ height: '60%' }"
>
<div class="theme-picker-container">
<div class="theme-picker-header">
<div class="header-title">选择主题</div>
<van-icon name="cross" @click="showThemePicker = false" />
</div>
<div class="theme-list">
<div
v-for="theme in themes"
:key="theme.id"
class="theme-item"
:class="{ active: currentThemeId === theme.id }"
@click="selectTheme(theme.id)"
>
<div class="theme-preview" :style="{ background: theme.preview }"></div>
<div class="theme-info">
<div class="theme-name">{{ theme.name }}</div>
<div class="theme-description">{{ theme.description }}</div>
</div>
<van-icon v-if="currentThemeId === theme.id" name="success" class="theme-selected" />
</div>
</div>
</div>
</van-popup>
<div class="theme-switcher-btn" @click="showThemePicker = true">
<van-icon name="photo-o" />
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue'
import { getCurrentTheme, setTheme, getAvailableThemes } from '@/utils/theme'
export default {
name: 'ThemeSwitcher',
setup() {
const showThemePicker = ref(false)
const currentThemeId = ref('')
const themes = ref([])
// 初始化主题
const initThemes = () => {
currentThemeId.value = getCurrentTheme()
themes.value = getAvailableThemes()
}
// 选择主题
const selectTheme = (themeId) => {
setTheme(themeId)
currentThemeId.value = themeId
showThemePicker.value = false
}
onMounted(() => {
initThemes()
})
return {
showThemePicker,
currentThemeId,
themes,
selectTheme
}
}
}
</script>
<style scoped>
.theme-switcher {
position: fixed;
bottom: 80px;
right: 20px;
z-index: 1000;
}
.theme-switcher-btn {
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--button-gradient);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: all 0.3s ease;
}
.theme-switcher-btn:hover {
transform: scale(1.05);
}
.theme-switcher-btn:active {
transform: scale(0.95);
}
.theme-picker-container {
height: 100%;
display: flex;
flex-direction: column;
background: var(--glass-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
}
.theme-picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--glass-border);
}
.header-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.theme-list {
flex: 1;
overflow-y: auto;
padding: 16px 20px;
}
.theme-item {
display: flex;
align-items: center;
padding: 16px;
margin-bottom: 12px;
border-radius: 12px;
background: var(--glass-bg);
border: 1px solid var(--glass-border);
cursor: pointer;
transition: all 0.3s ease;
}
.theme-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.theme-item.active {
border-color: var(--accent-color);
background: var(--glass-bg-active);
}
.theme-preview {
width: 60px;
height: 60px;
border-radius: 12px;
margin-right: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.theme-info {
flex: 1;
}
.theme-name {
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 4px;
}
.theme-description {
font-size: 14px;
color: var(--text-secondary);
line-height: 1.4;
}
.theme-selected {
color: var(--accent-color);
font-size: 20px;
}
</style>

View File

@@ -1,4 +1,113 @@
import { touchDirectives } from './touch';
/**
* 移动端触摸交互指令
*/
// 触摸反馈指令
const touchFeedback = {
mounted(el, binding) {
const { value = {} } = binding;
const { scale = 0.95, duration = 200 } = value;
el.addEventListener('touchstart', function() {
el.style.transition = `transform ${duration}ms`;
el.style.transform = `scale(${scale})`;
}, { passive: true });
el.addEventListener('touchend', function() {
el.style.transform = 'scale(1)';
}, { passive: true });
el.addEventListener('touchcancel', function() {
el.style.transform = 'scale(1)';
}, { passive: true });
}
};
// 滑动指令
const swipe = {
mounted(el, binding) {
const { value = {} } = binding;
const {
left = null,
right = null,
up = null,
down = null,
threshold = 50
} = value;
let startX = 0;
let startY = 0;
el.addEventListener('touchstart', function(e) {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}, { passive: true });
el.addEventListener('touchend', function(e) {
if (!e.changedTouches.length) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const deltaX = endX - startX;
const deltaY = endY - startY;
// 检测水平滑动
if (Math.abs(deltaX) > Math.abs(deltaY) && Math.abs(deltaX) > threshold) {
if (deltaX > 0 && right) {
right();
} else if (deltaX < 0 && left) {
left();
}
}
// 检测垂直滑动
else if (Math.abs(deltaY) > threshold) {
if (deltaY > 0 && down) {
down();
} else if (deltaY < 0 && up) {
up();
}
}
}, { passive: true });
}
};
// 长按指令
const longPress = {
mounted(el, binding) {
const { value = {} } = binding;
const { duration = 500, callback = null } = value;
let pressTimer = null;
el.addEventListener('touchstart', function() {
pressTimer = setTimeout(() => {
if (callback) callback();
}, duration);
}, { passive: true });
el.addEventListener('touchend', function() {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
}, { passive: true });
el.addEventListener('touchmove', function() {
if (pressTimer) {
clearTimeout(pressTimer);
pressTimer = null;
}
}, { passive: true });
}
};
// 导出所有触摸指令
const touchDirectives = {
'touch-feedback': touchFeedback,
'swipe': swipe,
'long-press': longPress
};
/**
* 移动端触摸交互Vue插件

View File

@@ -179,7 +179,7 @@ export const userActions = {
try {
const res = await api.user.getSubscription()
// 根据后端实际返回的数据结构提取数据
userState.subscription = res.data.data
userState.subscription = res.data
return res
} catch (error) {
console.error('获取订阅信息失败:', error)

152
src/utils/theme.js Normal file
View File

@@ -0,0 +1,152 @@
import { ref, computed } from 'vue'
// 主题配置
const themes = {
starryNight: {
name: '星空之夜',
primary: '#0F1C2E',
secondary: '#1D3B5A',
accent: '#00D4FF',
gradient: 'linear-gradient(135deg, #1D3B5A, #0F1C2E)',
textPrimary: '#ffffff',
textSecondary: '#a0b3d0',
glassBg: 'rgba(255, 255, 255, 0.1)',
glassBorder: 'rgba(255, 255, 255, 0.2)',
buttonGradient: 'linear-gradient(135deg, #00D4FF, #0099CC)',
buttonHoverGradient: 'linear-gradient(135deg, #0099CC, #006699)',
starColor: '#ffffff',
starAnimation: 'twinkle'
},
oceanDeep: {
name: '深海秘境',
primary: '#0A1929',
secondary: '#134074',
accent: '#00BCD4',
gradient: 'linear-gradient(135deg, #134074, #0A1929)',
textPrimary: '#ffffff',
textSecondary: '#B0E0E6',
glassBg: 'rgba(255, 255, 255, 0.08)',
glassBorder: 'rgba(0, 188, 212, 0.3)',
buttonGradient: 'linear-gradient(135deg, #00BCD4, #0097A7)',
buttonHoverGradient: 'linear-gradient(135deg, #0097A7, #00838F)',
starColor: '#00E5FF',
starAnimation: 'float'
},
sunsetGlow: {
name: '晚霞余晖',
primary: '#2D1B69',
secondary: '#624CAB',
accent: '#FF6B6B',
gradient: 'linear-gradient(135deg, #624CAB, #2D1B69)',
textPrimary: '#ffffff',
textSecondary: '#F8B500',
glassBg: 'rgba(255, 107, 107, 0.1)',
glassBorder: 'rgba(248, 181, 0, 0.3)',
buttonGradient: 'linear-gradient(135deg, #FF6B6B, #FF8E53)',
buttonHoverGradient: 'linear-gradient(135deg, #FF8E53, #FF6B6B)',
starColor: '#FFE66D',
starAnimation: 'pulse'
},
aurora: {
name: '极光幻彩',
primary: '#0F2027',
secondary: '#203A43',
accent: '#2C5364',
gradient: 'linear-gradient(135deg, #203A43, #0F2027, #2C5364)',
textPrimary: '#ffffff',
textSecondary: '#D4E6F1',
glassBg: 'rgba(44, 83, 100, 0.1)',
glassBorder: 'rgba(212, 230, 241, 0.3)',
buttonGradient: 'linear-gradient(135deg, #2C5364, #203A43)',
buttonHoverGradient: 'linear-gradient(135deg, #203A43, #0F2027)',
starColor: '#00FF88',
starAnimation: 'shimmer'
},
galaxy: {
name: '银河漫游',
primary: '#1A0033',
secondary: '#330867',
accent: '#C77DFF',
gradient: 'linear-gradient(135deg, #330867, #1A0033)',
textPrimary: '#ffffff',
textSecondary: '#E0AAFF',
glassBg: 'rgba(199, 125, 255, 0.1)',
glassBorder: 'rgba(224, 170, 255, 0.3)',
buttonGradient: 'linear-gradient(135deg, #C77DFF, #9D4EDD)',
buttonHoverGradient: 'linear-gradient(135deg, #9D4EDD, #7B2CBF)',
starColor: '#E0AAFF',
starAnimation: 'twinkle'
}
}
// 当前主题
const currentTheme = ref(localStorage.getItem('selectedTheme') || 'starryNight')
// 获取当前主题名称
const getCurrentTheme = () => {
return localStorage.getItem('selectedTheme') || 'starryNight'
}
// 切换主题
const setTheme = (themeName) => {
if (themes[themeName]) {
currentTheme.value = themeName
localStorage.setItem('selectedTheme', themeName)
applyThemeToDocument(themes[themeName])
return true
}
return false
}
// 应用主题到文档
const applyThemeToDocument = (theme) => {
const root = document.documentElement
// 设置CSS变量
root.style.setProperty('--primary-color', theme.primary)
root.style.setProperty('--secondary-color', theme.secondary)
root.style.setProperty('--accent-color', theme.accent)
root.style.setProperty('--gradient-color', theme.gradient)
root.style.setProperty('--text-primary', theme.textPrimary)
root.style.setProperty('--text-secondary', theme.textSecondary)
root.style.setProperty('--glass-bg', theme.glassBg)
root.style.setProperty('--glass-border', theme.glassBorder)
root.style.setProperty('--button-gradient', theme.buttonGradient)
root.style.setProperty('--button-hover-gradient', theme.buttonHoverGradient)
root.style.setProperty('--star-color', theme.starColor)
}
// 应用主题
const applyTheme = (themeName) => {
const theme = themes[themeName] || themes.starryNight
applyThemeToDocument(theme)
// 设置主题类名
document.body.className = `theme-${themeName}`
}
// 初始化主题
const initTheme = () => {
const themeName = getCurrentTheme()
applyTheme(themeName)
}
// 获取所有可用主题
const getAvailableThemes = () => {
return Object.keys(themes).map(key => ({
id: key,
name: themes[key].name,
description: themes[key].description || `美丽的${themes[key].name}主题`,
preview: themes[key].gradient
}))
}
export {
themes,
currentTheme,
getCurrentTheme,
setTheme,
applyTheme,
initTheme,
getAvailableThemes
}

View File

@@ -1,9 +1,5 @@
<template>
<div class="page">
<div class="space-background">
<div class="stars" ref="stars"></div>
</div>
<div class="page-content">
<van-nav-bar
title="API示例"
@@ -200,7 +196,6 @@ export default {
name: 'ApiDemo',
setup() {
const router = useRouter()
const stars = ref(null)
// 登录表单
const loginForm = reactive({
@@ -224,33 +219,6 @@ export default {
const aiPrompt = ref('')
const aiLoading = ref(false)
// 初始化星空背景
const initStars = () => {
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 size = Math.random() * 3 + 1
star.style.width = `${size}px`
star.style.height = `${size}px`
// 随机位置
star.style.left = `${Math.random() * 100}%`
star.style.top = `${Math.random() * 100}%`
// 随机动画延迟
star.style.animationDelay = `${Math.random() * 4}s`
starsContainer.appendChild(star)
}
}
// 处理登录
const handleLogin = async () => {
if (!loginForm.email || !loginForm.password) {
@@ -382,11 +350,10 @@ export default {
}
onMounted(() => {
initStars()
// 组件挂载后的初始化代码
})
return {
stars,
loginForm,
loginLoading,
mails,

View File

@@ -1,11 +1,5 @@
<template>
<div class="capsule-detail-container">
<!-- 深空背景 -->
<div class="space-background">
<div class="stars" ref="stars"></div>
<div class="floating-particles" ref="particles"></div>
</div>
<!-- 顶部导航 -->
<div class="header glass-card">
<van-icon name="arrow-left" size="24" @click="goBack" />
@@ -147,8 +141,6 @@ export default {
setup() {
const router = useRouter()
const route = useRoute()
const stars = ref(null)
const particles = ref(null)
const capsule3d = ref(null)
// 胶囊数据
@@ -164,70 +156,6 @@ export default {
// 获取胶囊ID
const mailId = computed(() => route.params.id)
// 生成星空背景
const generateStars = () => {
if (!stars.value) return
const starsContainer = stars.value
const starCount = 200
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 generateParticles = () => {
if (!particles.value) return
const particlesContainer = particles.value
const particleCount = 30
for (let i = 0; i < particleCount; i++) {
const particle = document.createElement('div')
particle.className = 'particle'
// 随机位置
const left = Math.random() * 100
const top = Math.random() * 100
// 随机大小
const size = Math.random() * 6 + 2
// 随机动画延迟和持续时间
const delay = Math.random() * 10
const duration = Math.random() * 20 + 20
particle.style.left = `${left}%`
particle.style.top = `${top}%`
particle.style.width = `${size}px`
particle.style.height = `${size}px`
particle.style.animationDelay = `${delay}s`
particle.style.animationDuration = `${duration}s`
particlesContainer.appendChild(particle)
}
}
// 加载胶囊数据
const loadCapsuleData = async () => {
try {
@@ -355,14 +283,10 @@ export default {
}
onMounted(() => {
generateStars()
generateParticles()
loadCapsuleData()
})
return {
stars,
particles,
capsule3d,
capsuleData,
isCapsuleOpened,
@@ -692,29 +616,4 @@ export default {
transform: translateX(5px);
}
}
/* 粒子效果 */
.particle {
position: absolute;
background: radial-gradient(circle, rgba(0, 212, 255, 0.8) 0%, rgba(0, 212, 255, 0) 70%);
border-radius: 50%;
animation: float-particle linear infinite;
}
@keyframes float-particle {
0% {
transform: translateY(100vh) rotate(0deg);
opacity: 0;
}
10% {
opacity: 1;
}
90% {
opacity: 1;
}
100% {
transform: translateY(-100vh) rotate(720deg);
opacity: 0;
}
}
</style>

View File

@@ -1,10 +1,5 @@
<template>
<div class="compose-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" />
@@ -192,12 +187,12 @@
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showLoadingToast, showSuccessToast, showFailToast, closeToast, Dialog } from 'vant'
import { mailAPI, capsuleAPI } from '../api'
export default {
name: 'Compose',
setup() {
const router = useRouter()
const stars = ref(null)
// 表单数据
const recipientType = ref('SELF') // 对应API的SELF, SPECIFIC, PUBLIC
@@ -324,37 +319,6 @@ export default {
}
}
// 生成星空背景
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 goBack = () => {
router.back()
@@ -590,32 +554,59 @@ export default {
return
}
if (!content.value) {
showFailToast('请填写邮件内容')
return
}
try {
showLoadingToast({
message: '保存中...',
forbidClick: true,
})
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1000))
const mailData = {
...buildMailData(),
status: 'DRAFT'
}
// 保存到本地存储作为草稿
const mailData = buildMailData()
const drafts = JSON.parse(localStorage.getItem('draftMails') || '[]')
drafts.push({
...mailData,
id: Date.now().toString(),
status: 'DRAFT',
createdAt: new Date().toISOString()
})
localStorage.setItem('draftMails', JSON.stringify(drafts))
// 先创建邮件草稿
const mailResponse = await mailAPI.createMail(mailData)
if (mailResponse.code !== 200) {
closeToast()
showFailToast(mailResponse.message || '保存失败')
return
}
// 将邮件存入胶囊
const capsuleData = {
capsuleStyle: capsuleStyle.value,
position: {
x: Math.random(),
y: Math.random(),
z: Math.random()
},
glowIntensity: Math.random()
}
const capsuleResponse = await capsuleAPI.saveToCapsule(
mailResponse.data.mailId,
capsuleData
)
closeToast()
showSuccessToast('草稿已保存')
if (capsuleResponse.code === 200) {
showSuccessToast('邮件已存入胶囊')
router.back()
} else {
showFailToast(capsuleResponse.message || '存入胶囊失败')
}
} catch (error) {
closeToast()
const errorMessage = error.response?.data?.message || '保存失败,请重试'
console.error('存入胶囊失败:', error)
const errorMessage = error.response?.data?.message || '存入胶囊失败,请重试'
showFailToast(errorMessage)
}
}
@@ -643,34 +634,48 @@ export default {
forbidClick: true,
})
// 模拟API调用延迟
await new Promise(resolve => setTimeout(resolve, 1500))
// 先构建邮件数据
const mailData = buildMailData()
// 保存到本地存储作为已发送邮件
const sentMails = JSON.parse(localStorage.getItem('sentMails') || '[]')
sentMails.push({
// 创建邮件草稿
const createMailData = {
...mailData,
id: Date.now().toString(),
status: 'PENDING',
createdAt: new Date().toISOString()
})
localStorage.setItem('sentMails', JSON.stringify(sentMails))
status: 'DRAFT'
}
const mailResponse = await mailAPI.createMail(createMailData)
if (mailResponse.code !== 200) {
closeToast()
showFailToast(mailResponse.message || '创建邮件失败')
return
}
// 获取发送时间和触发条件
const { sendTime, triggerType, triggerCondition } = mailData
// 调用发送至未来API
const response = await mailAPI.sendToFuture(
mailResponse.data.mailId,
sendTime,
triggerType,
triggerCondition
)
if (response.code === 200) {
closeToast()
// 计算发送时间用于显示
let deliveryDate
if (timeType.value === 'preset' || timeType.value === 'custom') {
deliveryDate = new Date(mailData.sendTime)
deliveryDate = new Date(sendTime)
} else {
deliveryDate = new Date()
deliveryDate.setFullYear(deliveryDate.getFullYear() + 1) // 默认显示一年后
}
Dialog.confirm({
title: '邮件已发送',
title: '邮件已发送至未来',
message: `您的邮件将在${deliveryDate.toLocaleDateString()}送达,是否返回首页?`,
confirmButtonText: '返回首页',
cancelButtonText: '继续撰写',
@@ -681,19 +686,23 @@ export default {
.catch(() => {
// 继续撰写
})
} else {
closeToast()
showFailToast(response.message || '发送失败')
}
} catch (error) {
closeToast()
console.error('发送邮件失败:', error)
const errorMessage = error.response?.data?.message || '发送失败,请重试'
showFailToast(errorMessage)
}
}
onMounted(() => {
generateStars()
// 组件初始化
})
return {
stars,
recipientType,
recipientEmail,
timeType,

View File

@@ -1,10 +1,5 @@
<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">
@@ -54,36 +49,11 @@
</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>
<BottomTabbar v-model:current-active="active" />
<!-- 搜索弹窗 -->
<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>
<SearchComponent v-model="showSearch" />
<!-- 通知弹窗 -->
<van-popup v-model:show="showNotifications" position="top" :style="{ height: '40%' }">
@@ -112,13 +82,18 @@ import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { showFailToast } from 'vant'
import { userState, mailState, capsuleState, mailActions, capsuleActions } from '../store'
import SearchComponent from '@/components/SearchComponent.vue'
import BottomTabbar from '@/components/BottomTabbar.vue'
export default {
components: {
SearchComponent,
BottomTabbar
},
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)
@@ -139,37 +114,6 @@ export default {
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, index) => {
// 添加基于索引的延迟动画
@@ -262,24 +206,14 @@ export default {
router.push(`/capsule/${capsule.mailId || capsule.capsuleId || capsule.id}`)
}
// 跳转到撰写页面
const goToCompose = () => {
router.push('/compose')
}
// 搜索处理
const onSearch = (value) => {
if (!value) {
const onSearch = () => {
if (!searchValue.value.trim()) {
showFailToast('请输入搜索内容')
return
}
// 暂时显示提示,因为搜索页面尚未实现
showFailToast(`搜索功能开发中,您搜索了: ${value}`)
showSearch.value = false
// TODO: 实现搜索功能后,可以跳转到搜索页面
// router.push(`/search?q=${encodeURIComponent(value)}`)
showFailToast(`搜索: ${searchValue.value}`)
// TODO: 实现搜索功能
}
// 获取时光胶囊数据
@@ -314,7 +248,6 @@ export default {
}
onMounted(async () => {
generateStars()
await fetchCapsules()
await fetchNotifications()
})
@@ -323,12 +256,10 @@ export default {
active,
userName,
greetingText,
stars,
capsulesSpace,
capsules,
showSearch,
showNotifications,
searchValue,
notifications,
activeCapsule,
getCapsuleStyle,
@@ -341,7 +272,6 @@ export default {
formatDate,
formatTime,
openCapsule,
goToCompose,
onSearch
}
}
@@ -538,30 +468,6 @@ export default {
}
}
.fab-container {
position: absolute;
bottom: 80px;
right: 20px;
z-index: 10;
}
.fab-button {
width: 60px;
height: 60px;
border-radius: 50%;
border: none;
font-size: 14px;
min-height: 44px;
min-width: 44px;
-webkit-tap-highlight-color: transparent;
transition: transform 0.2s, box-shadow 0.2s;
}
.fab-button:active {
transform: scale(0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
}
.search-popup {
padding: 20px;
}

View File

@@ -1,10 +1,5 @@
<template>
<div class="inbox-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" />
@@ -140,7 +135,6 @@ export default {
const router = useRouter()
const active = ref(1)
const activeTab = ref(0)
const stars = ref(null)
// 使用直接导入的状态和操作
const mails = computed(() => mailState.inboxList || [])
@@ -211,37 +205,6 @@ export default {
}
}
// 生成星空背景
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 goBack = () => {
router.back()
@@ -325,14 +288,12 @@ export default {
}
onMounted(() => {
generateStars()
fetchMails(true)
})
return {
active,
activeTab,
stars,
deliveredMails,
incomingMails,
loading,

View File

@@ -1,10 +1,5 @@
<template>
<div class="login-container">
<!-- 深空背景 -->
<div class="space-background">
<div class="stars" ref="stars"></div>
</div>
<div class="login-content">
<!-- Logo和标题 -->
<div class="logo-section">
@@ -37,6 +32,10 @@
<van-button round block type="primary" native-type="submit" class="login-button">
登录
</van-button>
<div class="test-account-hint mt-10 text-center">
<p class="text-secondary text-xs">测试账号test@example.com / testuser</p>
<p class="text-secondary text-xs">密码任意密码</p>
</div>
<div class="register-link mt-20">
<span class="text-secondary text-sm">还没有账号</span><span class="text-accent font-medium" @click="goToRegister">立即注册</span>
</div>
@@ -59,38 +58,6 @@ export default {
const router = useRouter()
const email = ref('')
const password = ref('')
const stars = ref(null)
// 生成星空背景
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 onSubmit = async () => {
@@ -124,13 +91,12 @@ export default {
}
onMounted(() => {
generateStars()
// 组件挂载后的初始化代码
})
return {
email,
password,
stars,
onSubmit,
goToRegister
}

View File

@@ -1,10 +1,5 @@
<template>
<div class="profile-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" />
@@ -148,7 +143,6 @@ export default {
setup() {
const router = useRouter()
const active = ref(3)
const stars = ref(null)
const showAboutPopup = ref(false)
// 使用直接导入的状态和操作
@@ -195,37 +189,6 @@ export default {
}
}
// 生成星空背景
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 goBack = () => {
router.back()
@@ -303,7 +266,6 @@ export default {
}
onMounted(() => {
generateStars()
fetchUserProfile()
fetchStatistics()
@@ -318,7 +280,6 @@ export default {
return {
active,
stars,
userName,
userEmail,
userMotto,

View File

@@ -1,10 +1,5 @@
<template>
<div class="register-container">
<!-- 深空背景 -->
<div class="space-background">
<div class="stars" ref="stars"></div>
</div>
<div class="register-content">
<!-- Logo和标题 -->
<div class="logo-section">
@@ -85,38 +80,6 @@ export default {
const email = ref('')
const password = ref('')
const confirmPassword = ref('')
const stars = ref(null)
// 生成星空背景
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 validatePassword = () => {
@@ -159,7 +122,7 @@ export default {
}
onMounted(() => {
generateStars()
// 组件挂载后的初始化代码
})
return {
@@ -167,7 +130,6 @@ export default {
email,
password,
confirmPassword,
stars,
validatePassword,
onSubmit,
goToLogin

View File

@@ -1,10 +1,5 @@
<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" />
@@ -138,7 +133,6 @@ export default {
setup() {
const router = useRouter()
const active = ref(2)
const stars = ref(null)
const sortType = ref('sendDate')
const showSort = ref(false)
const showPreview = ref(false)
@@ -176,37 +170,6 @@ export default {
}
})
// 生成星空背景
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
@@ -378,13 +341,11 @@ export default {
}
onMounted(() => {
generateStars()
fetchMails(true)
})
return {
active,
stars,
sortType,
showSort,
showPreview,

View File

@@ -1,10 +1,5 @@
<template>
<div class="timeline-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" />
@@ -138,7 +133,6 @@ export default {
setup() {
const router = useRouter()
const active = ref(3)
const stars = ref(null)
const showFilter = ref(false)
// 使用直接导入的状态和操作
@@ -198,37 +192,6 @@ export default {
return timelineData.value.length
})
// 生成星空背景
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 formatDate = (dateStr) => {
const date = new Date(dateStr)
@@ -256,13 +219,11 @@ export default {
}
onMounted(() => {
generateStars()
fetchTimeline()
})
return {
active,
stars,
timelineData,
showFilter,
filterType,

307
发送至未来API文档.md Normal file
View File

@@ -0,0 +1,307 @@
# 发送至未来功能 API 文档
## 概述
发送至未来功能允许用户将邮件设置为在未来特定时间自动发送,邮件状态将变为待投递(PENDING),系统会在指定时间自动处理发送。
## API 接口
### 发送至未来
**接口地址:** `POST /api/v1/mails/send-to-future`
**接口描述:** 将草稿状态的邮件设置为在未来特定时间自动发送
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID |
| sendTime | string | 是 | 发送时间ISO格式时间字符串2025-12-31T23:59:59Z |
| triggerType | string | 否 | 触发类型TIME时间、LOCATION地点、EVENT事件默认为TIME |
| triggerCondition | object | 否 | 触发条件 |
| triggerCondition.location | object | 否 | 地点触发条件 |
| triggerCondition.location.latitude | number | 否 | 纬度 |
| triggerCondition.location.longitude | number | 否 | 经度 |
| triggerCondition.location.city | string | 否 | 城市 |
| triggerCondition.event | object | 否 | 事件触发条件 |
| triggerCondition.event.keywords | array | 否 | 关键词列表 |
| triggerCondition.event.type | string | 否 | 事件类型 |
#### 请求示例
```json
{
"mailId": "mail_1234567890",
"sendTime": "2025-12-31T23:59:59Z",
"triggerType": "TIME",
"triggerCondition": {}
}
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.capsuleId | string | 胶囊ID |
| data.status | string | 邮件状态PENDING |
| data.sendTime | string | 发送时间 |
| data.countdown | number | 倒计时秒数 |
| data.updatedAt | string | 更新时间ISO格式时间字符串 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"capsuleId": "capsule_1234567890",
"status": "PENDING",
"sendTime": "2025-12-31T23:59:59Z",
"countdown": 94608000,
"updatedAt": "2023-07-20T10:30:00Z"
}
}
```
### 获取待发送邮件列表
**接口地址:** `GET /api/v1/mails`
**接口描述:** 获取用户的待发送邮件列表
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| type | string | 否 | 邮件类型INBOX、SENT、DRAFT获取待发送时使用SENT |
| status | string | 否 | 状态筛选PENDING、DELIVERING、DELIVERED、DRAFT获取待发送时使用PENDING |
| page | number | 否 | 页码默认为1 |
| size | number | 否 | 每页数量默认为10 |
#### 请求示例
```
GET /api/v1/mails?type=SENT&status=PENDING&page=1&size=10
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.list | array | 邮件列表 |
| data.list[].mailId | string | 邮件ID |
| data.list[].title | string | 邮件标题 |
| data.list[].sender | object | 发件人信息 |
| data.list[].recipient | object | 收件人信息 |
| data.list[].sendTime | string | 发送时间 |
| data.list[].deliveryTime | string | 送达时间 |
| data.list[].status | string | 邮件状态 |
| data.list[].hasAttachments | boolean | 是否有附件 |
| data.list[].isEncrypted | boolean | 是否加密 |
| data.list[].capsuleStyle | string | 胶囊样式 |
| data.list[].countdown | number | 倒计时秒数 |
| data.total | number | 总数量 |
| data.page | number | 当前页码 |
| data.size | number | 每页数量 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"mailId": "mail_1234567890",
"title": "写给未来的自己",
"sender": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg"
},
"recipient": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg"
},
"sendTime": "2025-12-31T23:59:59Z",
"deliveryTime": null,
"status": "PENDING",
"hasAttachments": true,
"isEncrypted": false,
"capsuleStyle": "default",
"countdown": 94608000
}
],
"total": 1,
"page": 1,
"size": 10
}
}
```
### 获取待发送邮件详情
**接口地址:** `GET /api/v1/mails/{mailId}`
**接口描述:** 获取指定待发送邮件的详细信息
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID |
#### 请求示例
```
GET /api/v1/mails/mail_1234567890
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.title | string | 邮件标题 |
| data.content | string | 邮件内容 |
| data.sender | object | 发件人信息 |
| data.recipient | object | 收件人信息 |
| data.sendTime | string | 发送时间 |
| data.createdAt | string | 创建时间 |
| data.deliveryTime | string | 送达时间 |
| data.status | string | 邮件状态 |
| data.triggerType | string | 触发类型 |
| data.triggerCondition | object | 触发条件 |
| data.attachments | array | 附件列表 |
| data.isEncrypted | boolean | 是否加密 |
| data.capsuleStyle | string | 胶囊样式 |
| data.canEdit | boolean | 是否可编辑待发送状态为false |
| data.canRevoke | boolean | 是否可撤销待发送状态为true |
| data.countdown | number | 倒计时秒数 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"title": "写给未来的自己",
"content": "亲爱的未来的我,当你读到这封信时,希望你已经实现了现在的梦想...",
"sender": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg",
"email": "zhangsan@example.com"
},
"recipient": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg",
"email": "zhangsan@example.com"
},
"sendTime": "2025-12-31T23:59:59Z",
"createdAt": "2023-07-20T10:30:00Z",
"deliveryTime": null,
"status": "PENDING",
"triggerType": "TIME",
"triggerCondition": {},
"attachments": [
{
"id": "attach_123",
"type": "IMAGE",
"url": "https://example.com/image.jpg",
"thumbnail": "https://example.com/thumb.jpg",
"size": 1024000
}
],
"isEncrypted": false,
"capsuleStyle": "default",
"canEdit": false,
"canRevoke": true,
"countdown": 94608000
}
}
```
### 撤销待发送邮件
**接口地址:** `POST /api/v1/mails/{mailId}/revoke`
**接口描述:** 撤销待发送的邮件,将状态改回草稿
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID路径参数 |
#### 请求示例
```
POST /api/v1/mails/mail_1234567890/revoke
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.status | string | 邮件状态DRAFT |
| data.revokedAt | string | 撤销时间ISO格式时间字符串 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"status": "DRAFT",
"revokedAt": "2023-07-21T14:30:00Z"
}
}
```
## 错误码
| 错误码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权,需要登录 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 422 | 验证失败 |
| 500 | 服务器内部错误 |
## 注意事项
1. 发送至未来的邮件状态为PENDING表示等待系统在未来指定时间自动发送
2. 只有草稿状态(DRAFT)的邮件可以设置为发送至未来
3. 发送时间必须晚于当前时间至少1小时
4. 待发送状态的邮件不能编辑内容,但可以撤销发送
5. 撤销后的邮件状态将变回草稿(DRAFT),可以重新编辑或设置发送时间
6. 系统会在发送时间到达前10分钟进入投递中状态(DELIVERING)
7. 免费用户每月最多可设置5封邮件发送至未来
8. 附件大小限制为10MB
9. 加密邮件需要额外验证才能查看内容

372
存入胶囊API文档.md Normal file
View File

@@ -0,0 +1,372 @@
# 存入胶囊功能 API 文档
## 概述
存入胶囊功能允许用户将邮件保存为时光胶囊状态,邮件将以草稿形式保存,用户可以随时编辑或发送。
## API 接口
### 创建胶囊邮件
**接口地址:** `POST /api/v1/mails`
**接口描述:** 创建一个新邮件并将其保存为时光胶囊状态(草稿)
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| title | string | 是 | 邮件标题 |
| content | string | 是 | 邮件内容 |
| recipientType | string | 是 | 收件人类型SELF自己、SPECIFIC指定收件人、PUBLIC公开信 |
| recipientEmail | string | 否 | 指定收件人邮箱当recipientType为SPECIFIC时必填 |
| sendTime | string | 否 | 发送时间ISO格式时间字符串2025-12-31T23:59:59Z |
| triggerType | string | 否 | 触发类型TIME时间、LOCATION地点、EVENT事件 |
| triggerCondition | object | 否 | 触发条件 |
| triggerCondition.location | object | 否 | 地点触发条件 |
| triggerCondition.location.latitude | number | 否 | 纬度 |
| triggerCondition.location.longitude | number | 否 | 经度 |
| triggerCondition.location.city | string | 否 | 城市 |
| triggerCondition.event | object | 否 | 事件触发条件 |
| triggerCondition.event.keywords | array | 否 | 关键词列表 |
| triggerCondition.event.type | string | 否 | 事件类型 |
| attachments | array | 否 | 附件列表 |
| attachments[].type | string | 否 | 附件类型IMAGE、VOICE、VIDEO |
| attachments[].url | string | 否 | 附件URL |
| attachments[].thumbnail | string | 否 | 缩略图URL |
| isEncrypted | boolean | 否 | 是否加密 |
| capsuleStyle | string | 否 | 胶囊样式 |
| status | string | 是 | 邮件状态存入胶囊时固定为DRAFT |
#### 请求示例
```json
{
"title": "写给未来的自己",
"content": "亲爱的未来的我,当你读到这封信时,希望你已经实现了现在的梦想...",
"recipientType": "SELF",
"sendTime": "2025-12-31T23:59:59Z",
"triggerType": "TIME",
"attachments": [
{
"type": "IMAGE",
"url": "https://example.com/image.jpg",
"thumbnail": "https://example.com/thumb.jpg"
}
],
"isEncrypted": false,
"capsuleStyle": "default",
"status": "DRAFT"
}
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.capsuleId | string | 胶囊ID |
| data.status | string | 邮件状态DRAFT、PENDING、DELIVERING、DELIVERED |
| data.createdAt | string | 创建时间ISO格式时间字符串 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"capsuleId": "capsule_1234567890",
"status": "DRAFT",
"createdAt": "2023-07-20T10:30:00Z"
}
}
```
### 获取胶囊列表
**接口地址:** `GET /api/v1/mails`
**接口描述:** 获取用户的胶囊邮件列表
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| type | string | 否 | 邮件类型INBOX、SENT、DRAFT获取胶囊时使用DRAFT |
| status | string | 否 | 状态筛选PENDING、DELIVERING、DELIVERED、DRAFT |
| page | number | 否 | 页码默认为1 |
| size | number | 否 | 每页数量默认为10 |
#### 请求示例
```
GET /api/v1/mails?type=DRAFT&page=1&size=10
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.list | array | 邮件列表 |
| data.list[].mailId | string | 邮件ID |
| data.list[].title | string | 邮件标题 |
| data.list[].sender | object | 发件人信息 |
| data.list[].recipient | object | 收件人信息 |
| data.list[].sendTime | string | 发送时间 |
| data.list[].deliveryTime | string | 送达时间 |
| data.list[].status | string | 邮件状态 |
| data.list[].hasAttachments | boolean | 是否有附件 |
| data.list[].isEncrypted | boolean | 是否加密 |
| data.list[].capsuleStyle | string | 胶囊样式 |
| data.total | number | 总数量 |
| data.page | number | 当前页码 |
| data.size | number | 每页数量 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"list": [
{
"mailId": "mail_1234567890",
"title": "写给未来的自己",
"sender": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg"
},
"recipient": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg"
},
"sendTime": "2025-12-31T23:59:59Z",
"deliveryTime": null,
"status": "DRAFT",
"hasAttachments": true,
"isEncrypted": false,
"capsuleStyle": "default"
}
],
"total": 1,
"page": 1,
"size": 10
}
}
```
### 获取胶囊详情
**接口地址:** `GET /api/v1/mails/{mailId}`
**接口描述:** 获取指定胶囊邮件的详细信息
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID |
#### 请求示例
```
GET /api/v1/mails/mail_1234567890
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.title | string | 邮件标题 |
| data.content | string | 邮件内容 |
| data.sender | object | 发件人信息 |
| data.recipient | object | 收件人信息 |
| data.sendTime | string | 发送时间 |
| data.createdAt | string | 创建时间 |
| data.deliveryTime | string | 送达时间 |
| data.status | string | 邮件状态 |
| data.triggerType | string | 触发类型 |
| data.triggerCondition | object | 触发条件 |
| data.attachments | array | 附件列表 |
| data.isEncrypted | boolean | 是否加密 |
| data.capsuleStyle | string | 胶囊样式 |
| data.canEdit | boolean | 是否可编辑草稿状态为true |
| data.canRevoke | boolean | 是否可撤销待投递状态为true |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"title": "写给未来的自己",
"content": "亲爱的未来的我,当你读到这封信时,希望你已经实现了现在的梦想...",
"sender": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg",
"email": "zhangsan@example.com"
},
"recipient": {
"userId": "user_123",
"username": "张三",
"avatar": "https://example.com/avatar.jpg",
"email": "zhangsan@example.com"
},
"sendTime": "2025-12-31T23:59:59Z",
"createdAt": "2023-07-20T10:30:00Z",
"deliveryTime": null,
"status": "DRAFT",
"triggerType": "TIME",
"triggerCondition": {},
"attachments": [
{
"id": "attach_123",
"type": "IMAGE",
"url": "https://example.com/image.jpg",
"thumbnail": "https://example.com/thumb.jpg",
"size": 1024000
}
],
"isEncrypted": false,
"capsuleStyle": "default",
"canEdit": true,
"canRevoke": false
}
}
```
### 更新胶囊邮件
**接口地址:** `PUT /api/v1/mails/{mailId}`
**接口描述:** 更新胶囊邮件内容(仅草稿状态可更新)
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID路径参数 |
| title | string | 否 | 邮件标题 |
| content | string | 否 | 邮件内容 |
| recipientType | string | 否 | 收件人类型SELF、SPECIFIC、PUBLIC |
| recipientEmail | string | 否 | 指定收件人邮箱当recipientType为SPECIFIC时必填 |
| sendTime | string | 否 | 发送时间ISO格式时间字符串 |
| triggerType | string | 否 | 触发类型TIME、LOCATION、EVENT |
| triggerCondition | object | 否 | 触发条件 |
| attachments | array | 否 | 附件列表 |
| isEncrypted | boolean | 否 | 是否加密 |
| capsuleStyle | string | 否 | 胶囊样式 |
#### 请求示例
```json
{
"title": "更新后的标题",
"content": "更新后的内容...",
"sendTime": "2026-12-31T23:59:59Z"
}
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 邮件ID |
| data.capsuleId | string | 胶囊ID |
| data.status | string | 邮件状态 |
| data.updatedAt | string | 更新时间ISO格式时间字符串 |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890",
"capsuleId": "capsule_1234567890",
"status": "DRAFT",
"updatedAt": "2023-07-21T14:30:00Z"
}
}
```
### 删除胶囊邮件
**接口地址:** `DELETE /api/v1/mails/{mailId}`
**接口描述:** 删除指定的胶囊邮件
#### 请求参数
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| mailId | string | 是 | 邮件ID路径参数 |
#### 请求示例
```
DELETE /api/v1/mails/mail_1234567890
```
#### 响应参数
| 参数名 | 类型 | 说明 |
|--------|------|------|
| code | number | 响应状态码200表示成功 |
| message | string | 响应消息 |
| data | object | 响应数据 |
| data.mailId | string | 已删除的邮件ID |
#### 响应示例
```json
{
"code": 200,
"message": "success",
"data": {
"mailId": "mail_1234567890"
}
}
```
## 错误码
| 错误码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 请求参数错误 |
| 401 | 未授权,需要登录 |
| 403 | 权限不足 |
| 404 | 资源不存在 |
| 422 | 验证失败 |
| 500 | 服务器内部错误 |
## 注意事项
1. 存入胶囊的邮件状态为DRAFT可以在任何时候编辑或发送
2. 只有草稿状态的邮件可以编辑或删除
3. 发送时间必须晚于当前时间
4. 附件大小限制为10MB
5. 免费用户每月最多可创建10个胶囊邮件
6. 加密邮件需要额外验证才能查看内容