feat: 优化人生轨迹详情页布局与顶部安全区
- 合并事件标题卡与描述卡为统一的 event-content-card - 移除顶部悬浮导航,避免与小程序原生胶囊按钮重叠 - 添加顶部安全区占位(参考 profile/index.vue 等页面) - 调整按钮布局:收藏、分析、分享、编辑、收起放入内容区域 - 底部只保留居中的'聊聊'主按钮 - 使用 Markdown 组件渲染事件描述 - 添加 scrollToAnalysis 滚动定位功能 - 更新 AGENTS.md 添加操作系统命令执行规则 - 更新 CLAUDE.md 添加操作系统命令执行规则
This commit is contained in:
@@ -0,0 +1,379 @@
|
|||||||
|
# AGENTS.md
|
||||||
|
|
||||||
|
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
情绪博物馆 (Emotion Museum) 是一款基于 AI 技术的心理健康应用,通过智能对话、情绪分析、个性化成长方案等功能,帮助用户建立健康的情绪管理习惯。
|
||||||
|
|
||||||
|
**生产环境地址**: `lifescript.happylifeos.com`
|
||||||
|
- 用户前端:https://lifescript.happylifeos.com/
|
||||||
|
- 管理后台:https://lifescript.happylifeos.com/emotion-museum-admin/
|
||||||
|
- 后端 API:https://lifescript.happylifeos.com/api
|
||||||
|
- WebSocket:wss://lifescript.happylifeos.com/ws
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── backend-single/ # Spring Boot 单体后端服务
|
||||||
|
├── web/ # 用户前端 (Vue3 + TS + Vite)
|
||||||
|
├── web-admin/ # 管理后台 (Vue3 + TS + Element Plus)
|
||||||
|
├── UniApp/ # 跨平台移动应用 (微信小程序/H5)
|
||||||
|
├── mini-program/ # 小程序项目
|
||||||
|
├── course-web/ # 课程 Web 项目
|
||||||
|
├── life-script/ # 生活脚本工具
|
||||||
|
└── tools/ # 工具脚本
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
### 后端 (backend-single)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入后端目录
|
||||||
|
cd backend-single
|
||||||
|
|
||||||
|
# 编译打包
|
||||||
|
mvn clean package -DskipTests
|
||||||
|
|
||||||
|
# 本地运行
|
||||||
|
mvn spring-boot:run
|
||||||
|
|
||||||
|
# 或直接运行 JAR
|
||||||
|
java -jar target/backend-single-1.0.0.jar
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
mvn test
|
||||||
|
|
||||||
|
# 运行单个测试类
|
||||||
|
mvn test -Dtest=ClassNameTest
|
||||||
|
```
|
||||||
|
|
||||||
|
### 用户前端 (web)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入前端目录
|
||||||
|
cd web
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器 (端口 5173)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
npm run type-check
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
npm run test
|
||||||
|
|
||||||
|
# E2E 测试
|
||||||
|
npm run test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
### 管理后台 (web-admin)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入目录
|
||||||
|
cd web-admin
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 启动开发服务器 (端口 5174)
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# 类型检查
|
||||||
|
npm run type-check
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# 构建生产版本
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### UniApp/小程序
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入目录
|
||||||
|
cd UniApp
|
||||||
|
|
||||||
|
# 开发微信小程序
|
||||||
|
npm run dev:mp-weixin
|
||||||
|
|
||||||
|
# 构建微信小程序
|
||||||
|
npm run build:mp-weixin
|
||||||
|
|
||||||
|
# 开发 H5
|
||||||
|
npm run dev:h5
|
||||||
|
|
||||||
|
# 构建 H5
|
||||||
|
npm run build:h5
|
||||||
|
```
|
||||||
|
|
||||||
|
### mini-program(推荐开发模式)
|
||||||
|
|
||||||
|
**H5 开发模式**(日常开发,推荐):
|
||||||
|
```bash
|
||||||
|
cd mini-program
|
||||||
|
# 或使用启动脚本
|
||||||
|
start-h5-dev.bat
|
||||||
|
```
|
||||||
|
访问 `http://localhost:5173`,登录态保留,支持热更新。
|
||||||
|
|
||||||
|
**微信小程序开发模式**(仅用于小程序特性验证):
|
||||||
|
```bash
|
||||||
|
cd mini-program
|
||||||
|
npm run dev:mp-weixin
|
||||||
|
```
|
||||||
|
在微信开发者工具中打开 `unpackage/dist/dev/mp-weixin`。
|
||||||
|
|
||||||
|
**开发工作流说明**:详见 [小程序开发工作流程设计](docs/superpowers/specs/2026-04-07-mini-program-dev-workflow-design.md)
|
||||||
|
|
||||||
|
### 一键部署
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 部署所有服务(后端 + 前端 + 管理后台)到生产服务器
|
||||||
|
bash deploy-all.sh
|
||||||
|
|
||||||
|
# 仅部署后端
|
||||||
|
bash deploy-all.sh backend
|
||||||
|
|
||||||
|
# 仅部署前端
|
||||||
|
bash deploy-all.sh frontend
|
||||||
|
|
||||||
|
# 仅部署管理后台
|
||||||
|
bash deploy-all.sh admin
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
- **框架**: Spring Boot 2.7.18
|
||||||
|
- **ORM**: MyBatis-Plus 3.5.3.1
|
||||||
|
- **数据库**: MySQL 8.0.33
|
||||||
|
- **缓存**: Redis 7.0+
|
||||||
|
- **JWT**: io.jsonwebtoken 0.11.5
|
||||||
|
- **构建**: Maven 3.6+
|
||||||
|
- **JDK**: 17
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
- **框架**: Vue 3.4.x + TypeScript 5.x
|
||||||
|
- **构建**: Vite 5.x
|
||||||
|
- **UI**: Element Plus 2.4.x
|
||||||
|
- **样式**: Tailwind CSS 3.4.x
|
||||||
|
- **状态管理**: Pinia 2.1.x
|
||||||
|
- **路由**: Vue Router 4.2.x
|
||||||
|
- **HTTP**: Axios 1.6.x
|
||||||
|
- **实时通信**: @stomp/stompjs + SockJS
|
||||||
|
- **图表**: ECharts 5.4.x
|
||||||
|
- **包管理**: npm 9+
|
||||||
|
|
||||||
|
### 移动端
|
||||||
|
- **框架**: UniApp (Vue 3)
|
||||||
|
- **平台**: 微信小程序、H5
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 架构规范
|
||||||
|
|
||||||
|
### 后端分层架构
|
||||||
|
- **Controller 层**: 接口定义,参数校验,禁止业务逻辑
|
||||||
|
- **Service 层**: 业务逻辑实现
|
||||||
|
- **Mapper 层**: 数据访问层
|
||||||
|
- **Entity 层**: 数据实体
|
||||||
|
- **Request/Response 层**: 入参出参封装
|
||||||
|
- **Config 层**: 配置类
|
||||||
|
|
||||||
|
### 接口设计规范
|
||||||
|
- Controller 层路由禁止添加 `/api` 前缀
|
||||||
|
- 使用 `@RequestParam` 传递路径参数,禁止使用 `@PathVariable`
|
||||||
|
- 接口方法参数不超过两个,超出时使用 Request/DTO 对象封装
|
||||||
|
- 使用项目已有的 `Result` 作为统一接口返回
|
||||||
|
- 禁止使用枚举类型作为接口入参
|
||||||
|
|
||||||
|
### 前端架构
|
||||||
|
- 使用 Vue 3 Composition API (`<script setup>`)
|
||||||
|
- 使用 TypeScript 进行类型检查
|
||||||
|
- 组件通信优先使用 Pinia 状态管理
|
||||||
|
- 前端访问后端通过网关统一调用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 验证规则(强制)
|
||||||
|
|
||||||
|
**修改任何前后端代码后,必须使用浏览器进行验证**
|
||||||
|
|
||||||
|
### 验证步骤
|
||||||
|
|
||||||
|
1. **使用浏览器工具打开对应页面**
|
||||||
|
- 使用 `agent-browser` skill 或 MCP 工具
|
||||||
|
- 前台页面:通常是 `http://localhost:5173` 或项目指定端口
|
||||||
|
- 管理端页面:通常是 `http://localhost:5174` 或项目指定路径
|
||||||
|
|
||||||
|
2. **验证标准**(全部满足才算通过)
|
||||||
|
- ✅ 前端修改已生效
|
||||||
|
- ✅ 页面没有报错
|
||||||
|
- ✅ 可以正常访问
|
||||||
|
- ✅ 浏览器 Console 中没有任何错误
|
||||||
|
|
||||||
|
3. **只有验证通过才算任务完成**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 热加载规则(强制)
|
||||||
|
|
||||||
|
**修改前后端代码后,优先利用热加载,避免不必要的重启**
|
||||||
|
|
||||||
|
### 判断逻辑
|
||||||
|
|
||||||
|
1. **不需要重启的场景**(热加载自动生效)
|
||||||
|
- 前端:修改 `.vue`、`.tsx`、`.ts`、`.js`、`.css`、`.scss` 等源码文件
|
||||||
|
- 后端:修改业务逻辑、API 接口、组件代码等支持热更新的文件
|
||||||
|
- 样式、模板、静态资源等修改
|
||||||
|
|
||||||
|
2. **需要重启的场景**(热加载无法生效)
|
||||||
|
- 前端:修改 `vite.config.ts`、`tsconfig.json`、`.env`、`package.json`、别名配置等
|
||||||
|
- 后端:修改启动配置、数据库连接、中间件注册、环境变量等
|
||||||
|
- 新增或删除依赖包后
|
||||||
|
|
||||||
|
### 操作原则
|
||||||
|
|
||||||
|
- 修改后先观察终端输出,确认热加载是否成功
|
||||||
|
- 只有修改了必须重启才能生效的配置时,才执行重启操作
|
||||||
|
- 重启前后端服务前,需告知用户
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 语言规则(强制)
|
||||||
|
|
||||||
|
- 所有对话和文档使用**中文**
|
||||||
|
- 代码内容(变量名、函数名、注释)使用英文
|
||||||
|
- 技术术语保持英文(API、HTTP、JSON 等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Git 提交规范
|
||||||
|
|
||||||
|
- 使用中文提交
|
||||||
|
- 格式:`类型:描述`
|
||||||
|
- 类型包括:feat, fix, docs, style, refactor, test, chore
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 开发规范(来自 Cursor Rules)
|
||||||
|
|
||||||
|
### 基础设置
|
||||||
|
- 保持对话语言为中文
|
||||||
|
- 不允许在未经允许的情况下删除代码和文件
|
||||||
|
- 不允许破坏正常的业务代码
|
||||||
|
- 执行终端命令时要关注执行情况,避免无效等待
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
- 生成代码时必须添加类级和函数级注释
|
||||||
|
- 使用 `import` 导包,禁止使用全限定名称引用类
|
||||||
|
- 禁止使用枚举类型作为 Entity、Request、Response、DTO 对象的字段
|
||||||
|
- 禁止以枚举类作为方法的入参
|
||||||
|
- 新增数据的 id 使用已存在的雪花算法生成器生成
|
||||||
|
|
||||||
|
### 架构规范
|
||||||
|
- Controller 层禁止添加业务逻辑
|
||||||
|
- 使用全局异常处理,禁止使用 try-catch
|
||||||
|
- 前端接口访问尽可能走网关调用
|
||||||
|
|
||||||
|
### 接口设计规范
|
||||||
|
- Controller 层接口定义:
|
||||||
|
- 入参使用 request 封装传递到 service 层
|
||||||
|
- service 层方法命名与 controller 层保持一致
|
||||||
|
- 出参使用 response 封装由 service 层传递到 controller 层
|
||||||
|
- 禁止在 controller 层做 entity 与 request/response 的转换
|
||||||
|
- 使用项目已有的 `Result` 做接口返回
|
||||||
|
- 接口和方法参数不允许超过两个,超过时使用 request 或 DTO 对象封装
|
||||||
|
- Controller 层路由禁止添加 `/api` 前缀
|
||||||
|
- Controller 层 `@RequestMapping` 的 value 属性值不允许重复且不允许为空
|
||||||
|
- 必须明确指定 value 属性值且使用驼峰结构命名,避免使用下划线
|
||||||
|
- 禁止使用 `@GetMapping()` 或 `@PostMapping` 等空注解形式
|
||||||
|
- 用户相关接口禁止直接传递用户 id,需要后端根据 token 获取当前登录用户信息
|
||||||
|
- 禁止使用 `/{param}` 格式的路径参数,避免网关路由冲突
|
||||||
|
- 路径参数统一使用 `@RequestParam` 而非 `@PathVariable`
|
||||||
|
- 接口路径命名应具有明确的语义,避免使用通用词汇如 `/get`、`/list` 等
|
||||||
|
- 批量操作接口应使用专门的 Request 对象封装参数,而非直接传递 List
|
||||||
|
- 接口路径应避免层级过深,建议不超过 3 级路径结构
|
||||||
|
|
||||||
|
### 数据库规范
|
||||||
|
- 所有数据表必须包含 `create_time` 和 `update_time` 字段
|
||||||
|
- 删除操作优先使用逻辑删除,添加 `deleted` 字段标识
|
||||||
|
- 数据库字段命名使用下划线分隔,Java 实体类使用驼峰命名
|
||||||
|
- 优先使用 `LambdaQueryWrapper` 构造条件查询
|
||||||
|
- 使用 Lambda 表达式引用实体类属性,提高代码可维护性
|
||||||
|
- 复杂查询条件应使用 `LambdaQueryWrapper` 的链式调用
|
||||||
|
|
||||||
|
### 安全规范
|
||||||
|
- 所有外部输入必须进行参数校验
|
||||||
|
- 敏感信息不得在日志中输出
|
||||||
|
- 数据库操作必须使用参数化查询,防止 SQL 注入
|
||||||
|
|
||||||
|
### 性能规范
|
||||||
|
- 避免 N+1 查询问题,合理使用批量查询
|
||||||
|
- 大数据量查询必须分页处理
|
||||||
|
- 缓存策略要考虑数据一致性问题
|
||||||
|
|
||||||
|
### 日志规范
|
||||||
|
- 关键业务操作必须记录操作日志
|
||||||
|
- 异常信息要包含足够的上下文信息
|
||||||
|
- 生产环境禁止输出 debug 级别日志
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作系统命令执行规则(强制)
|
||||||
|
|
||||||
|
**执行任何 Shell 命令前,必须根据当前操作系统(`win32` / `darwin` / `linux`)选择合适的命令语法。**
|
||||||
|
|
||||||
|
### 后台启动长期运行进程(dev server 等)
|
||||||
|
|
||||||
|
不同操作系统后台启动进程的方式完全不同,选错会导致命令阻塞或失败:
|
||||||
|
|
||||||
|
| 操作系统 | 正确方式 | 错误方式(禁止) |
|
||||||
|
|---------|---------|----------------|
|
||||||
|
| **Windows** | `Start-Process -FilePath "npm" -ArgumentList "run","dev" -WorkingDirectory "..." -WindowStyle Hidden` | `cmd /c "start /B npm run dev > log 2>&1"`(`cmd /c` 会阻塞等待重定向完成) |
|
||||||
|
| **macOS/Linux** | `nohup npm run dev > log 2>&1 &` | |
|
||||||
|
|
||||||
|
**核心原则:**
|
||||||
|
- Windows 下 `cmd /c "start /B ... > log 2>&1"` **绝对禁止**用于启动长期运行的后台服务。`cmd /c` 会等待重定向管道关闭,导致命令无限阻塞。
|
||||||
|
- Windows 下启动后台进程必须使用 PowerShell 的 `Start-Process` cmdlet,并通过 `-WindowStyle Hidden` 隐藏窗口。
|
||||||
|
|
||||||
|
### 启动前检查(强制)
|
||||||
|
|
||||||
|
**启动任何开发服务器前,必须先检查目标端口是否已被占用:**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Windows
|
||||||
|
netstat -ano | findstr "端口号"
|
||||||
|
# macOS/Linux
|
||||||
|
lsof -i :端口号
|
||||||
|
```
|
||||||
|
|
||||||
|
如果端口已被占用,必须先终止旧进程再启动新进程,**禁止重复启动**导致端口递增。
|
||||||
|
|
||||||
|
### 热加载优先原则
|
||||||
|
|
||||||
|
Vite / Webpack 等现代构建工具支持热更新(HMR):
|
||||||
|
- **启动一次即可**:`npm run dev` 启动后,修改源码文件会自动热更新
|
||||||
|
- **禁止频繁重启**:除非修改了 `vite.config.ts`、`.env`、`package.json` 等配置文件,否则不需要重启 dev server
|
||||||
|
- 重启前必须告知用户原因
|
||||||
@@ -2,61 +2,91 @@
|
|||||||
<view class="detail-page">
|
<view class="detail-page">
|
||||||
<view class="space-bg"></view>
|
<view class="space-bg"></view>
|
||||||
|
|
||||||
<view class="floating-nav" :style="floatingTopStyle">
|
<scroll-view
|
||||||
<view class="circle-btn" @click="goBack">
|
class="content"
|
||||||
<view class="back-icon"></view>
|
scroll-y
|
||||||
</view>
|
:show-scrollbar="false"
|
||||||
<view class="circle-btn" @click="openMore">
|
:scroll-top="scrollTop"
|
||||||
<view class="more-dot"></view>
|
@scroll="onContentScroll"
|
||||||
<view class="more-dot"></view>
|
scroll-with-animation
|
||||||
<view class="more-dot"></view>
|
>
|
||||||
</view>
|
<view class="top-safe" :style="{ height: capsuleTopReservePx + 'px' }"></view>
|
||||||
</view>
|
<view class="event-content-card glass-card">
|
||||||
|
<view class="event-hero-section">
|
||||||
|
<view class="hero-timeline">
|
||||||
|
<view class="timeline-glow"></view>
|
||||||
|
<view class="timeline-node"></view>
|
||||||
|
<view class="timeline-line"></view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<scroll-view class="content" scroll-y :show-scrollbar="false">
|
<view class="hero-copy">
|
||||||
<view class="hero-card glass-card">
|
<text class="year">{{ eventYear }}</text>
|
||||||
<view class="hero-timeline">
|
<view class="date-row">
|
||||||
<view class="timeline-glow"></view>
|
<text class="date-range">{{ dateRange }}</text>
|
||||||
<view class="timeline-node"></view>
|
<text class="type-chip">{{ primaryTag }}</text>
|
||||||
<view class="timeline-line"></view>
|
</view>
|
||||||
|
<text class="event-title">{{ displayEvent.title || '决定全职做自己热爱的AI产品' }}</text>
|
||||||
|
<view class="location-row">
|
||||||
|
<view class="pin-icon"></view>
|
||||||
|
<text>{{ locationText }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="memory-cover">
|
||||||
|
<view class="cover-sky"></view>
|
||||||
|
<view class="cover-window"></view>
|
||||||
|
<view class="cover-desk"></view>
|
||||||
|
<view class="cover-screen main"></view>
|
||||||
|
<view class="cover-screen small left"></view>
|
||||||
|
<view class="cover-screen small right"></view>
|
||||||
|
</view>
|
||||||
|
<view class="hero-star">★</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="hero-copy">
|
<view class="event-body-section">
|
||||||
<text class="year">{{ eventYear }}</text>
|
<view class="event-body-toolbar">
|
||||||
<view class="date-row">
|
<view class="content-action favorite-action" @click="toggleFavorite">
|
||||||
<text class="date-range">{{ dateRange }}</text>
|
<view class="bookmark-icon" :class="{ active: isFavorite }"></view>
|
||||||
<text class="type-chip">{{ primaryTag }}</text>
|
<text>{{ isFavorite ? '已收藏' : '收藏' }}</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="toolbar-right">
|
||||||
|
<view class="analysis-pill" @click="scrollToAnalysis">
|
||||||
|
<text>分析</text>
|
||||||
|
</view>
|
||||||
|
<view class="content-action share-action" @click="shareCurrent">
|
||||||
|
<view class="share-icon">
|
||||||
|
<view></view>
|
||||||
|
<view></view>
|
||||||
|
<view></view>
|
||||||
|
</view>
|
||||||
|
<text>分享</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<text class="event-title">{{ displayEvent.title || '决定全职做自己热爱的AI产品' }}</text>
|
|
||||||
<view class="location-row">
|
<view class="section-title-row description-title-row">
|
||||||
<view class="pin-icon"></view>
|
<view class="orbit-icon"></view>
|
||||||
<text>{{ locationText }}</text>
|
<text>事件描述</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="description-markdown" :class="{ collapsed: isDescriptionCollapsed }">
|
||||||
|
<Markdown :content="eventDescription" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="event-body-footer">
|
||||||
|
<view class="content-action edit-action" @click="editEvent">
|
||||||
|
<view class="edit-icon"></view>
|
||||||
|
<text>编辑</text>
|
||||||
|
</view>
|
||||||
|
<text class="collapse-action" @click="isDescriptionCollapsed = !isDescriptionCollapsed">
|
||||||
|
{{ isDescriptionCollapsed ? '展开' : '收起' }} ^
|
||||||
|
</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="memory-cover">
|
|
||||||
<view class="cover-sky"></view>
|
|
||||||
<view class="cover-window"></view>
|
|
||||||
<view class="cover-desk"></view>
|
|
||||||
<view class="cover-screen main"></view>
|
|
||||||
<view class="cover-screen small left"></view>
|
|
||||||
<view class="cover-screen small right"></view>
|
|
||||||
</view>
|
|
||||||
<view class="hero-star">★</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="description-card glass-card">
|
<view class="analysis-panel" id="analysisPanel">
|
||||||
<view class="section-title-row">
|
|
||||||
<view class="orbit-icon"></view>
|
|
||||||
<text>事件描述</text>
|
|
||||||
</view>
|
|
||||||
<text class="description-text" :class="{ collapsed: isDescriptionCollapsed }">{{ eventDescription }}</text>
|
|
||||||
<text class="collapse-action" @click="isDescriptionCollapsed = !isDescriptionCollapsed">
|
|
||||||
{{ isDescriptionCollapsed ? '展开' : '收起' }} ^
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="analysis-panel">
|
|
||||||
<view class="section-title-row analysis-head">
|
<view class="section-title-row analysis-head">
|
||||||
<text class="sparkle">✦</text>
|
<text class="sparkle">✦</text>
|
||||||
<text>AI 分析</text>
|
<text>AI 分析</text>
|
||||||
@@ -90,25 +120,9 @@
|
|||||||
</scroll-view>
|
</scroll-view>
|
||||||
|
|
||||||
<view class="bottom-actions" :style="{ paddingBottom: bottomInset + 'px' }">
|
<view class="bottom-actions" :style="{ paddingBottom: bottomInset + 'px' }">
|
||||||
<view class="bottom-action" @click="editEvent">
|
|
||||||
<view class="edit-icon"></view>
|
|
||||||
<text>编辑</text>
|
|
||||||
</view>
|
|
||||||
<view class="bottom-action" @click="toggleFavorite">
|
|
||||||
<view class="bookmark-icon" :class="{ active: isFavorite }"></view>
|
|
||||||
<text>{{ isFavorite ? '已收藏' : '收藏' }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="chat-action" @click="chatEvent">
|
<view class="chat-action" @click="chatEvent">
|
||||||
<view class="chat-badge">AI</view>
|
<view class="chat-badge">AI</view>
|
||||||
<text>和开开聊聊这段经历</text>
|
<text>聊聊</text>
|
||||||
</view>
|
|
||||||
<view class="bottom-action" @click="shareCurrent">
|
|
||||||
<view class="share-icon">
|
|
||||||
<view></view>
|
|
||||||
<view></view>
|
|
||||||
<view></view>
|
|
||||||
</view>
|
|
||||||
<text>分享</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -118,17 +132,19 @@
|
|||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useAppStore } from '../../stores/app.js'
|
import { useAppStore } from '../../stores/app.js'
|
||||||
import Markdown from '../../components/Markdown.vue'
|
import Markdown from '../../components/Markdown.vue'
|
||||||
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
|
|
||||||
import analytics from '../../services/analytics.js'
|
import analytics from '../../services/analytics.js'
|
||||||
|
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
|
||||||
|
|
||||||
const store = useAppStore()
|
const store = useAppStore()
|
||||||
const pagePath = '/pages/life-event/detail'
|
const pagePath = '/pages/life-event/detail'
|
||||||
const { floatingTopStyle } = useMenuButtonSafeArea()
|
const { capsuleTopReservePx } = useMenuButtonSafeArea({ extraTopPx: 10 })
|
||||||
const safeAreaBottom = ref(0)
|
const safeAreaBottom = ref(0)
|
||||||
const eventId = ref('')
|
const eventId = ref('')
|
||||||
const cachedEvent = ref(null)
|
const cachedEvent = ref(null)
|
||||||
const isFavorite = ref(false)
|
const isFavorite = ref(false)
|
||||||
const isDescriptionCollapsed = ref(false)
|
const isDescriptionCollapsed = ref(false)
|
||||||
|
const scrollTop = ref(0)
|
||||||
|
const currentScrollTop = ref(0)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const info = uni.getWindowInfo()
|
const info = uni.getWindowInfo()
|
||||||
@@ -280,6 +296,20 @@ const analysisBlocks = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onContentScroll = (event) => {
|
||||||
|
currentScrollTop.value = event.detail?.scrollTop || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollToAnalysis = () => {
|
||||||
|
uni.createSelectorQuery()
|
||||||
|
.select('#analysisPanel')
|
||||||
|
.boundingClientRect((rect) => {
|
||||||
|
if (!rect) return
|
||||||
|
scrollTop.value = Math.max(0, currentScrollTop.value + rect.top - 24)
|
||||||
|
})
|
||||||
|
.exec()
|
||||||
|
}
|
||||||
|
|
||||||
const editEvent = () => {
|
const editEvent = () => {
|
||||||
if (!eventId.value) return
|
if (!eventId.value) return
|
||||||
uni.navigateTo({ url: `/pages/life-event/form?id=${eventId.value}` })
|
uni.navigateTo({ url: `/pages/life-event/form?id=${eventId.value}` })
|
||||||
@@ -385,59 +415,18 @@ const goBack = () => {
|
|||||||
background-position: 44rpx 44rpx, 10rpx 112rpx;
|
background-position: 44rpx 44rpx, 10rpx 112rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-nav,
|
|
||||||
.content,
|
.content,
|
||||||
.bottom-actions {
|
.bottom-actions {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.floating-nav {
|
|
||||||
height: 108rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 32rpx;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.circle-btn {
|
|
||||||
width: 64rpx;
|
|
||||||
height: 64rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 5rpx;
|
|
||||||
color: #fff;
|
|
||||||
border: 1rpx solid rgba(118, 82, 225, 0.35);
|
|
||||||
background: rgba(22, 18, 63, 0.72);
|
|
||||||
box-shadow: inset 0 0 20rpx rgba(137, 91, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-icon {
|
|
||||||
width: 23rpx;
|
|
||||||
height: 23rpx;
|
|
||||||
border-left: 6rpx solid #fff;
|
|
||||||
border-bottom: 6rpx solid #fff;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
margin-left: 7rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.more-dot {
|
|
||||||
width: 7rpx;
|
|
||||||
height: 7rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 0;
|
height: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0 32rpx 134rpx;
|
padding: 12rpx 32rpx 154rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glass-card {
|
.glass-card {
|
||||||
@@ -450,15 +439,24 @@ const goBack = () => {
|
|||||||
-webkit-backdrop-filter: blur(24rpx);
|
-webkit-backdrop-filter: blur(24rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-card {
|
.event-content-card {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-hero-section {
|
||||||
position: relative;
|
position: relative;
|
||||||
min-height: 264rpx;
|
min-height: 264rpx;
|
||||||
border-radius: 28rpx;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 70rpx 1fr 148rpx;
|
grid-template-columns: 70rpx 1fr 148rpx;
|
||||||
gap: 20rpx;
|
gap: 20rpx;
|
||||||
padding: 38rpx 34rpx 32rpx;
|
padding: 40rpx 34rpx 34rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 86% 12%, rgba(129, 76, 255, 0.22), transparent 36%),
|
||||||
|
linear-gradient(145deg, rgba(6, 8, 35, 0.98) 0%, rgba(15, 13, 58, 0.96) 58%, rgba(23, 16, 72, 0.92) 100%);
|
||||||
|
box-shadow: inset 0 -1rpx 0 rgba(164, 116, 255, 0.22), inset 0 0 42rpx rgba(63, 45, 148, 0.16);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-timeline {
|
.hero-timeline {
|
||||||
@@ -633,7 +631,15 @@ const goBack = () => {
|
|||||||
text-shadow: 0 0 22rpx rgba(255, 198, 85, 0.9);
|
text-shadow: 0 0 22rpx rgba(255, 198, 85, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-card,
|
.event-body-section {
|
||||||
|
position: relative;
|
||||||
|
padding: 26rpx 30rpx 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 92% 0%, rgba(159, 98, 255, 0.12), transparent 36%),
|
||||||
|
rgba(13, 16, 52, 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
.tags-card {
|
.tags-card {
|
||||||
border-radius: 28rpx;
|
border-radius: 28rpx;
|
||||||
margin-top: 18rpx;
|
margin-top: 18rpx;
|
||||||
@@ -641,6 +647,62 @@ const goBack = () => {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.event-body-toolbar,
|
||||||
|
.event-body-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 18rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-body-toolbar {
|
||||||
|
min-height: 64rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-right {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-action {
|
||||||
|
min-height: 64rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
color: rgba(238, 231, 255, 0.88);
|
||||||
|
font-size: 23rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorite-action,
|
||||||
|
.share-action,
|
||||||
|
.edit-action {
|
||||||
|
padding: 0 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.analysis-pill {
|
||||||
|
min-width: 92rpx;
|
||||||
|
height: 54rpx;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-radius: 0 18rpx 18rpx 18rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: rgba(231, 217, 255, 0.86);
|
||||||
|
font-size: 23rpx;
|
||||||
|
font-weight: 900;
|
||||||
|
background: rgba(157, 104, 255, 0.18);
|
||||||
|
border: 1rpx solid rgba(193, 154, 255, 0.24);
|
||||||
|
box-shadow: inset 0 0 18rpx rgba(185, 124, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-title-row {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.section-title-row {
|
.section-title-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -671,25 +733,24 @@ const goBack = () => {
|
|||||||
border-color: rgba(176, 112, 255, 0.62);
|
border-color: rgba(176, 112, 255, 0.62);
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-text {
|
.description-markdown {
|
||||||
display: block;
|
margin-top: 22rpx;
|
||||||
margin-top: 24rpx;
|
overflow: hidden;
|
||||||
color: rgba(227, 218, 246, 0.73);
|
|
||||||
font-size: 25rpx;
|
|
||||||
line-height: 1.75;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.description-text.collapsed {
|
.description-markdown.collapsed {
|
||||||
display: -webkit-box;
|
max-height: 168rpx;
|
||||||
overflow: hidden;
|
}
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
.event-body-footer {
|
||||||
|
margin-top: 18rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapse-action {
|
.collapse-action {
|
||||||
display: block;
|
min-height: 64rpx;
|
||||||
margin-top: 14rpx;
|
display: flex;
|
||||||
text-align: right;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
color: #a970ff;
|
color: #a970ff;
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -830,10 +891,9 @@ const goBack = () => {
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
min-height: 108rpx;
|
min-height: 108rpx;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 0.76fr 0.76fr 2fr 0.76fr;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8rpx;
|
justify-content: center;
|
||||||
padding: 14rpx 32rpx 0;
|
padding: 14rpx 32rpx 0;
|
||||||
border-radius: 36rpx 36rpx 0 0;
|
border-radius: 36rpx 36rpx 0 0;
|
||||||
border-top: 1rpx solid rgba(126, 87, 255, 0.32);
|
border-top: 1rpx solid rgba(126, 87, 255, 0.32);
|
||||||
@@ -843,25 +903,17 @@ const goBack = () => {
|
|||||||
-webkit-backdrop-filter: blur(28rpx);
|
-webkit-backdrop-filter: blur(28rpx);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-action,
|
|
||||||
.chat-action {
|
.chat-action {
|
||||||
|
width: min(420rpx, 100%);
|
||||||
height: 72rpx;
|
height: 72rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10rpx;
|
gap: 12rpx;
|
||||||
color: rgba(238, 231, 255, 0.9);
|
|
||||||
font-size: 24rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottom-action {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-action {
|
|
||||||
border-radius: 999rpx;
|
border-radius: 999rpx;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 900;
|
||||||
background: linear-gradient(135deg, #b246ff, #742fff 58%, #5126ff);
|
background: linear-gradient(135deg, #b246ff, #742fff 58%, #5126ff);
|
||||||
box-shadow: 0 0 28rpx rgba(168, 85, 247, 0.58), inset 0 1rpx 0 rgba(255, 255, 255, 0.18);
|
box-shadow: 0 0 28rpx rgba(168, 85, 247, 0.58), inset 0 1rpx 0 rgba(255, 255, 255, 0.18);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user