修改
This commit is contained in:
@@ -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
39
dist/report.html
vendored
Normal file
File diff suppressed because one or more lines are too long
13
src/App.vue
13
src/App.vue
@@ -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');
|
||||
|
||||
116
src/api/index.js
116
src/api/index.js
@@ -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
951
src/api/mock.js
Normal 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
|
||||
}
|
||||
@@ -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
121
src/components/BottomTabbar.vue
Normal file
121
src/components/BottomTabbar.vue
Normal 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>
|
||||
93
src/components/GlobalBackground.vue
Normal file
93
src/components/GlobalBackground.vue
Normal 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
122
src/components/README.md
Normal 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 组件重写样式
|
||||
- 实现搜索建议、历史记录和结果展示功能
|
||||
- 添加搜索筛选器功能
|
||||
1130
src/components/SearchComponent.vue
Normal file
1130
src/components/SearchComponent.vue
Normal file
File diff suppressed because it is too large
Load Diff
188
src/components/ThemeSwitcher.vue
Normal file
188
src/components/ThemeSwitcher.vue
Normal 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>
|
||||
@@ -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插件
|
||||
|
||||
@@ -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
152
src/utils/theme.js
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -601,5 +507,5 @@ export default {
|
||||
.notification-time {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
307
发送至未来API文档.md
Normal 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
372
存入胶囊API文档.md
Normal 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. 加密邮件需要额外验证才能查看内容
|
||||
Reference in New Issue
Block a user