提交
This commit is contained in:
445
PROJECT_DOCUMENTATION.md
Normal file
445
PROJECT_DOCUMENTATION.md
Normal 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月*
|
||||||
@@ -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 {
|
.fade-enter-active, .fade-leave-active {
|
||||||
transition: opacity 0.5s;
|
transition: opacity 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-enter-from, .fade-leave-to {
|
.fade-enter-from, .fade-leave-to {
|
||||||
opacity: 0;
|
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 {
|
.slide-fade-enter-active {
|
||||||
transition: all 0.3s ease-out;
|
transition: all 0.3s ease-out;
|
||||||
}
|
}
|
||||||
@@ -163,6 +465,573 @@ body {
|
|||||||
opacity: 0;
|
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 {
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ import './assets/styles/global.css'
|
|||||||
// 引入状态管理
|
// 引入状态管理
|
||||||
import { userActions } from './store'
|
import { userActions } from './store'
|
||||||
|
|
||||||
|
// 引入移动端触摸交互插件
|
||||||
|
import touchPlugin from './plugins/touch'
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(Vant)
|
app.use(Vant)
|
||||||
|
app.use(touchPlugin)
|
||||||
|
|
||||||
// 初始化用户状态
|
// 初始化用户状态
|
||||||
userActions.initUserState()
|
userActions.initUserState()
|
||||||
|
|||||||
16
src/plugins/touch.js
Normal file
16
src/plugins/touch.js
Normal 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
392
src/utils/touch.js
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -14,32 +14,57 @@
|
|||||||
|
|
||||||
<!-- 撰写表单 -->
|
<!-- 撰写表单 -->
|
||||||
<div class="compose-content">
|
<div class="compose-content">
|
||||||
<div class="form-section glass-card p-20">
|
<div class="form-container">
|
||||||
<!-- 收件人选择 -->
|
<!-- 收件人选择 -->
|
||||||
<div class="form-group">
|
<div class="form-section">
|
||||||
<h3 class="font-semibold">收件人</h3>
|
<div class="section-header">
|
||||||
<van-radio-group v-model="recipientType" direction="horizontal">
|
<van-icon name="friends-o" class="section-icon" />
|
||||||
<van-radio name="SELF">自己</van-radio>
|
<span class="section-title">收件人</span>
|
||||||
<van-radio name="SPECIFIC">他人</van-radio>
|
</div>
|
||||||
<van-radio name="PUBLIC">任意有缘人</van-radio>
|
<div class="radio-group">
|
||||||
</van-radio-group>
|
<div
|
||||||
|
v-for="option in recipientOptions"
|
||||||
<van-field
|
:key="option.value"
|
||||||
v-if="recipientType === 'SPECIFIC'"
|
class="radio-option"
|
||||||
v-model="recipientEmail"
|
:class="{'radio-selected': recipientType === option.value}"
|
||||||
placeholder="收件人邮箱"
|
@click="selectRecipientType(option.value)"
|
||||||
class="custom-field mt-10"
|
>
|
||||||
/>
|
<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-model="recipientEmail"
|
||||||
|
placeholder="收件人邮箱"
|
||||||
|
class="custom-field mt-10"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 发送时间选择 -->
|
<!-- 发送时间选择 -->
|
||||||
<div class="form-group mt-20">
|
<div class="form-section">
|
||||||
<h3 class="font-semibold">发送时间</h3>
|
<div class="section-header">
|
||||||
<van-radio-group v-model="timeType" direction="horizontal">
|
<van-icon name="clock-o" class="section-icon" />
|
||||||
<van-radio name="preset">预设时间</van-radio>
|
<span class="section-title">发送时间</span>
|
||||||
<van-radio name="custom">自定义</van-radio>
|
</div>
|
||||||
<van-radio name="condition">条件触发</van-radio>
|
<div class="time-selector">
|
||||||
</van-radio-group>
|
<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">
|
<div v-if="timeType === 'preset'" class="preset-options mt-10">
|
||||||
<van-button
|
<van-button
|
||||||
@@ -70,23 +95,30 @@
|
|||||||
</van-cell-group>
|
</van-cell-group>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 邮件内容 -->
|
<!-- 邮件内容 -->
|
||||||
<div class="form-group mt-20">
|
<div class="form-section">
|
||||||
<h3 class="font-semibold">邮件内容</h3>
|
<div class="section-header">
|
||||||
<van-field
|
<van-icon name="envelop-o" class="section-icon" />
|
||||||
v-model="subject"
|
<span class="section-title">邮件内容</span>
|
||||||
placeholder="标题"
|
</div>
|
||||||
class="custom-field"
|
<div class="form-field">
|
||||||
/>
|
<van-field
|
||||||
<van-field
|
v-model="subject"
|
||||||
v-model="content"
|
placeholder="标题"
|
||||||
type="textarea"
|
class="custom-field title-field"
|
||||||
placeholder="写下你想对未来的自己说的话..."
|
/>
|
||||||
rows="8"
|
</div>
|
||||||
autosize
|
<div class="form-field">
|
||||||
class="custom-field mt-10"
|
<van-field
|
||||||
/>
|
v-model="content"
|
||||||
|
type="textarea"
|
||||||
|
placeholder="写下你想对未来的自己说的话..."
|
||||||
|
rows="8"
|
||||||
|
autosize
|
||||||
|
class="custom-field content-field"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 附件和多媒体 -->
|
<!-- 附件和多媒体 -->
|
||||||
<div class="media-options mt-10">
|
<div class="media-options mt-10">
|
||||||
@@ -105,8 +137,11 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- AI助手 -->
|
<!-- AI助手 -->
|
||||||
<div class="form-group mt-20">
|
<div class="form-section">
|
||||||
<h3 class="font-semibold">AI写作助手</h3>
|
<div class="section-header">
|
||||||
|
<van-icon name="bulb-o" class="section-icon" />
|
||||||
|
<span class="section-title">AI写作助手</span>
|
||||||
|
</div>
|
||||||
<van-cell-group>
|
<van-cell-group>
|
||||||
<van-cell title="生成开头" is-link @click="generateOpening" />
|
<van-cell title="生成开头" is-link @click="generateOpening" />
|
||||||
<van-cell title="内容建议" is-link @click="generateSuggestions" />
|
<van-cell title="内容建议" is-link @click="generateSuggestions" />
|
||||||
@@ -176,6 +211,35 @@ export default {
|
|||||||
const isEncrypted = ref(false) // 是否加密
|
const isEncrypted = ref(false) // 是否加密
|
||||||
const capsuleStyle = ref('default') // 胶囊样式
|
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 showLocationPicker = ref(false)
|
||||||
const showEventPicker = ref(false)
|
const showEventPicker = ref(false)
|
||||||
@@ -296,6 +360,16 @@ export default {
|
|||||||
router.back()
|
router.back()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 选择收件人类型
|
||||||
|
const selectRecipientType = (type) => {
|
||||||
|
recipientType.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
// 选择触发类型
|
||||||
|
const selectTriggerType = (type) => {
|
||||||
|
timeType.value = type
|
||||||
|
}
|
||||||
|
|
||||||
// 选择预设时间
|
// 选择预设时间
|
||||||
const selectPresetTime = (value) => {
|
const selectPresetTime = (value) => {
|
||||||
selectedPresetTime.value = value
|
selectedPresetTime.value = value
|
||||||
@@ -619,31 +693,35 @@ export default {
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stars,
|
stars,
|
||||||
recipientType,
|
recipientType,
|
||||||
recipientEmail,
|
recipientEmail,
|
||||||
timeType,
|
timeType,
|
||||||
selectedPresetTime,
|
selectedPresetTime,
|
||||||
customDeliveryDate,
|
customDeliveryDate,
|
||||||
minDate,
|
minDate,
|
||||||
presetTimeOptions,
|
presetTimeOptions,
|
||||||
subject,
|
subject,
|
||||||
content,
|
content,
|
||||||
showLocationPicker,
|
showLocationPicker,
|
||||||
showEventPicker,
|
showEventPicker,
|
||||||
triggerEvents,
|
triggerEvents,
|
||||||
areaList,
|
areaList,
|
||||||
goBack,
|
recipientOptions,
|
||||||
selectPresetTime,
|
timeOptions,
|
||||||
onLocationConfirm,
|
goBack,
|
||||||
selectEvent,
|
selectRecipientType,
|
||||||
afterRead,
|
selectTriggerType,
|
||||||
generateOpening,
|
selectPresetTime,
|
||||||
generateSuggestions,
|
onLocationConfirm,
|
||||||
analyzeEmotion,
|
selectEvent,
|
||||||
saveDraft,
|
afterRead,
|
||||||
sendMail
|
generateOpening,
|
||||||
}
|
generateSuggestions,
|
||||||
|
analyzeEmotion,
|
||||||
|
saveDraft,
|
||||||
|
sendMail
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -672,55 +750,212 @@ export default {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 撰写内容区域 */
|
||||||
.compose-content {
|
.compose-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 0 15px;
|
padding: 0 0 80px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表单容器 */
|
||||||
|
.form-container {
|
||||||
|
padding: 16px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单区块 */
|
||||||
.form-section {
|
.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 {
|
.form-section:hover {
|
||||||
margin: 0 0 10px;
|
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-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 {
|
.custom-field {
|
||||||
background-color: rgba(255, 255, 255, 0.05);
|
background: rgba(255, 255, 255, 0.05);
|
||||||
border-radius: 8px;
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-field :deep(.van-field__control) {
|
.custom-field :deep(.van-field__control) {
|
||||||
color: var(--text-primary);
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 单选按钮选中样式 */
|
.custom-field:focus-within {
|
||||||
:deep(.van-radio-group) {
|
background: rgba(255, 255, 255, 0.08);
|
||||||
margin-bottom: 10px;
|
border-color: #00D4FF;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 212, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.van-radio) {
|
.title-field {
|
||||||
margin-right: 15px;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.van-radio__icon) {
|
.content-field {
|
||||||
font-size: 18px;
|
min-height: 120px;
|
||||||
}
|
|
||||||
|
|
||||||
: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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 预设选项 */
|
||||||
.preset-options {
|
.preset-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -737,32 +972,32 @@ export default {
|
|||||||
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.4);
|
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 {
|
.media-options {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-uploader {
|
.media-uploader {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 底部操作 */
|
||||||
.footer-actions {
|
.footer-actions {
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.save-button {
|
.save-button {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-button {
|
.send-button {
|
||||||
@@ -771,14 +1006,7 @@ export default {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preset-button {
|
/* 弹窗样式 */
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-uploader .van-button {
|
|
||||||
/* 按钮样式已由全局样式处理 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-picker {
|
.event-picker {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -788,5 +1016,46 @@ export default {
|
|||||||
.event-picker h3 {
|
.event-picker h3 {
|
||||||
margin: 0 0 15px;
|
margin: 0 0 15px;
|
||||||
font-size: 18px;
|
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>
|
</style>
|
||||||
@@ -18,20 +18,36 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 时光胶囊视图 -->
|
<!-- 时光胶囊视图 -->
|
||||||
<div class="capsules-container">
|
<div class="capsules-container mobile-scroll">
|
||||||
<div class="capsules-space" ref="capsulesSpace">
|
<div class="capsules-space" ref="capsulesSpace">
|
||||||
<!-- 时间胶囊 -->
|
<!-- 3D透视层 -->
|
||||||
<div
|
<div class="perspective-container">
|
||||||
v-for="capsule in capsules"
|
<!-- 时间胶囊 -->
|
||||||
:key="capsule.id"
|
<div
|
||||||
class="capsule-wrapper"
|
v-for="(capsule, index) in capsules"
|
||||||
:style="getCapsuleStyle(capsule)"
|
:key="capsule.id"
|
||||||
@click="openCapsule(capsule)"
|
class="capsule-wrapper mobile-card touch-feedback"
|
||||||
>
|
:class="{'capsule-active': activeCapsule === capsule.id}"
|
||||||
<div class="time-capsule" :class="{'glowing': capsule.isGlowing}">
|
:style="getCapsuleStyle(capsule, index)"
|
||||||
<div class="capsule-info">
|
@click="openCapsule(capsule)"
|
||||||
<p class="capsule-title font-semibold">{{ capsule.title }}</p>
|
@mouseenter="setActiveCapsule(capsule.id)"
|
||||||
<p class="capsule-date text-xs text-secondary">{{ formatDate(capsule.deliveryDate) }}</p>
|
@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, '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>
|
||||||
</div>
|
</div>
|
||||||
@@ -107,6 +123,7 @@ export default {
|
|||||||
const showSearch = ref(false)
|
const showSearch = ref(false)
|
||||||
const showNotifications = ref(false)
|
const showNotifications = ref(false)
|
||||||
const searchValue = ref('')
|
const searchValue = ref('')
|
||||||
|
const activeCapsule = ref(null) // 当前激活的胶囊
|
||||||
|
|
||||||
// 使用直接导入的状态和操作
|
// 使用直接导入的状态和操作
|
||||||
const userName = computed(() => userState.userInfo.username || '时光旅人')
|
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 {
|
return {
|
||||||
left: `${capsule.position.x}%`,
|
left: `${capsule.position.x}%`,
|
||||||
top: `${capsule.position.y}%`,
|
top: `${capsule.position.y}%`,
|
||||||
transform: `scale(${0.5 + capsule.position.z})`,
|
transform: `scale(${0.5 + capsule.position.z})`,
|
||||||
opacity: 0.5 + capsule.position.z * 0.5,
|
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 formatDate = (date) => {
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -272,7 +330,14 @@ export default {
|
|||||||
showNotifications,
|
showNotifications,
|
||||||
searchValue,
|
searchValue,
|
||||||
notifications,
|
notifications,
|
||||||
|
activeCapsule,
|
||||||
getCapsuleStyle,
|
getCapsuleStyle,
|
||||||
|
setActiveCapsule,
|
||||||
|
handleSwipeLeft,
|
||||||
|
handleSwipeRight,
|
||||||
|
handleSwipeUp,
|
||||||
|
handleSwipeDown,
|
||||||
|
handleLongPress,
|
||||||
formatDate,
|
formatDate,
|
||||||
formatTime,
|
formatTime,
|
||||||
openCapsule,
|
openCapsule,
|
||||||
@@ -313,22 +378,72 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
perspective: 1200px;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.capsules-space {
|
.capsules-space {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 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 {
|
.capsule-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.3s ease;
|
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 {
|
.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 {
|
.time-capsule {
|
||||||
@@ -340,6 +455,8 @@ export default {
|
|||||||
box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
|
box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
animation: float 6s ease-in-out infinite;
|
animation: float 6s ease-in-out infinite;
|
||||||
|
transform-style: preserve-3d;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-capsule::before {
|
.time-capsule::before {
|
||||||
@@ -355,6 +472,27 @@ export default {
|
|||||||
opacity: 0.7;
|
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 {
|
.time-capsule.glowing::before {
|
||||||
animation: pulse 2s infinite alternate;
|
animation: pulse 2s infinite alternate;
|
||||||
}
|
}
|
||||||
@@ -413,6 +551,15 @@ export default {
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 14px;
|
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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user