diff --git a/PROJECT_DOCUMENTATION.md b/PROJECT_DOCUMENTATION.md new file mode 100644 index 0000000..98a45f6 --- /dev/null +++ b/PROJECT_DOCUMENTATION.md @@ -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 + + + + + +``` + +### 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月* \ No newline at end of file diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index 9e895ef..d85eeb2 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -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; diff --git a/src/main.js b/src/main.js index 1b7ac20..a9b1027 100644 --- a/src/main.js +++ b/src/main.js @@ -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() diff --git a/src/plugins/touch.js b/src/plugins/touch.js new file mode 100644 index 0000000..fd9f80c --- /dev/null +++ b/src/plugins/touch.js @@ -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 }; \ No newline at end of file diff --git a/src/utils/touch.js b/src/utils/touch.js new file mode 100644 index 0000000..9a9090a --- /dev/null +++ b/src/utils/touch.js @@ -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(); + } + } + } +}; \ No newline at end of file diff --git a/src/views/Compose.vue b/src/views/Compose.vue index 96108b3..7a403b7 100644 --- a/src/views/Compose.vue +++ b/src/views/Compose.vue @@ -14,32 +14,57 @@
-
+
-
-

收件人

- - 自己 - 他人 - 任意有缘人 - - - +
+
+ + 收件人 +
+
+
+
+ {{ option.label }} +
+
+
+ +
- + -
-

发送时间

- - 预设时间 - 自定义 - 条件触发 - +
+
+ + 发送时间 +
+
+
+
+ +
+
+
{{ option.label }}
+
{{ option.description }}
+
+
+
- + -
-

邮件内容

- - +
+
+ + 邮件内容 +
+
+ +
+
+ +
@@ -105,8 +137,11 @@
-
-

AI写作助手

+
+
+ + AI写作助手 +
@@ -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 @@ -619,31 +693,35 @@ export default { }) return { - stars, - recipientType, - recipientEmail, - timeType, - selectedPresetTime, - customDeliveryDate, - minDate, - presetTimeOptions, - subject, - content, - showLocationPicker, - showEventPicker, - triggerEvents, - areaList, - goBack, - selectPresetTime, - onLocationConfirm, - selectEvent, - afterRead, - generateOpening, - generateSuggestions, - analyzeEmotion, - saveDraft, - sendMail - } + stars, + recipientType, + recipientEmail, + timeType, + selectedPresetTime, + customDeliveryDate, + minDate, + presetTimeOptions, + subject, + content, + showLocationPicker, + showEventPicker, + triggerEvents, + areaList, + recipientOptions, + timeOptions, + goBack, + selectRecipientType, + selectTriggerType, + selectPresetTime, + onLocationConfirm, + selectEvent, + afterRead, + generateOpening, + generateSuggestions, + analyzeEmotion, + saveDraft, + sendMail +} } } @@ -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; + } } \ No newline at end of file diff --git a/src/views/Home.vue b/src/views/Home.vue index 7b98a3d..3cb3d65 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -18,20 +18,36 @@
-
+
- -
-
-
-

{{ capsule.title }}

-

{{ formatDate(capsule.deliveryDate) }}

+ +
+ +
+
+
+

{{ capsule.title }}

+

{{ formatDate(capsule.deliveryDate) }}

+
+ +
@@ -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 {