修改
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
# 开发环境配置
|
# 开发环境配置
|
||||||
VUE_APP_API_BASE_URL=/api/v1
|
VUE_APP_API_BASE_URL=/api/v1
|
||||||
VUE_APP_TITLE=ChronoMail - 未来邮箱
|
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>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
<GlobalBackground />
|
||||||
<router-view />
|
<router-view />
|
||||||
|
<ThemeSwitcher />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import GlobalBackground from '@/components/GlobalBackground.vue'
|
||||||
|
import ThemeSwitcher from '@/components/ThemeSwitcher.vue'
|
||||||
|
import { getCurrentTheme, applyTheme } from '@/utils/theme'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
GlobalBackground,
|
||||||
|
ThemeSwitcher
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
// 初始化主题
|
||||||
|
applyTheme(getCurrentTheme());
|
||||||
|
|
||||||
// 隐藏加载动画
|
// 隐藏加载动画
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const loadingContainer = document.querySelector('.loading-container');
|
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 api from './request'
|
||||||
|
import mockAPI from './mock'
|
||||||
|
|
||||||
|
// 检查是否使用模拟API
|
||||||
|
const useMockAPI = process.env.VUE_APP_USE_MOCK_API === 'true'
|
||||||
|
|
||||||
// 用户认证相关API
|
// 用户认证相关API
|
||||||
export const authAPI = {
|
export const authAPI = {
|
||||||
// 用户注册
|
// 用户注册
|
||||||
register(data) {
|
register(data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.auth.register(data)
|
||||||
|
}
|
||||||
return api.post('/auth/register', data)
|
return api.post('/auth/register', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 用户登录
|
// 用户登录
|
||||||
login(data) {
|
login(data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.auth.login(data)
|
||||||
|
}
|
||||||
return api.post('/auth/login', data)
|
return api.post('/auth/login', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 刷新token
|
// 刷新token
|
||||||
refreshToken(refreshToken) {
|
refreshToken(refreshToken) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.auth.refreshToken(refreshToken)
|
||||||
|
}
|
||||||
return api.post('/auth/refresh', { refreshToken })
|
return api.post('/auth/refresh', { refreshToken })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 退出登录
|
// 退出登录
|
||||||
logout() {
|
logout() {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.auth.logout()
|
||||||
|
}
|
||||||
return api.post('/auth/logout')
|
return api.post('/auth/logout')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -27,32 +43,63 @@ export const authAPI = {
|
|||||||
export const mailAPI = {
|
export const mailAPI = {
|
||||||
// 创建邮件
|
// 创建邮件
|
||||||
createMail(data) {
|
createMail(data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.mail.createMail(data)
|
||||||
|
}
|
||||||
return api.post('/mails', data)
|
return api.post('/mails', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取邮件列表
|
// 获取邮件列表
|
||||||
getMails(params) {
|
getMails(params) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.mail.getMails(params)
|
||||||
|
}
|
||||||
return api.get('/mails', { params })
|
return api.get('/mails', { params })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取邮件详情
|
// 获取邮件详情
|
||||||
getMailDetail(mailId) {
|
getMailDetail(mailId) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.mail.getMailDetail(mailId)
|
||||||
|
}
|
||||||
return api.get(`/mails/${mailId}`)
|
return api.get(`/mails/${mailId}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新邮件
|
// 更新邮件
|
||||||
updateMail(mailId, data) {
|
updateMail(mailId, data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.mail.updateMail(mailId, data)
|
||||||
|
}
|
||||||
return api.put(`/mails/${mailId}`, data)
|
return api.put(`/mails/${mailId}`, data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 删除邮件
|
// 删除邮件
|
||||||
deleteMail(mailId) {
|
deleteMail(mailId) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.mail.deleteMail(mailId)
|
||||||
|
}
|
||||||
return api.delete(`/mails/${mailId}`)
|
return api.delete(`/mails/${mailId}`)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 撤销发送
|
// 撤销发送
|
||||||
revokeMail(mailId) {
|
revokeMail(mailId) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.mail.revokeMail(mailId)
|
||||||
|
}
|
||||||
return api.post(`/mails/${mailId}/revoke`)
|
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 = {
|
export const capsuleAPI = {
|
||||||
// 获取胶囊视图
|
// 获取胶囊视图
|
||||||
getCapsules() {
|
getCapsules() {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.capsule.getCapsules()
|
||||||
|
}
|
||||||
return api.get('/capsules')
|
return api.get('/capsules')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新胶囊样式
|
// 更新胶囊样式
|
||||||
updateCapsuleStyle(capsuleId, style) {
|
updateCapsuleStyle(capsuleId, style) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.capsule.updateCapsuleStyle(capsuleId, style)
|
||||||
|
}
|
||||||
return api.put(`/capsules/${capsuleId}/style`, { 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 = {
|
export const aiAPI = {
|
||||||
// 写作辅助
|
// 写作辅助
|
||||||
writingAssistant(data) {
|
writingAssistant(data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.ai.writingAssistant(data)
|
||||||
|
}
|
||||||
return api.post('/ai/writing-assistant', data)
|
return api.post('/ai/writing-assistant', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 情感分析
|
// 情感分析
|
||||||
sentimentAnalysis(content) {
|
sentimentAnalysis(content) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.ai.sentimentAnalysis(content)
|
||||||
|
}
|
||||||
return api.post('/ai/sentiment-analysis', { content })
|
return api.post('/ai/sentiment-analysis', { content })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 未来预测
|
// 未来预测
|
||||||
futurePrediction(data) {
|
futurePrediction(data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.ai.futurePrediction(data)
|
||||||
|
}
|
||||||
return api.post('/ai/future-prediction', data)
|
return api.post('/ai/future-prediction', data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,26 +177,41 @@ export const aiAPI = {
|
|||||||
export const userAPI = {
|
export const userAPI = {
|
||||||
// 获取时间线
|
// 获取时间线
|
||||||
getTimeline(params) {
|
getTimeline(params) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.user.getTimeline(params)
|
||||||
|
}
|
||||||
return api.get('/timeline', { params })
|
return api.get('/timeline', { params })
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取统计数据
|
// 获取统计数据
|
||||||
getStatistics() {
|
getStatistics() {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.user.getStatistics()
|
||||||
|
}
|
||||||
return api.get('/statistics')
|
return api.get('/statistics')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取用户信息
|
// 获取用户信息
|
||||||
getUserProfile() {
|
getUserProfile() {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.user.getUserProfile()
|
||||||
|
}
|
||||||
return api.get('/user/profile')
|
return api.get('/user/profile')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新用户信息
|
// 更新用户信息
|
||||||
updateUserProfile(data) {
|
updateUserProfile(data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.user.updateUserProfile(data)
|
||||||
|
}
|
||||||
return api.put('/user/profile', data)
|
return api.put('/user/profile', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取用户订阅信息
|
// 获取用户订阅信息
|
||||||
getSubscription() {
|
getSubscription() {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.user.getSubscription()
|
||||||
|
}
|
||||||
return api.get('/user/subscription')
|
return api.get('/user/subscription')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -119,6 +220,9 @@ export const userAPI = {
|
|||||||
export const uploadAPI = {
|
export const uploadAPI = {
|
||||||
// 上传附件
|
// 上传附件
|
||||||
uploadAttachment(file) {
|
uploadAttachment(file) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.upload.uploadAttachment(file)
|
||||||
|
}
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
||||||
@@ -131,6 +235,9 @@ export const uploadAPI = {
|
|||||||
|
|
||||||
// 上传头像
|
// 上传头像
|
||||||
uploadAvatar(file) {
|
uploadAvatar(file) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.upload.uploadAvatar(file)
|
||||||
|
}
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
formData.append('file', file)
|
||||||
|
|
||||||
@@ -146,16 +253,25 @@ export const uploadAPI = {
|
|||||||
export const notificationAPI = {
|
export const notificationAPI = {
|
||||||
// 注册设备
|
// 注册设备
|
||||||
registerDevice(data) {
|
registerDevice(data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.notification.registerDevice(data)
|
||||||
|
}
|
||||||
return api.post('/notification/device', data)
|
return api.post('/notification/device', data)
|
||||||
},
|
},
|
||||||
|
|
||||||
// 获取通知设置
|
// 获取通知设置
|
||||||
getNotificationSettings() {
|
getNotificationSettings() {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.notification.getNotificationSettings()
|
||||||
|
}
|
||||||
return api.get('/notification/settings')
|
return api.get('/notification/settings')
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新通知设置
|
// 更新通知设置
|
||||||
updateNotificationSettings(data) {
|
updateNotificationSettings(data) {
|
||||||
|
if (useMockAPI) {
|
||||||
|
return mockAPI.notification.updateNotificationSettings(data)
|
||||||
|
}
|
||||||
return api.put('/notification/settings', 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 => {
|
response => {
|
||||||
const res = response.data
|
const res = response.data
|
||||||
|
|
||||||
// 如果响应中的success不是true,则判断为错误
|
// 如果响应中的code不是200,则判断为错误
|
||||||
if (res.success !== true) {
|
if (res.success !== true) {
|
||||||
showFailToast(res.message || '请求失败')
|
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插件
|
* 移动端触摸交互Vue插件
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ export const userActions = {
|
|||||||
try {
|
try {
|
||||||
const res = await api.user.getSubscription()
|
const res = await api.user.getSubscription()
|
||||||
// 根据后端实际返回的数据结构提取数据
|
// 根据后端实际返回的数据结构提取数据
|
||||||
userState.subscription = res.data.data
|
userState.subscription = res.data
|
||||||
return res
|
return res
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取订阅信息失败:', 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>
|
<template>
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="page-content">
|
<div class="page-content">
|
||||||
<van-nav-bar
|
<van-nav-bar
|
||||||
title="API示例"
|
title="API示例"
|
||||||
@@ -200,7 +196,6 @@ export default {
|
|||||||
name: 'ApiDemo',
|
name: 'ApiDemo',
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const stars = ref(null)
|
|
||||||
|
|
||||||
// 登录表单
|
// 登录表单
|
||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
@@ -224,33 +219,6 @@ export default {
|
|||||||
const aiPrompt = ref('')
|
const aiPrompt = ref('')
|
||||||
const aiLoading = ref(false)
|
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 () => {
|
const handleLogin = async () => {
|
||||||
if (!loginForm.email || !loginForm.password) {
|
if (!loginForm.email || !loginForm.password) {
|
||||||
@@ -382,11 +350,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initStars()
|
// 组件挂载后的初始化代码
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stars,
|
|
||||||
loginForm,
|
loginForm,
|
||||||
loginLoading,
|
loginLoading,
|
||||||
mails,
|
mails,
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="capsule-detail-container">
|
<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">
|
<div class="header glass-card">
|
||||||
<van-icon name="arrow-left" size="24" @click="goBack" />
|
<van-icon name="arrow-left" size="24" @click="goBack" />
|
||||||
@@ -147,8 +141,6 @@ export default {
|
|||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const stars = ref(null)
|
|
||||||
const particles = ref(null)
|
|
||||||
const capsule3d = ref(null)
|
const capsule3d = ref(null)
|
||||||
|
|
||||||
// 胶囊数据
|
// 胶囊数据
|
||||||
@@ -164,70 +156,6 @@ export default {
|
|||||||
// 获取胶囊ID
|
// 获取胶囊ID
|
||||||
const mailId = computed(() => route.params.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 () => {
|
const loadCapsuleData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -355,14 +283,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateStars()
|
|
||||||
generateParticles()
|
|
||||||
loadCapsuleData()
|
loadCapsuleData()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stars,
|
|
||||||
particles,
|
|
||||||
capsule3d,
|
capsule3d,
|
||||||
capsuleData,
|
capsuleData,
|
||||||
isCapsuleOpened,
|
isCapsuleOpened,
|
||||||
@@ -692,29 +616,4 @@ export default {
|
|||||||
transform: translateX(5px);
|
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>
|
</style>
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="compose-container">
|
<div class="compose-container">
|
||||||
<!-- 深空背景 -->
|
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="header glass-card">
|
<div class="header glass-card">
|
||||||
<van-icon name="arrow-left" size="24" @click="goBack" />
|
<van-icon name="arrow-left" size="24" @click="goBack" />
|
||||||
@@ -192,12 +187,12 @@
|
|||||||
import { ref, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { showLoadingToast, showSuccessToast, showFailToast, closeToast, Dialog } from 'vant'
|
import { showLoadingToast, showSuccessToast, showFailToast, closeToast, Dialog } from 'vant'
|
||||||
|
import { mailAPI, capsuleAPI } from '../api'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Compose',
|
name: 'Compose',
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const stars = ref(null)
|
|
||||||
|
|
||||||
// 表单数据
|
// 表单数据
|
||||||
const recipientType = ref('SELF') // 对应API的SELF, SPECIFIC, PUBLIC
|
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 = () => {
|
const goBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -590,32 +554,59 @@ export default {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!content.value) {
|
||||||
|
showFailToast('请填写邮件内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
showLoadingToast({
|
showLoadingToast({
|
||||||
message: '保存中...',
|
message: '保存中...',
|
||||||
forbidClick: true,
|
forbidClick: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 模拟API调用延迟
|
const mailData = {
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
...buildMailData(),
|
||||||
|
status: 'DRAFT'
|
||||||
|
}
|
||||||
|
|
||||||
// 保存到本地存储作为草稿
|
// 先创建邮件草稿
|
||||||
const mailData = buildMailData()
|
const mailResponse = await mailAPI.createMail(mailData)
|
||||||
const drafts = JSON.parse(localStorage.getItem('draftMails') || '[]')
|
|
||||||
drafts.push({
|
if (mailResponse.code !== 200) {
|
||||||
...mailData,
|
closeToast()
|
||||||
id: Date.now().toString(),
|
showFailToast(mailResponse.message || '保存失败')
|
||||||
status: 'DRAFT',
|
return
|
||||||
createdAt: new Date().toISOString()
|
}
|
||||||
})
|
|
||||||
localStorage.setItem('draftMails', JSON.stringify(drafts))
|
// 将邮件存入胶囊
|
||||||
|
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()
|
closeToast()
|
||||||
showSuccessToast('草稿已保存')
|
|
||||||
router.back()
|
if (capsuleResponse.code === 200) {
|
||||||
|
showSuccessToast('邮件已存入胶囊')
|
||||||
|
router.back()
|
||||||
|
} else {
|
||||||
|
showFailToast(capsuleResponse.message || '存入胶囊失败')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
closeToast()
|
closeToast()
|
||||||
const errorMessage = error.response?.data?.message || '保存失败,请重试'
|
console.error('存入胶囊失败:', error)
|
||||||
|
const errorMessage = error.response?.data?.message || '存入胶囊失败,请重试'
|
||||||
showFailToast(errorMessage)
|
showFailToast(errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -643,59 +634,77 @@ export default {
|
|||||||
forbidClick: true,
|
forbidClick: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 模拟API调用延迟
|
// 先构建邮件数据
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
|
||||||
|
|
||||||
const mailData = buildMailData()
|
const mailData = buildMailData()
|
||||||
|
|
||||||
// 保存到本地存储作为已发送邮件
|
// 创建邮件草稿
|
||||||
const sentMails = JSON.parse(localStorage.getItem('sentMails') || '[]')
|
const createMailData = {
|
||||||
sentMails.push({
|
|
||||||
...mailData,
|
...mailData,
|
||||||
id: Date.now().toString(),
|
status: 'DRAFT'
|
||||||
status: 'PENDING',
|
|
||||||
createdAt: new Date().toISOString()
|
|
||||||
})
|
|
||||||
localStorage.setItem('sentMails', JSON.stringify(sentMails))
|
|
||||||
|
|
||||||
closeToast()
|
|
||||||
|
|
||||||
// 计算发送时间用于显示
|
|
||||||
let deliveryDate
|
|
||||||
if (timeType.value === 'preset' || timeType.value === 'custom') {
|
|
||||||
deliveryDate = new Date(mailData.sendTime)
|
|
||||||
} else {
|
|
||||||
deliveryDate = new Date()
|
|
||||||
deliveryDate.setFullYear(deliveryDate.getFullYear() + 1) // 默认显示一年后
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Dialog.confirm({
|
const mailResponse = await mailAPI.createMail(createMailData)
|
||||||
title: '邮件已发送',
|
|
||||||
message: `您的邮件将在${deliveryDate.toLocaleDateString()}送达,是否返回首页?`,
|
if (mailResponse.code !== 200) {
|
||||||
confirmButtonText: '返回首页',
|
closeToast()
|
||||||
cancelButtonText: '继续撰写',
|
showFailToast(mailResponse.message || '创建邮件失败')
|
||||||
})
|
return
|
||||||
.then(() => {
|
}
|
||||||
router.push('/home')
|
|
||||||
})
|
// 获取发送时间和触发条件
|
||||||
.catch(() => {
|
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(sendTime)
|
||||||
|
} else {
|
||||||
|
deliveryDate = new Date()
|
||||||
|
deliveryDate.setFullYear(deliveryDate.getFullYear() + 1) // 默认显示一年后
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.confirm({
|
||||||
|
title: '邮件已发送至未来',
|
||||||
|
message: `您的邮件将在${deliveryDate.toLocaleDateString()}送达,是否返回首页?`,
|
||||||
|
confirmButtonText: '返回首页',
|
||||||
|
cancelButtonText: '继续撰写',
|
||||||
})
|
})
|
||||||
|
.then(() => {
|
||||||
|
router.push('/home')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 继续撰写
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
closeToast()
|
||||||
|
showFailToast(response.message || '发送失败')
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
closeToast()
|
closeToast()
|
||||||
|
console.error('发送邮件失败:', error)
|
||||||
const errorMessage = error.response?.data?.message || '发送失败,请重试'
|
const errorMessage = error.response?.data?.message || '发送失败,请重试'
|
||||||
showFailToast(errorMessage)
|
showFailToast(errorMessage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateStars()
|
// 组件初始化
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stars,
|
recipientType,
|
||||||
recipientType,
|
recipientEmail,
|
||||||
recipientEmail,
|
|
||||||
timeType,
|
timeType,
|
||||||
selectedPresetTime,
|
selectedPresetTime,
|
||||||
customDeliveryDate,
|
customDeliveryDate,
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="home-container">
|
<div class="home-container">
|
||||||
<!-- 深空背景 -->
|
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顶部欢迎语 -->
|
<!-- 顶部欢迎语 -->
|
||||||
<div class="header glass-card">
|
<div class="header glass-card">
|
||||||
<div class="welcome-text">
|
<div class="welcome-text">
|
||||||
@@ -54,36 +49,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 悬浮按钮 -->
|
|
||||||
<div class="fab-container">
|
|
||||||
<van-button
|
|
||||||
icon="plus"
|
|
||||||
type="primary"
|
|
||||||
round
|
|
||||||
class="fab-button"
|
|
||||||
@click="goToCompose"
|
|
||||||
>
|
|
||||||
</van-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部导航 -->
|
<!-- 底部导航 -->
|
||||||
<van-tabbar v-model="active" class="custom-tabbar">
|
<BottomTabbar v-model:current-active="active" />
|
||||||
<van-tabbar-item icon="home-o" to="/home">时光胶囊</van-tabbar-item>
|
|
||||||
<van-tabbar-item icon="envelop-o" to="/inbox">收件箱</van-tabbar-item>
|
|
||||||
<van-tabbar-item icon="send-o" to="/sent">发件箱</van-tabbar-item>
|
|
||||||
<van-tabbar-item icon="user-o" to="/profile">个人中心</van-tabbar-item>
|
|
||||||
</van-tabbar>
|
|
||||||
|
|
||||||
<!-- 搜索弹窗 -->
|
<!-- 搜索弹窗 -->
|
||||||
<van-popup v-model:show="showSearch" position="top" :style="{ height: '30%' }">
|
<SearchComponent v-model="showSearch" />
|
||||||
<div class="search-popup">
|
|
||||||
<van-search
|
|
||||||
v-model="searchValue"
|
|
||||||
placeholder="搜索邮件"
|
|
||||||
@search="onSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</van-popup>
|
|
||||||
|
|
||||||
<!-- 通知弹窗 -->
|
<!-- 通知弹窗 -->
|
||||||
<van-popup v-model:show="showNotifications" position="top" :style="{ height: '40%' }">
|
<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 { useRouter } from 'vue-router'
|
||||||
import { showFailToast } from 'vant'
|
import { showFailToast } from 'vant'
|
||||||
import { userState, mailState, capsuleState, mailActions, capsuleActions } from '../store'
|
import { userState, mailState, capsuleState, mailActions, capsuleActions } from '../store'
|
||||||
|
import SearchComponent from '@/components/SearchComponent.vue'
|
||||||
|
import BottomTabbar from '@/components/BottomTabbar.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {
|
||||||
|
SearchComponent,
|
||||||
|
BottomTabbar
|
||||||
|
},
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const active = ref(0)
|
const active = ref(0)
|
||||||
const stars = ref(null)
|
|
||||||
const capsulesSpace = ref(null)
|
const capsulesSpace = ref(null)
|
||||||
const showSearch = ref(false)
|
const showSearch = ref(false)
|
||||||
const showNotifications = ref(false)
|
const showNotifications = ref(false)
|
||||||
@@ -139,37 +114,6 @@ export default {
|
|||||||
return '晚上好,今天过得怎么样'
|
return '晚上好,今天过得怎么样'
|
||||||
})
|
})
|
||||||
|
|
||||||
// 生成星空背景
|
|
||||||
const generateStars = () => {
|
|
||||||
if (!stars.value) return
|
|
||||||
|
|
||||||
const starsContainer = stars.value
|
|
||||||
const starCount = 150
|
|
||||||
|
|
||||||
for (let i = 0; i < starCount; i++) {
|
|
||||||
const star = document.createElement('div')
|
|
||||||
star.className = 'star'
|
|
||||||
|
|
||||||
// 随机位置
|
|
||||||
const left = Math.random() * 100
|
|
||||||
const top = Math.random() * 100
|
|
||||||
|
|
||||||
// 随机大小
|
|
||||||
const size = Math.random() * 3 + 1
|
|
||||||
|
|
||||||
// 随机动画延迟
|
|
||||||
const delay = Math.random() * 4
|
|
||||||
|
|
||||||
star.style.left = `${left}%`
|
|
||||||
star.style.top = `${top}%`
|
|
||||||
star.style.width = `${size}px`
|
|
||||||
star.style.height = `${size}px`
|
|
||||||
star.style.animationDelay = `${delay}s`
|
|
||||||
|
|
||||||
starsContainer.appendChild(star)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取胶囊样式
|
// 获取胶囊样式
|
||||||
const getCapsuleStyle = (capsule, index) => {
|
const getCapsuleStyle = (capsule, index) => {
|
||||||
// 添加基于索引的延迟动画
|
// 添加基于索引的延迟动画
|
||||||
@@ -262,24 +206,14 @@ export default {
|
|||||||
router.push(`/capsule/${capsule.mailId || capsule.capsuleId || capsule.id}`)
|
router.push(`/capsule/${capsule.mailId || capsule.capsuleId || capsule.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到撰写页面
|
|
||||||
const goToCompose = () => {
|
|
||||||
router.push('/compose')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索处理
|
// 搜索处理
|
||||||
const onSearch = (value) => {
|
const onSearch = () => {
|
||||||
if (!value) {
|
if (!searchValue.value.trim()) {
|
||||||
showFailToast('请输入搜索内容')
|
showFailToast('请输入搜索内容')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
showFailToast(`搜索: ${searchValue.value}`)
|
||||||
// 暂时显示提示,因为搜索页面尚未实现
|
// TODO: 实现搜索功能
|
||||||
showFailToast(`搜索功能开发中,您搜索了: ${value}`)
|
|
||||||
showSearch.value = false
|
|
||||||
|
|
||||||
// TODO: 实现搜索功能后,可以跳转到搜索页面
|
|
||||||
// router.push(`/search?q=${encodeURIComponent(value)}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取时光胶囊数据
|
// 获取时光胶囊数据
|
||||||
@@ -314,7 +248,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
generateStars()
|
|
||||||
await fetchCapsules()
|
await fetchCapsules()
|
||||||
await fetchNotifications()
|
await fetchNotifications()
|
||||||
})
|
})
|
||||||
@@ -323,12 +256,10 @@ export default {
|
|||||||
active,
|
active,
|
||||||
userName,
|
userName,
|
||||||
greetingText,
|
greetingText,
|
||||||
stars,
|
|
||||||
capsulesSpace,
|
capsulesSpace,
|
||||||
capsules,
|
capsules,
|
||||||
showSearch,
|
showSearch,
|
||||||
showNotifications,
|
showNotifications,
|
||||||
searchValue,
|
|
||||||
notifications,
|
notifications,
|
||||||
activeCapsule,
|
activeCapsule,
|
||||||
getCapsuleStyle,
|
getCapsuleStyle,
|
||||||
@@ -341,7 +272,6 @@ export default {
|
|||||||
formatDate,
|
formatDate,
|
||||||
formatTime,
|
formatTime,
|
||||||
openCapsule,
|
openCapsule,
|
||||||
goToCompose,
|
|
||||||
onSearch
|
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 {
|
.search-popup {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
@@ -599,7 +505,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.notification-time {
|
.notification-time {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="inbox-container">
|
<div class="inbox-container">
|
||||||
<!-- 深空背景 -->
|
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="header glass-card">
|
<div class="header glass-card">
|
||||||
<van-icon name="arrow-left" size="24" @click="goBack" />
|
<van-icon name="arrow-left" size="24" @click="goBack" />
|
||||||
@@ -140,7 +135,6 @@ export default {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const active = ref(1)
|
const active = ref(1)
|
||||||
const activeTab = ref(0)
|
const activeTab = ref(0)
|
||||||
const stars = ref(null)
|
|
||||||
|
|
||||||
// 使用直接导入的状态和操作
|
// 使用直接导入的状态和操作
|
||||||
const mails = computed(() => mailState.inboxList || [])
|
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 = () => {
|
const goBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -325,14 +288,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateStars()
|
|
||||||
fetchMails(true)
|
fetchMails(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active,
|
active,
|
||||||
activeTab,
|
activeTab,
|
||||||
stars,
|
|
||||||
deliveredMails,
|
deliveredMails,
|
||||||
incomingMails,
|
incomingMails,
|
||||||
loading,
|
loading,
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<!-- 深空背景 -->
|
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="login-content">
|
<div class="login-content">
|
||||||
<!-- Logo和标题 -->
|
<!-- Logo和标题 -->
|
||||||
<div class="logo-section">
|
<div class="logo-section">
|
||||||
@@ -37,6 +32,10 @@
|
|||||||
<van-button round block type="primary" native-type="submit" class="login-button">
|
<van-button round block type="primary" native-type="submit" class="login-button">
|
||||||
登录
|
登录
|
||||||
</van-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">
|
<div class="register-link mt-20">
|
||||||
<span class="text-secondary text-sm">还没有账号?</span><span class="text-accent font-medium" @click="goToRegister">立即注册</span>
|
<span class="text-secondary text-sm">还没有账号?</span><span class="text-accent font-medium" @click="goToRegister">立即注册</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -59,38 +58,6 @@ export default {
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const email = ref('')
|
const email = ref('')
|
||||||
const password = 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 () => {
|
const onSubmit = async () => {
|
||||||
@@ -124,13 +91,12 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateStars()
|
// 组件挂载后的初始化代码
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
stars,
|
|
||||||
onSubmit,
|
onSubmit,
|
||||||
goToRegister
|
goToRegister
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="profile-container">
|
<div class="profile-container">
|
||||||
<!-- 深空背景 -->
|
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="header glass-card">
|
<div class="header glass-card">
|
||||||
<van-icon name="arrow-left" size="24" @click="goBack" />
|
<van-icon name="arrow-left" size="24" @click="goBack" />
|
||||||
@@ -148,7 +143,6 @@ export default {
|
|||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const active = ref(3)
|
const active = ref(3)
|
||||||
const stars = ref(null)
|
|
||||||
const showAboutPopup = ref(false)
|
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 = () => {
|
const goBack = () => {
|
||||||
router.back()
|
router.back()
|
||||||
@@ -303,7 +266,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateStars()
|
|
||||||
fetchUserProfile()
|
fetchUserProfile()
|
||||||
fetchStatistics()
|
fetchStatistics()
|
||||||
|
|
||||||
@@ -318,7 +280,6 @@ export default {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
active,
|
active,
|
||||||
stars,
|
|
||||||
userName,
|
userName,
|
||||||
userEmail,
|
userEmail,
|
||||||
userMotto,
|
userMotto,
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="register-container">
|
<div class="register-container">
|
||||||
<!-- 深空背景 -->
|
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="register-content">
|
<div class="register-content">
|
||||||
<!-- Logo和标题 -->
|
<!-- Logo和标题 -->
|
||||||
<div class="logo-section">
|
<div class="logo-section">
|
||||||
@@ -85,38 +80,6 @@ export default {
|
|||||||
const email = ref('')
|
const email = ref('')
|
||||||
const password = ref('')
|
const password = ref('')
|
||||||
const confirmPassword = 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 = () => {
|
const validatePassword = () => {
|
||||||
@@ -159,7 +122,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateStars()
|
// 组件挂载后的初始化代码
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -167,7 +130,6 @@ export default {
|
|||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
confirmPassword,
|
confirmPassword,
|
||||||
stars,
|
|
||||||
validatePassword,
|
validatePassword,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
goToLogin
|
goToLogin
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="sent-container">
|
<div class="sent-container">
|
||||||
<!-- 深空背景 -->
|
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="header glass-card">
|
<div class="header glass-card">
|
||||||
<van-icon name="arrow-left" size="24" @click="goBack" />
|
<van-icon name="arrow-left" size="24" @click="goBack" />
|
||||||
@@ -138,7 +133,6 @@ export default {
|
|||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const active = ref(2)
|
const active = ref(2)
|
||||||
const stars = ref(null)
|
|
||||||
const sortType = ref('sendDate')
|
const sortType = ref('sendDate')
|
||||||
const showSort = ref(false)
|
const showSort = ref(false)
|
||||||
const showPreview = 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) => {
|
const fetchMails = async (reset = false) => {
|
||||||
if (loading.value || finished.value) return
|
if (loading.value || finished.value) return
|
||||||
@@ -378,13 +341,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateStars()
|
|
||||||
fetchMails(true)
|
fetchMails(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active,
|
active,
|
||||||
stars,
|
|
||||||
sortType,
|
sortType,
|
||||||
showSort,
|
showSort,
|
||||||
showPreview,
|
showPreview,
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="timeline-container">
|
<div class="timeline-container">
|
||||||
<!-- 深空背景 -->
|
|
||||||
<div class="space-background">
|
|
||||||
<div class="stars" ref="stars"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 顶部导航 -->
|
<!-- 顶部导航 -->
|
||||||
<div class="header glass-card">
|
<div class="header glass-card">
|
||||||
<van-icon name="arrow-left" size="24" @click="goBack" />
|
<van-icon name="arrow-left" size="24" @click="goBack" />
|
||||||
@@ -138,7 +133,6 @@ export default {
|
|||||||
setup() {
|
setup() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const active = ref(3)
|
const active = ref(3)
|
||||||
const stars = ref(null)
|
|
||||||
const showFilter = ref(false)
|
const showFilter = ref(false)
|
||||||
|
|
||||||
// 使用直接导入的状态和操作
|
// 使用直接导入的状态和操作
|
||||||
@@ -198,37 +192,6 @@ export default {
|
|||||||
return timelineData.value.length
|
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 formatDate = (dateStr) => {
|
||||||
const date = new Date(dateStr)
|
const date = new Date(dateStr)
|
||||||
@@ -256,13 +219,11 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
generateStars()
|
|
||||||
fetchTimeline()
|
fetchTimeline()
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
active,
|
active,
|
||||||
stars,
|
|
||||||
timelineData,
|
timelineData,
|
||||||
showFilter,
|
showFilter,
|
||||||
filterType,
|
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