This commit is contained in:
2025-10-17 16:28:41 +08:00
parent 89dbdc63db
commit 2b634ed4d4
7 changed files with 2265 additions and 123 deletions

445
PROJECT_DOCUMENTATION.md Normal file
View File

@@ -0,0 +1,445 @@
# ChronoMail - 时光胶囊邮箱 详细文档
## 项目概述
ChronoMail是一个基于Vue3和Electron的时光胶囊邮箱应用用户可以撰写邮件并设定未来的发送时间将此刻的心情、想法和祝福寄给未来的自己或他人。项目采用深空主题设计营造时光穿梭的视觉体验。
## 技术架构
### 前端技术栈
- **Vue 3**: 使用Composition API进行组件开发
- **Vue Router 4**: 负责前端路由管理和导航守卫
- **Vant 4**: 移动端UI组件库提供丰富的交互组件
- **Axios**: HTTP客户端用于API请求和响应处理
- **Electron**: 跨平台桌面应用框架
### 项目结构
```
ChronoMail/
├── public/ # 静态资源
│ ├── favicon.svg # 网站图标
│ └── index.html # HTML模板
├── src/ # 源代码
│ ├── api/ # API接口模块
│ │ ├── index.js # API方法集合
│ │ └── request.js # Axios实例和拦截器配置
│ ├── assets/ # 静态资源
│ │ └── styles/ # 样式文件
│ ├── router/ # 路由配置
│ │ └── index.js # 路由定义和守卫
│ ├── store/ # 状态管理
│ │ └── index.js # 全局状态和操作方法
│ ├── utils/ # 工具函数
│ │ └── index.js # 通用工具方法
│ ├── views/ # 页面组件
│ │ ├── Login.vue # 登录页
│ │ ├── Register.vue # 注册页
│ │ ├── Home.vue # 首页/时光胶囊
│ │ ├── Compose.vue # 撰写邮件
│ │ ├── Inbox.vue # 收件箱
│ │ ├── Sent.vue # 发件箱
│ │ ├── Profile.vue # 个人中心
│ │ ├── Timeline.vue # 时间线
│ │ ├── CapsuleDetail.vue # 胶囊详情
│ │ └── ApiDemo.vue # API示例
│ ├── App.vue # 根组件
│ └── main.js # 应用入口
├── .env.development # 开发环境变量
├── .env.production # 生产环境变量
├── babel.config.js # Babel配置
├── vue.config.js # Vue CLI配置
└── package.json # 项目依赖和脚本
```
## 核心功能模块
### 1. 用户认证模块
- **登录功能**: 用户通过邮箱和密码登录系统
- **注册功能**: 新用户注册账号
- **Token管理**: 自动刷新和存储访问令牌
- **路由守卫**: 保护需要登录的页面
### 2. 邮件管理模块
- **撰写邮件**: 创建带有未来发送时间的邮件
- **收件箱**: 查看接收到的邮件
- **发件箱**: 查看已发送的邮件
- **草稿箱**: 管理未发送的邮件草稿
- **邮件操作**: 编辑、删除、撤销发送等
### 3. 时光胶囊模块
- **3D胶囊视图**: 在深空背景中展示时光胶囊
- **胶囊交互**: 点击胶囊查看详情
- **胶囊样式**: 自定义胶囊外观
- **倒计时显示**: 显示距离发送的时间
### 4. AI助手模块
- **写作辅助**: AI帮助生成邮件开头
- **内容建议**: 提供邮件内容建议
- **情感分析**: 分析邮件内容的情感倾向
### 5. 个人空间模块
- **时间线**: 展示用户的邮件历史
- **数据统计**: 邮件发送/接收统计
- **个人信息**: 管理用户资料
## 页面功能详解
### 1. 登录页面 (Login.vue)
- 用户登录表单
- 记住登录状态选项
- 跳转注册页面链接
- 深空主题背景设计
### 2. 注册页面 (Register.vue)
- 用户注册表单
- 密码确认验证
- 用户协议同意选项
- 跳转登录页面链接
### 3. 首页/时光胶囊 (Home.vue)
- 个性化欢迎语
- 3D时光胶囊展示
- 星空动态背景
- 搜索和通知功能
- 底部导航栏
- 悬浮撰写按钮
### 4. 撰写邮件 (Compose.vue)
- 收件人类型选择(自己/指定/公开)
- 发送时间选择(预设时间/自定义时间)
- 邮件标题和内容编辑
- 附件上传功能
- AI写作辅助
- 胶囊样式选择
- 存入草稿和发送按钮
### 5. 收件箱 (Inbox.vue)
- 邮件列表展示
- 邮件状态筛选
- 邮件详情查看
- 搜索功能
### 6. 发件箱 (Sent.vue)
- 已发送邮件列表
- 邮件状态显示
- 撤销发送功能
- 邮件详情查看
### 7. 个人中心 (Profile.vue)
- 用户信息展示
- 订阅信息显示
- 功能设置选项
- 退出登录功能
### 8. 时间线 (Timeline.vue)
- 按时间顺序展示邮件
- 邮件情感标记
- 月份统计图表
### 9. 胶囊详情 (CapsuleDetail.vue)
- 邮件完整内容展示
- 发送/接收信息
- 附件查看
- 相关操作按钮
## 状态管理
项目使用Vue 3的响应式API进行状态管理主要状态包括
### 用户状态 (userState)
```javascript
{
isLoggedIn: boolean, // 登录状态
token: string, // 访问令牌
refreshToken: string, // 刷新令牌
userInfo: { // 用户信息
userId: string,
username: string,
email: string,
avatar: string
},
subscription: { // 订阅信息
plan: string,
remainingMails: number,
features: object
}
}
```
### 邮件状态 (mailState)
```javascript
{
inboxList: [], // 收件箱邮件列表
sentList: [], // 发件箱邮件列表
draftList: [], // 草稿箱邮件列表
currentMail: {}, // 当前查看的邮件
totalCounts: { // 各类邮件计数
inbox: number,
sent: number,
draft: number
}
}
```
### 胶囊状态 (capsuleState)
```javascript
{
capsules: [], // 时光胶囊列表
scene: string, // 场景类型
background: string // 背景配置
}
```
### 时间线状态 (timelineState)
```javascript
{
timeline: [], // 时间线数据
statistics: {} // 统计数据
}
```
## API接口设计
### 认证相关
- `POST /api/v1/auth/register` - 用户注册
- `POST /api/v1/auth/login` - 用户登录
- `POST /api/v1/auth/refresh` - 刷新令牌
- `POST /api/v1/auth/logout` - 退出登录
### 邮件管理
- `GET /api/v1/mails` - 获取邮件列表
- `POST /api/v1/mails` - 创建邮件
- `GET /api/v1/mails/{id}` - 获取邮件详情
- `PUT /api/v1/mails/{id}` - 更新邮件
- `DELETE /api/v1/mails/{id}` - 删除邮件
- `POST /api/v1/mails/{id}/revoke` - 撤销发送
### 时光胶囊
- `GET /api/v1/capsules` - 获取胶囊视图
- `PUT /api/v1/capsules/{id}/style` - 更新胶囊样式
### AI助手
- `POST /api/v1/ai/writing-assistant` - 写作辅助
- `POST /api/v1/ai/sentiment-analysis` - 情感分析
### 个人空间
- `GET /api/v1/timeline` - 获取时间线
- `GET /api/v1/statistics` - 获取统计数据
- `GET /api/v1/user/profile` - 获取用户信息
### 文件上传
- `POST /api/v1/upload/attachment` - 上传附件
- `POST /api/v1/upload/avatar` - 上传头像
## 环境配置
### 开发环境 (.env.development)
```
VUE_APP_API_BASE_URL=http://localhost:3000/api/v1
VUE_APP_TITLE=ChronoMail - 未来邮箱
```
### 生产环境 (.env.production)
```
VUE_APP_API_BASE_URL=https://api.chronomail.com/api/v1
VUE_APP_TITLE=ChronoMail - 未来邮箱
```
## 开发指南
### 1. 安装依赖
```bash
npm install
```
### 2. 启动开发服务器
```bash
npm run serve
```
### 3. 构建生产版本
```bash
npm run build
```
### 4. 运行Electron应用
```bash
npm run electron:serve
```
### 5. 代码检查
```bash
npm run lint
```
## 组件开发规范
### 1. 组件结构
```vue
<template>
<!-- 模板内容 -->
</template>
<script>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
export default {
name: 'ComponentName',
setup() {
// 响应式数据
const data = ref('')
// 计算属性
const computedData = computed(() => {
// 计算逻辑
return data.value
})
// 方法
const method = () => {
// 方法实现
}
// 生命周期
onMounted(() => {
// 初始化逻辑
})
// 返回需要在模板中使用的数据和方法
return {
data,
computedData,
method
}
}
}
</script>
<style scoped>
/* 组件样式 */
</style>
```
### 2. 状态管理
- 使用响应式API创建状态
- 通过操作方法修改状态
- 避免直接修改状态,使用提供的操作方法
### 3. API调用
- 使用统一的API模块
- 处理加载状态和错误情况
- 使用async/await处理异步操作
## 样式设计
### 1. 主题色彩
- 主色调:深空蓝 (#0A0E27)
- 强调色:星云紫 (#6A5ACD)
- 点缀色:星光蓝 (#4285F4)
- 背景色:深空黑 (#050714)
### 2. 字体规范
- 主标题24px粗体
- 副标题18px中等
- 正文14px常规
- 辅助文本12px常规
### 3. 间距规范
- 大间距24px
- 中间距16px
- 小间距8px
- 微间距4px
### 4. 组件样式
- 使用Vant组件库作为基础
- 通过CSS变量覆盖默认样式
- 使用scoped样式避免污染
## 部署说明
### 1. Web应用部署
1. 执行构建命令:`npm run build`
2. 将dist目录上传至Web服务器
3. 配置服务器路由重定向
### 2. Electron应用打包
1. 安装electron-builder`npm install electron-builder --save-dev`
2. 配置package.json的build字段
3. 执行打包命令:`npm run electron:build`
## 性能优化
### 1. 代码分割
- 使用路由懒加载
- 按需加载组件
- 分离第三方库
### 2. 资源优化
- 图片压缩和懒加载
- 字体文件优化
- CSS代码压缩
### 3. 缓存策略
- 静态资源长期缓存
- API响应缓存
- 离线数据缓存
## 测试策略
### 1. 单元测试
- 组件渲染测试
- 方法逻辑测试
- 状态管理测试
### 2. 集成测试
- 页面跳转测试
- API调用测试
- 用户交互测试
### 3. 端到端测试
- 关键流程测试
- 跨浏览器兼容性测试
- 移动端适配测试
## 常见问题
### 1. API请求失败
- 检查网络连接
- 确认API地址配置
- 验证Token有效性
### 2. 页面加载缓慢
- 检查资源大小
- 优化图片资源
- 启用Gzip压缩
### 3. 移动端适配问题
- 检查视口设置
- 调整字体大小
- 优化触摸交互
## 未来规划
### 1. 功能扩展
- 多媒体邮件支持
- 群组邮件功能
- 邮件模板库
### 2. 技术升级
- 迁移到Vite构建工具
- 引入TypeScript
- 实现PWA功能
### 3. 用户体验优化
- 个性化主题
- 智能推荐系统
- 社交分享功能
## 联系方式
如有问题或建议,请联系开发团队。
---
*最后更新时间2024年6月*

View File

@@ -141,14 +141,316 @@ body {
}
/* 页面过渡动画 */
.page-enter-active, .page-leave-active {
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.page-enter-from {
opacity: 0;
transform: translateX(30px) scale(0.95);
}
.page-leave-to {
opacity: 0;
transform: translateX(-30px) scale(0.95);
}
/* 淡入淡出过渡 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
/* 滑动过渡 */
.slide-up-enter-active, .slide-up-leave-active {
transition: all 0.3s ease;
}
.slide-up-enter-from {
opacity: 0;
transform: translateY(30px);
}
.slide-up-leave-to {
opacity: 0;
transform: translateY(-30px);
}
.slide-down-enter-active, .slide-down-leave-active {
transition: all 0.3s ease;
}
.slide-down-enter-from {
opacity: 0;
transform: translateY(-30px);
}
.slide-down-leave-to {
opacity: 0;
transform: translateY(30px);
}
/* 缩放过渡 */
.scale-enter-active, .scale-leave-active {
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.scale-enter-from, .scale-leave-to {
opacity: 0;
transform: scale(0.8);
}
/* 弹性过渡 */
.bounce-enter-active {
animation: bounce-in 0.6s;
}
.bounce-leave-active {
animation: bounce-in 0.6s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
/* 翻转过渡 */
.flip-enter-active {
animation: flip-in 0.6s;
}
.flip-leave-active {
animation: flip-out 0.6s;
}
@keyframes flip-in {
from {
transform: rotateY(90deg);
opacity: 0;
}
to {
transform: rotateY(0);
opacity: 1;
}
}
@keyframes flip-out {
from {
transform: rotateY(0);
opacity: 1;
}
to {
transform: rotateY(-90deg);
opacity: 0;
}
}
/* 旋转过渡 */
.rotate-enter-active {
animation: rotate-in 0.5s;
}
.rotate-leave-active {
animation: rotate-out 0.5s;
}
@keyframes rotate-in {
from {
transform: rotate(-180deg) scale(0.8);
opacity: 0;
}
to {
transform: rotate(0) scale(1);
opacity: 1;
}
}
@keyframes rotate-out {
from {
transform: rotate(0) scale(1);
opacity: 1;
}
to {
transform: rotate(180deg) scale(0.8);
opacity: 0;
}
}
/* 交错动画 */
.stagger-item {
opacity: 0;
animation: stagger-fade-in 0.5s forwards;
}
@keyframes stagger-fade-in {
to {
opacity: 1;
}
}
/* 波纹效果 */
.ripple {
position: relative;
overflow: hidden;
}
.ripple::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.5);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.ripple:active::before {
width: 300px;
height: 300px;
}
/* 呼吸效果 */
.breathing {
animation: breathing 3s infinite ease-in-out;
}
@keyframes breathing {
0%, 100% {
transform: scale(1);
opacity: 0.8;
}
50% {
transform: scale(1.05);
opacity: 1;
}
}
/* 摇摆效果 */
.wobble {
animation: wobble 1s ease-in-out;
}
@keyframes wobble {
0%, 100% {
transform: translateX(0);
}
15% {
transform: translateX(-5px) rotate(-5deg);
}
30% {
transform: translateX(5px) rotate(5deg);
}
45% {
transform: translateX(-5px) rotate(-5deg);
}
60% {
transform: translateX(5px) rotate(5deg);
}
75% {
transform: translateX(-5px) rotate(-5deg);
}
90% {
transform: translateX(5px) rotate(5deg);
}
}
/* 悬浮效果 */
.hover-lift {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.hover-lift:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
}
/* 发光效果 */
.glow {
transition: box-shadow 0.3s ease;
}
.glow:hover {
box-shadow: 0 0 15px rgba(0, 212, 255, 0.7);
}
/* 脉冲效果 */
.pulse {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% {
box-shadow: 0 0 0 0 rgba(0, 212, 255, 0.7);
}
70% {
box-shadow: 0 0 0 10px rgba(0, 212, 255, 0);
}
100% {
box-shadow: 0 0 0 0 rgba(0, 212, 255, 0);
}
}
/* 闪烁效果 */
.flash {
animation: flash 2s infinite;
}
@keyframes flash {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
/* 渐变背景动画 */
.gradient-bg {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient-shift 15s ease infinite;
}
@keyframes gradient-shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* 形状变换动画 */
.morph {
animation: morph 8s ease-in-out infinite;
}
@keyframes morph {
0%, 100% {
border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
}
50% {
border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%;
}
}
/* 滑动淡入过渡 */
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
@@ -163,6 +465,573 @@ body {
opacity: 0;
}
/* 3D翻转效果 */
.flip-3d {
perspective: 1000px;
}
.flip-3d-inner {
transition: transform 0.6s;
transform-style: preserve-3d;
}
.flip-3d:hover .flip-3d-inner {
transform: rotateY(180deg);
}
.flip-3d-front, .flip-3d-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
}
.flip-3d-back {
transform: rotateY(180deg);
}
/* 立方体效果 */
.cube-container {
perspective: 1000px;
width: 100px;
height: 100px;
}
.cube {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 1s;
}
.cube-face {
position: absolute;
width: 100px;
height: 100px;
border: 2px solid rgba(0, 212, 255, 0.5);
background: rgba(0, 20, 40, 0.7);
backdrop-filter: blur(10px);
}
.cube-face-front { transform: translateZ(50px); }
.cube-face-back { transform: rotateY(180deg) translateZ(50px); }
.cube-face-right { transform: rotateY(90deg) translateZ(50px); }
.cube-face-left { transform: rotateY(-90deg) translateZ(50px); }
.cube-face-top { transform: rotateX(90deg) translateZ(50px); }
.cube-face-bottom { transform: rotateX(-90deg) translateZ(50px); }
.cube-container:hover .cube {
transform: rotateX(45deg) rotateY(45deg);
}
/* 震动效果 */
.shake {
animation: shake 0.5s;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-10px); }
20%, 40%, 60%, 80% { transform: translateX(10px); }
}
/* 滚动显示动画 */
.scroll-reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.scroll-reveal.active {
opacity: 1;
transform: translateY(0);
}
/* 打字机效果 */
.typewriter {
overflow: hidden;
white-space: nowrap;
animation: typing 3.5s steps(40, end);
}
@keyframes typing {
from { width: 0 }
to { width: 100% }
}
/* 弹跳进入效果 */
.bounce-in {
animation: bounceIn 0.8s;
}
@keyframes bounceIn {
0% {
opacity: 0;
transform: scale(0.3);
}
50% {
opacity: 1;
transform: scale(1.05);
}
70% {
transform: scale(0.9);
}
100% {
opacity: 1;
transform: scale(1);
}
}
/* 缩放旋转进入 */
.zoom-rotate-in {
animation: zoomRotateIn 0.6s;
}
@keyframes zoomRotateIn {
from {
opacity: 0;
transform: scale(0) rotate(180deg);
}
to {
opacity: 1;
transform: scale(1) rotate(0);
}
}
/* 滑动门效果 */
.sliding-door {
position: relative;
overflow: hidden;
}
.sliding-door::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
transition: left 0.5s;
}
.sliding-door:hover::before {
left: 100%;
}
/* 液体填充效果 */
.liquid-fill {
position: relative;
overflow: hidden;
}
.liquid-fill::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 0;
background: linear-gradient(to top, rgba(0, 212, 255, 0.3), transparent);
transition: height 0.5s ease;
}
.liquid-fill:hover::after {
height: 100%;
}
/* 边框动画 */
.border-animation {
position: relative;
}
.border-animation::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: 2px solid transparent;
border-radius: inherit;
transition: border-color 0.3s;
}
.border-animation:hover::before {
border-color: rgba(0, 212, 255, 0.8);
animation: borderRotate 2s linear infinite;
}
@keyframes borderRotate {
0% {
border-top-color: rgba(0, 212, 255, 0.8);
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
}
25% {
border-top-color: transparent;
border-right-color: rgba(0, 212, 255, 0.8);
border-bottom-color: transparent;
border-left-color: transparent;
}
50% {
border-top-color: transparent;
border-right-color: transparent;
border-bottom-color: rgba(0, 212, 255, 0.8);
border-left-color: transparent;
}
75% {
border-top-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: rgba(0, 212, 255, 0.8);
}
100% {
border-top-color: rgba(0, 212, 255, 0.8);
border-right-color: transparent;
border-bottom-color: transparent;
border-left-color: transparent;
}
}
/* 文字闪烁效果 */
.text-glow {
animation: textGlow 2s infinite alternate;
}
@keyframes textGlow {
from {
text-shadow: 0 0 10px #fff, 0 0 20px #fff, 0 0 30px #e60073, 0 0 40px #e60073;
}
to {
text-shadow: 0 0 20px #fff, 0 0 30px #ff4da6, 0 0 40px #ff4da6, 0 0 50px #ff4da6;
}
}
/* 磁性吸附效果 */
.magnetic {
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* 粒子爆炸效果 */
.particle-explosion {
position: relative;
}
.particle {
position: absolute;
width: 4px;
height: 4px;
border-radius: 50%;
background: rgba(0, 212, 255, 0.8);
opacity: 0;
}
.particle-explosion.active .particle {
animation: particleExplode 1s forwards;
}
@keyframes particleExplode {
0% {
transform: translate(0, 0);
opacity: 1;
}
100% {
transform: translate(var(--tx), var(--ty));
opacity: 0;
}
}
/* 光线扫描效果 */
.scan-line {
position: relative;
overflow: hidden;
}
.scan-line::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(0, 212, 255, 0.8), transparent);
animation: scan 3s infinite;
}
@keyframes scan {
0% {
top: 0;
}
50% {
top: calc(100% - 2px);
}
100% {
top: 0;
}
}
/* 移动端触摸交互优化 */
/* 触摸反馈 */
.touch-feedback {
position: relative;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
.touch-feedback::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
transform: translate(-50%, -50%);
transition: width 0.4s, height 0.4s, opacity 0.4s;
pointer-events: none;
}
.touch-feedback.active::after {
width: 200px;
height: 200px;
opacity: 0;
}
/* 触摸滑动支持 */
.swipe-container {
touch-action: pan-x;
overflow-x: hidden;
position: relative;
}
.swipe-container.vertical {
touch-action: pan-y;
overflow-x: hidden;
overflow-y: hidden;
}
.swipe-item {
transition: transform 0.3s ease-out;
}
/* 移动端按钮优化 */
.mobile-button {
min-height: 44px; /* iOS推荐的最小触摸目标尺寸 */
min-width: 44px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
-webkit-tap-highlight-color: transparent;
}
/* 移动端卡片优化 */
.mobile-card {
margin: 8px 0;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
-webkit-tap-highlight-color: transparent;
}
.mobile-card:active {
transform: scale(0.98);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
/* 移动端列表项优化 */
.mobile-list-item {
padding: 12px 16px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
transition: background-color 0.2s;
-webkit-tap-highlight-color: transparent;
}
.mobile-list-item:active {
background-color: rgba(0, 212, 255, 0.1);
}
/* 移动端输入框优化 */
.mobile-input {
padding: 12px 16px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.2);
background-color: rgba(255, 255, 255, 0.05);
font-size: 16px; /* 防止iOS缩放 */
-webkit-appearance: none;
-webkit-tap-highlight-color: transparent;
}
/* 移动端滚动优化 */
.mobile-scroll {
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
scroll-behavior: smooth;
}
/* 移动端模态框优化 */
.mobile-modal {
padding: 20px;
max-height: 80vh;
overflow-y: auto;
-webkit-overflow-scrolling: touch;
}
/* 移动端标签页优化 */
.mobile-tab {
padding: 10px 16px;
font-size: 14px;
min-height: 44px;
display: flex;
align-items: center;
justify-content: center;
-webkit-tap-highlight-color: transparent;
}
/* 移动端下拉菜单优化 */
.mobile-dropdown {
min-height: 44px;
padding: 10px 16px;
-webkit-tap-highlight-color: transparent;
}
/* 移动端开关优化 */
.mobile-switch {
min-width: 51px;
min-height: 31px;
-webkit-tap-highlight-color: transparent;
}
/* 移动端滑块优化 */
.mobile-slider {
height: 44px;
padding: 0 16px;
-webkit-tap-highlight-color: transparent;
}
/* 移动端手势提示 */
.swipe-hint {
position: relative;
overflow: hidden;
}
.swipe-hint::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 30px;
height: 100%;
background: linear-gradient(to left, rgba(0, 0, 0, 0.2), transparent);
opacity: 0.7;
}
.swipe-hint::after {
content: '→';
position: absolute;
top: 50%;
right: 8px;
transform: translateY(-50%);
color: rgba(255, 255, 255, 0.7);
font-size: 18px;
animation: swipeHint 2s infinite;
}
@keyframes swipeHint {
0%, 100% {
opacity: 0.7;
transform: translateY(-50%) translateX(0);
}
50% {
opacity: 0.3;
transform: translateY(-50%) translateX(-5px);
}
}
/* 移动端长按效果 */
.long-press {
position: relative;
-webkit-tap-highlight-color: transparent;
}
.long-press.active {
animation: longPress 0.5s ease-out;
}
@keyframes longPress {
0% {
transform: scale(1);
}
50% {
transform: scale(0.95);
}
100% {
transform: scale(1);
}
}
/* 移动端弹性滚动 */
.elastic-scroll {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overscroll-behavior: contain;
}
/* 移动端安全区域适配 */
.safe-area-top {
padding-top: constant(safe-area-inset-top);
padding-top: env(safe-area-inset-top);
}
.safe-area-bottom {
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-left {
padding-left: constant(safe-area-inset-left);
padding-left: env(safe-area-inset-left);
}
.safe-area-right {
padding-right: constant(safe-area-inset-right);
padding-right: env(safe-area-inset-right);
}
/* 移动端横屏适配 */
@media screen and (orientation: landscape) and (max-height: 500px) {
.mobile-modal {
max-height: 90vh;
}
.mobile-card {
margin: 4px 0;
}
}
/* 移动端小屏幕适配 */
@media screen and (max-width: 375px) {
.mobile-card {
margin: 6px 0;
border-radius: 10px;
}
.mobile-list-item {
padding: 10px 14px;
}
.mobile-button {
min-height: 40px;
min-width: 40px;
}
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 8px;

View File

@@ -15,10 +15,14 @@ import './assets/styles/global.css'
// 引入状态管理
import { userActions } from './store'
// 引入移动端触摸交互插件
import touchPlugin from './plugins/touch'
const app = createApp(App)
app.use(router)
app.use(Vant)
app.use(touchPlugin)
// 初始化用户状态
userActions.initUserState()

16
src/plugins/touch.js Normal file
View File

@@ -0,0 +1,16 @@
import { touchDirectives } from './touch';
/**
* 移动端触摸交互Vue插件
*/
export default {
install(app) {
// 注册所有触摸指令
Object.keys(touchDirectives).forEach(key => {
app.directive(key, touchDirectives[key]);
});
}
};
// 单独导出指令,以便按需使用
export { touchDirectives };

392
src/utils/touch.js Normal file
View File

@@ -0,0 +1,392 @@
/**
* 移动端触摸交互工具函数
*/
// 触摸反馈效果
export function addTouchFeedback(element, callback) {
if (!element) return;
const handleTouchStart = (e) => {
element.classList.add('active');
};
const handleTouchEnd = (e) => {
setTimeout(() => {
element.classList.remove('active');
if (callback) callback(e);
}, 150);
};
element.addEventListener('touchstart', handleTouchStart, { passive: true });
element.addEventListener('touchend', handleTouchEnd, { passive: true });
element.addEventListener('touchcancel', handleTouchEnd, { passive: true });
// 返回清理函数
return () => {
element.removeEventListener('touchstart', handleTouchStart);
element.removeEventListener('touchend', handleTouchEnd);
element.removeEventListener('touchcancel', handleTouchEnd);
};
}
// 滑动手势检测
export function detectSwipe(element, options = {}) {
if (!element) return;
const {
onSwipeLeft,
onSwipeRight,
onSwipeUp,
onSwipeDown,
threshold = 50, // 最小滑动距离
restraint = 100, // 最大垂直方向移动距离
allowedTime = 300 // 最大滑动时间
} = options;
let touchObject = {};
const handleTouchStart = (e) => {
touchObject = {
startX: e.touches[0].pageX,
startY: e.touches[0].pageY,
startTime: new Date().getTime()
};
};
const handleTouchMove = (e) => {
// 可以在这里添加实时滑动效果
};
const handleTouchEnd = (e) => {
if (!touchObject.startTime) return;
const elapsedTime = new Date().getTime() - touchObject.startTime;
const distX = e.changedTouches[0].pageX - touchObject.startX;
const distY = e.changedTouches[0].pageY - touchObject.startY;
if (elapsedTime <= allowedTime) {
if (Math.abs(distX) >= threshold && Math.abs(distY) <= restraint) {
// 水平滑动
if (distX > 0 && onSwipeRight) {
onSwipeRight();
} else if (distX < 0 && onSwipeLeft) {
onSwipeLeft();
}
} else if (Math.abs(distY) >= threshold && Math.abs(distX) <= restraint) {
// 垂直滑动
if (distY > 0 && onSwipeDown) {
onSwipeDown();
} else if (distY < 0 && onSwipeUp) {
onSwipeUp();
}
}
}
touchObject = {};
};
element.addEventListener('touchstart', handleTouchStart, { passive: true });
element.addEventListener('touchmove', handleTouchMove, { passive: true });
element.addEventListener('touchend', handleTouchEnd, { passive: true });
// 返回清理函数
return () => {
element.removeEventListener('touchstart', handleTouchStart);
element.removeEventListener('touchmove', handleTouchMove);
element.removeEventListener('touchend', handleTouchEnd);
};
}
// 长按检测
export function detectLongPress(element, callback, duration = 500) {
if (!element) return;
let pressTimer;
const handleTouchStart = (e) => {
pressTimer = setTimeout(() => {
element.classList.add('active');
if (callback) callback(e);
}, duration);
};
const handleTouchEnd = (e) => {
clearTimeout(pressTimer);
setTimeout(() => {
element.classList.remove('active');
}, 150);
};
const handleTouchMove = (e) => {
clearTimeout(pressTimer);
};
element.addEventListener('touchstart', handleTouchStart, { passive: true });
element.addEventListener('touchend', handleTouchEnd, { passive: true });
element.addEventListener('touchmove', handleTouchMove, { passive: true });
// 返回清理函数
return () => {
clearTimeout(pressTimer);
element.removeEventListener('touchstart', handleTouchStart);
element.removeEventListener('touchend', handleTouchEnd);
element.removeEventListener('touchmove', handleTouchMove);
};
}
// 惯性滚动
export function enableInertiaScroll(element) {
if (!element) return;
let isScrolling = false;
let startY = 0;
let scrollTop = 0;
let velocity = 0;
let rafId = null;
let lastTime = 0;
let lastY = 0;
const handleTouchStart = (e) => {
isScrolling = true;
startY = e.touches[0].pageY;
scrollTop = element.scrollTop;
velocity = 0;
lastTime = Date.now();
lastY = startY;
if (rafId) {
cancelAnimationFrame(rafId);
rafId = null;
}
};
const handleTouchMove = (e) => {
if (!isScrolling) return;
const currentY = e.touches[0].pageY;
const currentTime = Date.now();
const deltaTime = currentTime - lastTime;
const deltaY = currentY - lastY;
// 计算速度
if (deltaTime > 0) {
velocity = deltaY / deltaTime;
}
element.scrollTop = scrollTop - (currentY - startY);
lastTime = currentTime;
lastY = currentY;
};
const handleTouchEnd = () => {
if (!isScrolling) return;
isScrolling = false;
// 应用惯性滚动
if (Math.abs(velocity) > 0.1) {
const inertiaScroll = () => {
velocity *= 0.95; // 摩擦系数
if (Math.abs(velocity) > 0.01) {
element.scrollTop -= velocity * 16; // 假设60fps每帧约16ms
rafId = requestAnimationFrame(inertiaScroll);
}
};
rafId = requestAnimationFrame(inertiaScroll);
}
};
element.addEventListener('touchstart', handleTouchStart, { passive: true });
element.addEventListener('touchmove', handleTouchMove, { passive: true });
element.addEventListener('touchend', handleTouchEnd, { passive: true });
// 返回清理函数
return () => {
if (rafId) {
cancelAnimationFrame(rafId);
}
element.removeEventListener('touchstart', handleTouchStart);
element.removeEventListener('touchmove', handleTouchMove);
element.removeEventListener('touchend', handleTouchEnd);
};
}
// 双击检测
export function detectDoubleTap(element, callback, timeout = 300) {
if (!element) return;
let lastTapTime = 0;
const handleTouchEnd = (e) => {
const currentTime = Date.now();
const tapLength = currentTime - lastTapTime;
if (tapLength < timeout && tapLength > 0) {
if (callback) callback(e);
e.preventDefault();
}
lastTapTime = currentTime;
};
element.addEventListener('touchend', handleTouchEnd, { passive: false });
// 返回清理函数
return () => {
element.removeEventListener('touchend', handleTouchEnd);
};
}
// 捏合缩放
export function detectPinchZoom(element, options = {}) {
if (!element) return;
const {
onZoomIn,
onZoomOut,
onZoomChange,
minScale = 0.5,
maxScale = 3,
scaleStep = 0.1
} = options;
let initialDistance = 0;
let currentScale = 1;
const handleTouchStart = (e) => {
if (e.touches.length === 2) {
initialDistance = getDistance(e.touches[0], e.touches[1]);
e.preventDefault();
}
};
const handleTouchMove = (e) => {
if (e.touches.length === 2) {
const currentDistance = getDistance(e.touches[0], e.touches[1]);
if (initialDistance > 0) {
const scale = currentDistance / initialDistance;
currentScale = Math.min(Math.max(1 + (scale - 1) * scaleStep, minScale), maxScale);
if (onZoomChange) {
onZoomChange(currentScale);
}
}
e.preventDefault();
}
};
const handleTouchEnd = (e) => {
if (currentScale > 1 && onZoomIn) {
onZoomIn();
} else if (currentScale < 1 && onZoomOut) {
onZoomOut();
}
initialDistance = 0;
currentScale = 1;
};
// 计算两点之间的距离
function getDistance(touch1, touch2) {
const dx = touch1.pageX - touch2.pageX;
const dy = touch1.pageY - touch2.pageY;
return Math.sqrt(dx * dx + dy * dy);
}
element.addEventListener('touchstart', handleTouchStart, { passive: false });
element.addEventListener('touchmove', handleTouchMove, { passive: false });
element.addEventListener('touchend', handleTouchEnd, { passive: true });
// 返回清理函数
return () => {
element.removeEventListener('touchstart', handleTouchStart);
element.removeEventListener('touchmove', handleTouchMove);
element.removeEventListener('touchend', handleTouchEnd);
};
}
// Vue自定义指令
export const touchDirectives = {
// 触摸反馈指令
'touch-feedback': {
mounted(el, binding) {
const cleanup = addTouchFeedback(el, binding.value);
el._cleanup = cleanup;
},
unmounted(el) {
if (el._cleanup) {
el._cleanup();
}
}
},
// 滑动指令
'swipe': {
mounted(el, binding) {
const cleanup = detectSwipe(el, binding.value);
el._cleanup = cleanup;
},
unmounted(el) {
if (el._cleanup) {
el._cleanup();
}
}
},
// 长按指令
'long-press': {
mounted(el, binding) {
const cleanup = detectLongPress(el, binding.value);
el._cleanup = cleanup;
},
unmounted(el) {
if (el._cleanup) {
el._cleanup();
}
}
},
// 惯性滚动指令
'inertia-scroll': {
mounted(el) {
const cleanup = enableInertiaScroll(el);
el._cleanup = cleanup;
},
unmounted(el) {
if (el._cleanup) {
el._cleanup();
}
}
},
// 双击指令
'double-tap': {
mounted(el, binding) {
const cleanup = detectDoubleTap(el, binding.value);
el._cleanup = cleanup;
},
unmounted(el) {
if (el._cleanup) {
el._cleanup();
}
}
},
// 捏合缩放指令
'pinch-zoom': {
mounted(el, binding) {
const cleanup = detectPinchZoom(el, binding.value);
el._cleanup = cleanup;
},
unmounted(el) {
if (el._cleanup) {
el._cleanup();
}
}
}
};

View File

@@ -14,32 +14,57 @@
<!-- 撰写表单 -->
<div class="compose-content">
<div class="form-section glass-card p-20">
<div class="form-container">
<!-- 收件人选择 -->
<div class="form-group">
<h3 class="font-semibold">收件人</h3>
<van-radio-group v-model="recipientType" direction="horizontal">
<van-radio name="SELF">自己</van-radio>
<van-radio name="SPECIFIC">他人</van-radio>
<van-radio name="PUBLIC">任意有缘人</van-radio>
</van-radio-group>
<div class="form-section">
<div class="section-header">
<van-icon name="friends-o" class="section-icon" />
<span class="section-title">收件人</span>
</div>
<div class="radio-group">
<div
v-for="option in recipientOptions"
:key="option.value"
class="radio-option"
:class="{'radio-selected': recipientType === option.value}"
@click="selectRecipientType(option.value)"
>
<div class="radio-indicator"></div>
<span class="radio-label">{{ option.label }}</span>
</div>
</div>
<div v-if="recipientType === 'SPECIFIC'" class="form-field">
<van-field
v-if="recipientType === 'SPECIFIC'"
v-model="recipientEmail"
placeholder="收件人邮箱"
class="custom-field mt-10"
/>
</div>
</div>
<!-- 发送时间选择 -->
<div class="form-group mt-20">
<h3 class="font-semibold">发送时间</h3>
<van-radio-group v-model="timeType" direction="horizontal">
<van-radio name="preset">预设时间</van-radio>
<van-radio name="custom">自定义</van-radio>
<van-radio name="condition">条件触发</van-radio>
</van-radio-group>
<div class="form-section">
<div class="section-header">
<van-icon name="clock-o" class="section-icon" />
<span class="section-title">发送时间</span>
</div>
<div class="time-selector">
<div
v-for="option in timeOptions"
:key="option.value"
class="time-option"
:class="{'time-selected': timeType === option.value}"
@click="selectTriggerType(option.value)"
>
<div class="time-icon">
<van-icon :name="option.icon" />
</div>
<div class="time-content">
<div class="time-label">{{ option.label }}</div>
<div class="time-desc">{{ option.description }}</div>
</div>
</div>
</div>
<div v-if="timeType === 'preset'" class="preset-options mt-10">
<van-button
@@ -72,21 +97,28 @@
</div>
<!-- 邮件内容 -->
<div class="form-group mt-20">
<h3 class="font-semibold">邮件内容</h3>
<div class="form-section">
<div class="section-header">
<van-icon name="envelop-o" class="section-icon" />
<span class="section-title">邮件内容</span>
</div>
<div class="form-field">
<van-field
v-model="subject"
placeholder="标题"
class="custom-field"
class="custom-field title-field"
/>
</div>
<div class="form-field">
<van-field
v-model="content"
type="textarea"
placeholder="写下你想对未来的自己说的话..."
rows="8"
autosize
class="custom-field mt-10"
class="custom-field content-field"
/>
</div>
<!-- 附件和多媒体 -->
<div class="media-options mt-10">
@@ -105,8 +137,11 @@
</div>
<!-- AI助手 -->
<div class="form-group mt-20">
<h3 class="font-semibold">AI写作助手</h3>
<div class="form-section">
<div class="section-header">
<van-icon name="bulb-o" class="section-icon" />
<span class="section-title">AI写作助手</span>
</div>
<van-cell-group>
<van-cell title="生成开头" is-link @click="generateOpening" />
<van-cell title="内容建议" is-link @click="generateSuggestions" />
@@ -176,6 +211,35 @@ export default {
const isEncrypted = ref(false) // 是否加密
const capsuleStyle = ref('default') // 胶囊样式
// 收件人选项
const recipientOptions = [
{ value: 'SELF', label: '自己' },
{ value: 'SPECIFIC', label: '指定收件人' },
{ value: 'PUBLIC', label: '公开信' }
]
// 时间选项
const timeOptions = [
{
value: 'preset',
label: '预设时间',
description: '选择一个预设的未来时间点',
icon: 'clock-o'
},
{
value: 'custom',
label: '自定义',
description: '自定义精确的发送时间',
icon: 'edit'
},
{
value: 'condition',
label: '条件触发',
description: '根据特定条件自动发送',
icon: 'fire-o'
}
]
// 弹窗控制
const showLocationPicker = ref(false)
const showEventPicker = ref(false)
@@ -296,6 +360,16 @@ export default {
router.back()
}
// 选择收件人类型
const selectRecipientType = (type) => {
recipientType.value = type
}
// 选择触发类型
const selectTriggerType = (type) => {
timeType.value = type
}
// 选择预设时间
const selectPresetTime = (value) => {
selectedPresetTime.value = value
@@ -633,7 +707,11 @@ export default {
showEventPicker,
triggerEvents,
areaList,
recipientOptions,
timeOptions,
goBack,
selectRecipientType,
selectTriggerType,
selectPresetTime,
onLocationConfirm,
selectEvent,
@@ -672,55 +750,212 @@ export default {
font-weight: bold;
}
/* 撰写内容区域 */
.compose-content {
flex: 1;
overflow-y: auto;
padding: 0 15px;
padding: 0 0 80px 0;
}
/* 表单容器 */
.form-container {
padding: 16px;
display: flex;
flex-direction: column;
gap: 24px;
}
/* 表单区块 */
.form-section {
margin-bottom: 20px;
background: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 20px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.form-group h3 {
margin: 0 0 10px;
.form-section:hover {
transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
/* 区块标题 */
.section-header {
display: flex;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.section-icon {
font-size: 20px;
margin-right: 10px;
color: #00D4FF;
}
.section-title {
font-size: 16px;
font-weight: bold;
font-weight: 600;
color: white;
}
/* 单选组 */
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
}
.radio-option {
flex: 1;
min-width: 100px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 2px solid rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
cursor: pointer;
transition: all 0.3s ease;
}
.radio-option:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateY(-2px);
}
.radio-option.radio-selected {
background: rgba(0, 212, 255, 0.2);
border-color: #00D4FF;
box-shadow: 0 0 15px rgba(0, 212, 255, 0.3);
}
.radio-indicator {
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.5);
margin-right: 10px;
position: relative;
transition: all 0.3s ease;
}
.radio-option.radio-selected .radio-indicator {
border-color: #00D4FF;
background: #00D4FF;
}
.radio-option.radio-selected .radio-indicator::after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 6px;
height: 6px;
border-radius: 50%;
background: white;
}
.radio-label {
font-size: 14px;
color: white;
}
/* 时间选择器 */
.time-selector {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 16px;
}
.time-option {
display: flex;
align-items: center;
padding: 16px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 2px solid rgba(255, 255, 255, 0.1);
cursor: pointer;
transition: all 0.3s ease;
}
.time-option:hover {
background: rgba(255, 255, 255, 0.1);
transform: translateX(4px);
}
.time-option.time-selected {
background: rgba(0, 212, 255, 0.2);
border-color: #00D4FF;
box-shadow: 0 0 15px rgba(0, 212, 255, 0.3);
}
.time-icon {
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
margin-right: 16px;
font-size: 20px;
color: #00D4FF;
}
.time-content {
flex: 1;
}
.time-label {
font-size: 16px;
font-weight: 600;
color: white;
margin-bottom: 4px;
}
.time-desc {
font-size: 13px;
color: rgba(255, 255, 255, 0.7);
}
/* 表单字段 */
.form-field {
margin-bottom: 16px;
}
.custom-field {
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
background: rgba(255, 255, 255, 0.05);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
transition: all 0.3s ease;
}
.custom-field :deep(.van-field__control) {
color: var(--text-primary);
color: white;
}
/* 单选按钮选中样式 */
:deep(.van-radio-group) {
margin-bottom: 10px;
.custom-field:focus-within {
background: rgba(255, 255, 255, 0.08);
border-color: #00D4FF;
box-shadow: 0 0 10px rgba(0, 212, 255, 0.2);
}
:deep(.van-radio) {
margin-right: 15px;
.title-field {
font-weight: 600;
}
:deep(.van-radio__icon) {
font-size: 18px;
}
:deep(.van-radio__icon--checked .van-icon) {
background-color: #4285f4;
border-color: #4285f4;
box-shadow: 0 0 8px rgba(66, 133, 244, 0.5);
}
:deep(.van-radio__label) {
color: var(--text-primary);
font-size: 14px;
.content-field {
min-height: 120px;
}
/* 预设选项 */
.preset-options {
display: flex;
flex-wrap: wrap;
@@ -737,32 +972,32 @@ export default {
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.4);
}
.custom-date-picker {
background-color: rgba(255, 255, 255, 0.05);
border-radius: 8px;
}
/* 媒体选项 */
.media-options {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.media-uploader {
display: inline-block;
}
/* 底部操作 */
.footer-actions {
padding: 15px;
display: flex;
flex-direction: column;
gap: 10px;
background: rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
}
.save-button {
height: 50px;
font-size: 16px;
font-weight: bold;
margin-bottom: 10px;
}
.send-button {
@@ -771,14 +1006,7 @@ export default {
font-weight: bold;
}
.preset-button {
margin: 5px;
}
.media-uploader .van-button {
/* 按钮样式已由全局样式处理 */
}
/* 弹窗样式 */
.event-picker {
padding: 20px;
height: 100%;
@@ -788,5 +1016,46 @@ export default {
.event-picker h3 {
margin: 0 0 15px;
font-size: 18px;
color: white;
}
/* 响应式设计 */
@media (max-width: 768px) {
.form-container {
padding: 12px;
gap: 16px;
}
.form-section {
padding: 16px;
}
.radio-group {
flex-direction: column;
}
.radio-option {
min-width: auto;
}
.time-option {
padding: 12px;
}
.time-icon {
width: 36px;
height: 36px;
font-size: 18px;
margin-right: 12px;
}
.section-header {
margin-bottom: 12px;
}
.media-options {
flex-direction: column;
align-items: stretch;
}
}
</style>

View File

@@ -18,21 +18,37 @@
</div>
<!-- 时光胶囊视图 -->
<div class="capsules-container">
<div class="capsules-container mobile-scroll">
<div class="capsules-space" ref="capsulesSpace">
<!-- 3D透视层 -->
<div class="perspective-container">
<!-- 时间胶囊 -->
<div
v-for="capsule in capsules"
v-for="(capsule, index) in capsules"
:key="capsule.id"
class="capsule-wrapper"
:style="getCapsuleStyle(capsule)"
class="capsule-wrapper mobile-card touch-feedback"
:class="{'capsule-active': activeCapsule === capsule.id}"
:style="getCapsuleStyle(capsule, index)"
@click="openCapsule(capsule)"
@mouseenter="setActiveCapsule(capsule.id)"
@mouseleave="setActiveCapsule(null)"
v-touch-feedback="() => setActiveCapsule(capsule.id)"
v-swipe="{
onSwipeLeft: () => handleSwipeLeft(capsule),
onSwipeRight: () => handleSwipeRight(capsule),
onSwipeUp: () => handleSwipeUp(capsule),
onSwipeDown: () => handleSwipeDown(capsule)
}"
v-long-press="() => handleLongPress(capsule)"
>
<div class="time-capsule" :class="{'glowing': capsule.isGlowing}">
<div class="time-capsule" :class="{'glowing': capsule.isGlowing, 'active': activeCapsule === capsule.id}">
<div class="capsule-info">
<p class="capsule-title font-semibold">{{ capsule.title }}</p>
<p class="capsule-date text-xs text-secondary">{{ formatDate(capsule.deliveryDate) }}</p>
</div>
<!-- 胶囊光环效果 -->
<div class="capsule-ring" v-if="activeCapsule === capsule.id"></div>
</div>
</div>
</div>
</div>
@@ -107,6 +123,7 @@ export default {
const showSearch = ref(false)
const showNotifications = ref(false)
const searchValue = ref('')
const activeCapsule = ref(null) // 当前激活的胶囊
// 使用直接导入的状态和操作
const userName = computed(() => userState.userInfo.username || '时光旅人')
@@ -154,16 +171,57 @@ export default {
}
// 获取胶囊样式
const getCapsuleStyle = (capsule) => {
const getCapsuleStyle = (capsule, index) => {
// 添加基于索引的延迟动画
const delay = index * 0.1
return {
left: `${capsule.position.x}%`,
top: `${capsule.position.y}%`,
transform: `scale(${0.5 + capsule.position.z})`,
opacity: 0.5 + capsule.position.z * 0.5,
zIndex: Math.floor(capsule.position.z * 10)
zIndex: Math.floor(capsule.position.z * 10),
animationDelay: `${delay}s`,
'--glow-color': capsule.isGlowing ? '#00D4FF' : '#6A5ACD'
}
}
// 设置激活的胶囊
const setActiveCapsule = (capsuleId) => {
activeCapsule.value = capsuleId
}
// 处理滑动左
const handleSwipeLeft = (capsule) => {
console.log('向左滑动胶囊:', capsule.title)
// 可以添加删除或归档功能
}
// 处理滑动右
const handleSwipeRight = (capsule) => {
console.log('向右滑动胶囊:', capsule.title)
// 可以添加编辑功能
}
// 处理滑动上
const handleSwipeUp = (capsule) => {
console.log('向上滑动胶囊:', capsule.title)
// 可以添加置顶功能
}
// 处理滑动下
const handleSwipeDown = (capsule) => {
console.log('向下滑动胶囊:', capsule.title)
// 可以添加分享功能
}
// 处理长按
const handleLongPress = (capsule) => {
console.log('长按胶囊:', capsule.title)
// 可以添加更多操作选项
// 例如显示操作菜单
}
// 格式化日期
const formatDate = (date) => {
const now = new Date()
@@ -272,7 +330,14 @@ export default {
showNotifications,
searchValue,
notifications,
activeCapsule,
getCapsuleStyle,
setActiveCapsule,
handleSwipeLeft,
handleSwipeRight,
handleSwipeUp,
handleSwipeDown,
handleLongPress,
formatDate,
formatTime,
openCapsule,
@@ -313,22 +378,72 @@ export default {
flex: 1;
position: relative;
overflow: hidden;
perspective: 1200px;
-webkit-overflow-scrolling: touch;
}
.capsules-space {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.5s ease;
}
/* 3D透视容器 */
.perspective-container {
position: absolute;
width: 100%;
height: 100%;
transform-style: preserve-3d;
animation: float 20s infinite ease-in-out;
}
@keyframes float {
0%, 100% { transform: rotateX(0deg) rotateY(0deg); }
25% { transform: rotateX(2deg) rotateY(5deg); }
50% { transform: rotateX(-2deg) rotateY(-5deg); }
75% { transform: rotateX(1deg) rotateY(3deg); }
}
.capsule-wrapper {
position: absolute;
cursor: pointer;
transition: all 0.3s ease;
transform-style: preserve-3d;
animation: fadeInScale 0.8s ease forwards;
opacity: 0;
-webkit-tap-highlight-color: transparent;
user-select: none;
margin: 8px 0;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.capsule-wrapper:active {
transform: scale(0.98) translateZ(0);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
@keyframes fadeInScale {
0% {
opacity: 0;
transform: scale(0.5) translateZ(-50px);
}
100% {
opacity: 1;
transform: scale(1) translateZ(0);
}
}
.capsule-wrapper:hover {
transform: scale(1.1);
transform: scale(1.1) translateZ(30px);
z-index: 100;
}
.capsule-wrapper.capsule-active {
transform: scale(1.15) translateZ(50px);
z-index: 101;
}
.time-capsule {
@@ -340,6 +455,8 @@ export default {
box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
transition: all 0.3s ease;
animation: float 6s ease-in-out infinite;
transform-style: preserve-3d;
overflow: hidden;
}
.time-capsule::before {
@@ -355,6 +472,27 @@ export default {
opacity: 0.7;
}
.time-capsule.active::before {
opacity: 0.7;
animation: pulse 2s infinite;
}
.capsule-ring {
position: absolute;
top: -15px;
left: -15px;
right: -15px;
bottom: -15px;
border: 3px solid rgba(255, 255, 255, 0.6);
border-radius: 50%;
animation: rotate 10s linear infinite;
}
@keyframes rotate {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.time-capsule.glowing::before {
animation: pulse 2s infinite alternate;
}
@@ -413,6 +551,15 @@ export default {
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 {