提交
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 {
|
||||
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;
|
||||
|
||||
@@ -15,10 +15,14 @@ import './assets/styles/global.css'
|
||||
// 引入状态管理
|
||||
import { userActions } from './store'
|
||||
|
||||
// 引入移动端触摸交互插件
|
||||
import touchPlugin from './plugins/touch'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(router)
|
||||
app.use(Vant)
|
||||
app.use(touchPlugin)
|
||||
|
||||
// 初始化用户状态
|
||||
userActions.initUserState()
|
||||
|
||||
16
src/plugins/touch.js
Normal file
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="form-section glass-card p-20">
|
||||
<div class="form-container">
|
||||
<!-- 收件人选择 -->
|
||||
<div class="form-group">
|
||||
<h3 class="font-semibold">收件人</h3>
|
||||
<van-radio-group v-model="recipientType" direction="horizontal">
|
||||
<van-radio name="SELF">自己</van-radio>
|
||||
<van-radio name="SPECIFIC">他人</van-radio>
|
||||
<van-radio name="PUBLIC">任意有缘人</van-radio>
|
||||
</van-radio-group>
|
||||
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<van-icon name="friends-o" class="section-icon" />
|
||||
<span class="section-title">收件人</span>
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<div
|
||||
v-for="option in recipientOptions"
|
||||
:key="option.value"
|
||||
class="radio-option"
|
||||
:class="{'radio-selected': recipientType === option.value}"
|
||||
@click="selectRecipientType(option.value)"
|
||||
>
|
||||
<div class="radio-indicator"></div>
|
||||
<span class="radio-label">{{ option.label }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="recipientType === 'SPECIFIC'" class="form-field">
|
||||
<van-field
|
||||
v-if="recipientType === 'SPECIFIC'"
|
||||
v-model="recipientEmail"
|
||||
placeholder="收件人邮箱"
|
||||
class="custom-field mt-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发送时间选择 -->
|
||||
<div class="form-group mt-20">
|
||||
<h3 class="font-semibold">发送时间</h3>
|
||||
<van-radio-group v-model="timeType" direction="horizontal">
|
||||
<van-radio name="preset">预设时间</van-radio>
|
||||
<van-radio name="custom">自定义</van-radio>
|
||||
<van-radio name="condition">条件触发</van-radio>
|
||||
</van-radio-group>
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<van-icon name="clock-o" class="section-icon" />
|
||||
<span class="section-title">发送时间</span>
|
||||
</div>
|
||||
<div class="time-selector">
|
||||
<div
|
||||
v-for="option in timeOptions"
|
||||
:key="option.value"
|
||||
class="time-option"
|
||||
:class="{'time-selected': timeType === option.value}"
|
||||
@click="selectTriggerType(option.value)"
|
||||
>
|
||||
<div class="time-icon">
|
||||
<van-icon :name="option.icon" />
|
||||
</div>
|
||||
<div class="time-content">
|
||||
<div class="time-label">{{ option.label }}</div>
|
||||
<div class="time-desc">{{ option.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="timeType === 'preset'" class="preset-options mt-10">
|
||||
<van-button
|
||||
@@ -72,21 +97,28 @@
|
||||
</div>
|
||||
|
||||
<!-- 邮件内容 -->
|
||||
<div class="form-group mt-20">
|
||||
<h3 class="font-semibold">邮件内容</h3>
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<van-icon name="envelop-o" class="section-icon" />
|
||||
<span class="section-title">邮件内容</span>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<van-field
|
||||
v-model="subject"
|
||||
placeholder="标题"
|
||||
class="custom-field"
|
||||
class="custom-field title-field"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-field">
|
||||
<van-field
|
||||
v-model="content"
|
||||
type="textarea"
|
||||
placeholder="写下你想对未来的自己说的话..."
|
||||
rows="8"
|
||||
autosize
|
||||
class="custom-field mt-10"
|
||||
class="custom-field content-field"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 附件和多媒体 -->
|
||||
<div class="media-options mt-10">
|
||||
@@ -105,8 +137,11 @@
|
||||
</div>
|
||||
|
||||
<!-- AI助手 -->
|
||||
<div class="form-group mt-20">
|
||||
<h3 class="font-semibold">AI写作助手</h3>
|
||||
<div class="form-section">
|
||||
<div class="section-header">
|
||||
<van-icon name="bulb-o" class="section-icon" />
|
||||
<span class="section-title">AI写作助手</span>
|
||||
</div>
|
||||
<van-cell-group>
|
||||
<van-cell title="生成开头" is-link @click="generateOpening" />
|
||||
<van-cell title="内容建议" is-link @click="generateSuggestions" />
|
||||
@@ -176,6 +211,35 @@ export default {
|
||||
const isEncrypted = ref(false) // 是否加密
|
||||
const capsuleStyle = ref('default') // 胶囊样式
|
||||
|
||||
// 收件人选项
|
||||
const recipientOptions = [
|
||||
{ value: 'SELF', label: '自己' },
|
||||
{ value: 'SPECIFIC', label: '指定收件人' },
|
||||
{ value: 'PUBLIC', label: '公开信' }
|
||||
]
|
||||
|
||||
// 时间选项
|
||||
const timeOptions = [
|
||||
{
|
||||
value: 'preset',
|
||||
label: '预设时间',
|
||||
description: '选择一个预设的未来时间点',
|
||||
icon: 'clock-o'
|
||||
},
|
||||
{
|
||||
value: 'custom',
|
||||
label: '自定义',
|
||||
description: '自定义精确的发送时间',
|
||||
icon: 'edit'
|
||||
},
|
||||
{
|
||||
value: 'condition',
|
||||
label: '条件触发',
|
||||
description: '根据特定条件自动发送',
|
||||
icon: 'fire-o'
|
||||
}
|
||||
]
|
||||
|
||||
// 弹窗控制
|
||||
const showLocationPicker = ref(false)
|
||||
const showEventPicker = ref(false)
|
||||
@@ -296,6 +360,16 @@ export default {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 选择收件人类型
|
||||
const selectRecipientType = (type) => {
|
||||
recipientType.value = type
|
||||
}
|
||||
|
||||
// 选择触发类型
|
||||
const selectTriggerType = (type) => {
|
||||
timeType.value = type
|
||||
}
|
||||
|
||||
// 选择预设时间
|
||||
const selectPresetTime = (value) => {
|
||||
selectedPresetTime.value = value
|
||||
@@ -633,7 +707,11 @@ export default {
|
||||
showEventPicker,
|
||||
triggerEvents,
|
||||
areaList,
|
||||
recipientOptions,
|
||||
timeOptions,
|
||||
goBack,
|
||||
selectRecipientType,
|
||||
selectTriggerType,
|
||||
selectPresetTime,
|
||||
onLocationConfirm,
|
||||
selectEvent,
|
||||
@@ -672,55 +750,212 @@ export default {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 撰写内容区域 */
|
||||
.compose-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 15px;
|
||||
padding: 0 0 80px 0;
|
||||
}
|
||||
|
||||
/* 表单容器 */
|
||||
.form-container {
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* 表单区块 */
|
||||
.form-section {
|
||||
margin-bottom: 20px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
backdrop-filter: blur(10px);
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.form-group h3 {
|
||||
margin: 0 0 10px;
|
||||
.form-section:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
/* 区块标题 */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 20px;
|
||||
margin-right: 10px;
|
||||
color: #00D4FF;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 单选组 */
|
||||
.radio-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.radio-option:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.radio-option.radio-selected {
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-color: #00D4FF;
|
||||
box-shadow: 0 0 15px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.radio-indicator {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(255, 255, 255, 0.5);
|
||||
margin-right: 10px;
|
||||
position: relative;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.radio-option.radio-selected .radio-indicator {
|
||||
border-color: #00D4FF;
|
||||
background: #00D4FF;
|
||||
}
|
||||
|
||||
.radio-option.radio-selected .radio-indicator::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.radio-label {
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 时间选择器 */
|
||||
.time-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.time-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.time-option:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
.time-option.time-selected {
|
||||
background: rgba(0, 212, 255, 0.2);
|
||||
border-color: #00D4FF;
|
||||
box-shadow: 0 0 15px rgba(0, 212, 255, 0.3);
|
||||
}
|
||||
|
||||
.time-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 10px;
|
||||
margin-right: 16px;
|
||||
font-size: 20px;
|
||||
color: #00D4FF;
|
||||
}
|
||||
|
||||
.time-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.time-label {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.time-desc {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 表单字段 */
|
||||
.form-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.custom-field {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.custom-field :deep(.van-field__control) {
|
||||
color: var(--text-primary);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 单选按钮选中样式 */
|
||||
:deep(.van-radio-group) {
|
||||
margin-bottom: 10px;
|
||||
.custom-field:focus-within {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border-color: #00D4FF;
|
||||
box-shadow: 0 0 10px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
|
||||
:deep(.van-radio) {
|
||||
margin-right: 15px;
|
||||
.title-field {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.van-radio__icon) {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
:deep(.van-radio__icon--checked .van-icon) {
|
||||
background-color: #4285f4;
|
||||
border-color: #4285f4;
|
||||
box-shadow: 0 0 8px rgba(66, 133, 244, 0.5);
|
||||
}
|
||||
|
||||
:deep(.van-radio__label) {
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
.content-field {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
/* 预设选项 */
|
||||
.preset-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -737,32 +972,32 @@ export default {
|
||||
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.4);
|
||||
}
|
||||
|
||||
.custom-date-picker {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
/* 媒体选项 */
|
||||
.media-options {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.media-uploader {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 底部操作 */
|
||||
.footer-actions {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.save-button {
|
||||
height: 50px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.send-button {
|
||||
@@ -771,14 +1006,7 @@ export default {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.preset-button {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.media-uploader .van-button {
|
||||
/* 按钮样式已由全局样式处理 */
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.event-picker {
|
||||
padding: 20px;
|
||||
height: 100%;
|
||||
@@ -788,5 +1016,46 @@ export default {
|
||||
.event-picker h3 {
|
||||
margin: 0 0 15px;
|
||||
font-size: 18px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.form-container {
|
||||
padding: 12px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.radio-option {
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.time-option {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.time-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 18px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.media-options {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -18,21 +18,37 @@
|
||||
</div>
|
||||
|
||||
<!-- 时光胶囊视图 -->
|
||||
<div class="capsules-container">
|
||||
<div class="capsules-container mobile-scroll">
|
||||
<div class="capsules-space" ref="capsulesSpace">
|
||||
<!-- 3D透视层 -->
|
||||
<div class="perspective-container">
|
||||
<!-- 时间胶囊 -->
|
||||
<div
|
||||
v-for="capsule in capsules"
|
||||
v-for="(capsule, index) in capsules"
|
||||
:key="capsule.id"
|
||||
class="capsule-wrapper"
|
||||
:style="getCapsuleStyle(capsule)"
|
||||
class="capsule-wrapper mobile-card touch-feedback"
|
||||
:class="{'capsule-active': activeCapsule === capsule.id}"
|
||||
:style="getCapsuleStyle(capsule, index)"
|
||||
@click="openCapsule(capsule)"
|
||||
@mouseenter="setActiveCapsule(capsule.id)"
|
||||
@mouseleave="setActiveCapsule(null)"
|
||||
v-touch-feedback="() => setActiveCapsule(capsule.id)"
|
||||
v-swipe="{
|
||||
onSwipeLeft: () => handleSwipeLeft(capsule),
|
||||
onSwipeRight: () => handleSwipeRight(capsule),
|
||||
onSwipeUp: () => handleSwipeUp(capsule),
|
||||
onSwipeDown: () => handleSwipeDown(capsule)
|
||||
}"
|
||||
v-long-press="() => handleLongPress(capsule)"
|
||||
>
|
||||
<div class="time-capsule" :class="{'glowing': capsule.isGlowing}">
|
||||
<div class="time-capsule" :class="{'glowing': capsule.isGlowing, 'active': activeCapsule === capsule.id}">
|
||||
<div class="capsule-info">
|
||||
<p class="capsule-title font-semibold">{{ capsule.title }}</p>
|
||||
<p class="capsule-date text-xs text-secondary">{{ formatDate(capsule.deliveryDate) }}</p>
|
||||
</div>
|
||||
<!-- 胶囊光环效果 -->
|
||||
<div class="capsule-ring" v-if="activeCapsule === capsule.id"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,6 +123,7 @@ export default {
|
||||
const showSearch = ref(false)
|
||||
const showNotifications = ref(false)
|
||||
const searchValue = ref('')
|
||||
const activeCapsule = ref(null) // 当前激活的胶囊
|
||||
|
||||
// 使用直接导入的状态和操作
|
||||
const userName = computed(() => userState.userInfo.username || '时光旅人')
|
||||
@@ -154,16 +171,57 @@ export default {
|
||||
}
|
||||
|
||||
// 获取胶囊样式
|
||||
const getCapsuleStyle = (capsule) => {
|
||||
const getCapsuleStyle = (capsule, index) => {
|
||||
// 添加基于索引的延迟动画
|
||||
const delay = index * 0.1
|
||||
|
||||
return {
|
||||
left: `${capsule.position.x}%`,
|
||||
top: `${capsule.position.y}%`,
|
||||
transform: `scale(${0.5 + capsule.position.z})`,
|
||||
opacity: 0.5 + capsule.position.z * 0.5,
|
||||
zIndex: Math.floor(capsule.position.z * 10)
|
||||
zIndex: Math.floor(capsule.position.z * 10),
|
||||
animationDelay: `${delay}s`,
|
||||
'--glow-color': capsule.isGlowing ? '#00D4FF' : '#6A5ACD'
|
||||
}
|
||||
}
|
||||
|
||||
// 设置激活的胶囊
|
||||
const setActiveCapsule = (capsuleId) => {
|
||||
activeCapsule.value = capsuleId
|
||||
}
|
||||
|
||||
// 处理滑动左
|
||||
const handleSwipeLeft = (capsule) => {
|
||||
console.log('向左滑动胶囊:', capsule.title)
|
||||
// 可以添加删除或归档功能
|
||||
}
|
||||
|
||||
// 处理滑动右
|
||||
const handleSwipeRight = (capsule) => {
|
||||
console.log('向右滑动胶囊:', capsule.title)
|
||||
// 可以添加编辑功能
|
||||
}
|
||||
|
||||
// 处理滑动上
|
||||
const handleSwipeUp = (capsule) => {
|
||||
console.log('向上滑动胶囊:', capsule.title)
|
||||
// 可以添加置顶功能
|
||||
}
|
||||
|
||||
// 处理滑动下
|
||||
const handleSwipeDown = (capsule) => {
|
||||
console.log('向下滑动胶囊:', capsule.title)
|
||||
// 可以添加分享功能
|
||||
}
|
||||
|
||||
// 处理长按
|
||||
const handleLongPress = (capsule) => {
|
||||
console.log('长按胶囊:', capsule.title)
|
||||
// 可以添加更多操作选项
|
||||
// 例如显示操作菜单
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (date) => {
|
||||
const now = new Date()
|
||||
@@ -272,7 +330,14 @@ export default {
|
||||
showNotifications,
|
||||
searchValue,
|
||||
notifications,
|
||||
activeCapsule,
|
||||
getCapsuleStyle,
|
||||
setActiveCapsule,
|
||||
handleSwipeLeft,
|
||||
handleSwipeRight,
|
||||
handleSwipeUp,
|
||||
handleSwipeDown,
|
||||
handleLongPress,
|
||||
formatDate,
|
||||
formatTime,
|
||||
openCapsule,
|
||||
@@ -313,22 +378,72 @@ export default {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
perspective: 1200px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.capsules-space {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.5s ease;
|
||||
}
|
||||
|
||||
/* 3D透视容器 */
|
||||
.perspective-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform-style: preserve-3d;
|
||||
animation: float 20s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% { transform: rotateX(0deg) rotateY(0deg); }
|
||||
25% { transform: rotateX(2deg) rotateY(5deg); }
|
||||
50% { transform: rotateX(-2deg) rotateY(-5deg); }
|
||||
75% { transform: rotateX(1deg) rotateY(3deg); }
|
||||
}
|
||||
|
||||
.capsule-wrapper {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
transform-style: preserve-3d;
|
||||
animation: fadeInScale 0.8s ease forwards;
|
||||
opacity: 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
margin: 8px 0;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.capsule-wrapper:active {
|
||||
transform: scale(0.98) translateZ(0);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
@keyframes fadeInScale {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.5) translateZ(-50px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
.capsule-wrapper:hover {
|
||||
transform: scale(1.1);
|
||||
transform: scale(1.1) translateZ(30px);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.capsule-wrapper.capsule-active {
|
||||
transform: scale(1.15) translateZ(50px);
|
||||
z-index: 101;
|
||||
}
|
||||
|
||||
.time-capsule {
|
||||
@@ -340,6 +455,8 @@ export default {
|
||||
box-shadow: 0 8px 20px rgba(0, 212, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
animation: float 6s ease-in-out infinite;
|
||||
transform-style: preserve-3d;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.time-capsule::before {
|
||||
@@ -355,6 +472,27 @@ export default {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.time-capsule.active::before {
|
||||
opacity: 0.7;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.capsule-ring {
|
||||
position: absolute;
|
||||
top: -15px;
|
||||
left: -15px;
|
||||
right: -15px;
|
||||
bottom: -15px;
|
||||
border: 3px solid rgba(255, 255, 255, 0.6);
|
||||
border-radius: 50%;
|
||||
animation: rotate 10s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.time-capsule.glowing::before {
|
||||
animation: pulse 2s infinite alternate;
|
||||
}
|
||||
@@ -413,6 +551,15 @@ export default {
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.fab-button:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.search-popup {
|
||||
|
||||
Reference in New Issue
Block a user