# Mini Program Life Event Detail Actions Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 调整小程序人生轨迹详情页，把事件标题卡与事件描述卡合并，移除顶部悬浮操作 icon，并将收藏、分享、分析、编辑、收起和“聊聊”入口按新布局展示。

**Architecture:** 只修改 `mini-program/src/pages/life-event/detail.vue`。模板层用新的 `event-content-card` 替换 `floating-nav`、`hero-card`、`description-card` 组合；脚本层增加 `scrollTop`、`currentScrollTop`、`onContentScroll` 与 `scrollToAnalysis` 来支持分析按钮定位；样式层复用当前宇宙玻璃风格并新增局部按钮和 Markdown 折叠样式。

**Tech Stack:** UniApp, Vue 3 `<script setup>`, CSS scoped, existing `mini-program/src/components/Markdown.vue`, npm scripts from `mini-program/package.json`.

---

## File Structure

- Modify: `mini-program/src/pages/life-event/detail.vue`
  - Template: 合并事件标题区和事件描述区，迁移操作按钮，底部只保留居中的聊天主按钮。
  - Script: 删除 `useMenuButtonSafeArea` 使用，新增 `scrollTop`、`currentScrollTop`、`onContentScroll`、`scrollToAnalysis`，保留现有业务方法。
  - Style: 删除顶部悬浮导航样式，新增合并卡、正文 Markdown 折叠、内容区按钮和底部聊天按钮样式。

- Verify only: `mini-program/src/components/Markdown.vue`
  - 不改文件，仅复用 `<Markdown :content="eventDescription" />`。

---

### Task 1: Template Restructure

**Files:**
- Modify: `mini-program/src/pages/life-event/detail.vue`

- [ ] **Step 1: Remove the floating nav block**

Delete this block from the template:

```vue
<view class="floating-nav" :style="floatingTopStyle">
  <view class="circle-btn" @click="goBack">
    <view class="back-icon"></view>
  </view>
  <view class="circle-btn" @click="openMore">
    <view class="more-dot"></view>
    <view class="more-dot"></view>
    <view class="more-dot"></view>
  </view>
</view>
```

Expected result: 页面左上角和右上角不再渲染悬浮 icon。

- [ ] **Step 2: Add scroll state binding to the page scroll-view**

Change the current scroll-view opening tag from:

```vue
<scroll-view class="content" scroll-y :show-scrollbar="false">
```

to:

```vue
<scroll-view
  class="content"
  scroll-y
  :show-scrollbar="false"
  :scroll-top="scrollTop"
  @scroll="onContentScroll"
  scroll-with-animation
>
```

Expected result: `scrollToAnalysis` can move the scroll-view to the AI 分析 section and account for the current scroll position.

- [ ] **Step 3: Replace hero-card and description-card with the merged event-content-card**

Replace the whole existing `hero-card` and `description-card` blocks with:

```vue
<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>

    <view class="hero-copy">
      <text class="year">{{ eventYear }}</text>
      <view class="date-row">
        <text class="date-range">{{ dateRange }}</text>
        <text class="type-chip">{{ primaryTag }}</text>
      </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 class="event-body-section">
    <view class="event-body-toolbar">
      <view class="content-action favorite-action" @click="toggleFavorite">
        <view class="bookmark-icon" :class="{ active: isFavorite }"></view>
        <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 class="section-title-row description-title-row">
      <view class="orbit-icon"></view>
      <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>
```

Expected result: 事件信息和事件描述在同一张卡内，正文通过 `Markdown` 组件渲染。

- [ ] **Step 4: Add the analysis anchor data attribute**

Change the current AI analysis opening block from:

```vue
<view class="analysis-panel">
```

to:

```vue
<view class="analysis-panel" id="analysisPanel">
```

Expected result: `uni.createSelectorQuery()` can locate the AI 分析 block.

- [ ] **Step 5: Replace bottom action bar with centered chat action**

Replace the existing bottom action bar:

```vue
<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-badge">AI</view>
    <text>和开开聊聊这段经历</text>
  </view>
  <view class="bottom-action" @click="shareCurrent">
    <view class="share-icon">
      <view></view>
      <view></view>
      <view></view>
    </view>
    <text>分享</text>
  </view>
</view>
```

with:

```vue
<view class="bottom-actions" :style="{ paddingBottom: bottomInset + 'px' }">
  <view class="chat-action" @click="chatEvent">
    <view class="chat-badge">AI</view>
    <text>聊聊</text>
  </view>
</view>
```

Expected result: 底部只保留居中的紫色主按钮，文案为“聊聊”。

---

### Task 2: Script State And Methods

**Files:**
- Modify: `mini-program/src/pages/life-event/detail.vue`

- [ ] **Step 1: Remove the unused safe-area composable import**

Change imports from:

```js
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import Markdown from '../../components/Markdown.vue'
import { useMenuButtonSafeArea } from '../../composables/useMenuButtonSafeArea.js'
import analytics from '../../services/analytics.js'
```

to:

```js
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useAppStore } from '../../stores/app.js'
import Markdown from '../../components/Markdown.vue'
import analytics from '../../services/analytics.js'
```

Expected result: 没有未使用的 `useMenuButtonSafeArea` import。

- [ ] **Step 2: Replace floating nav state with scroll state**

Change:

```js
const store = useAppStore()
const pagePath = '/pages/life-event/detail'
const { floatingTopStyle } = useMenuButtonSafeArea()
const safeAreaBottom = ref(0)
const eventId = ref('')
const cachedEvent = ref(null)
const isFavorite = ref(false)
const isDescriptionCollapsed = ref(false)
```

to:

```js
const store = useAppStore()
const pagePath = '/pages/life-event/detail'
const safeAreaBottom = ref(0)
const eventId = ref('')
const cachedEvent = ref(null)
const isFavorite = ref(false)
const isDescriptionCollapsed = ref(false)
const scrollTop = ref(0)
const currentScrollTop = ref(0)
```

Expected result: scroll-view has a reactive value available for animated positioning.

- [ ] **Step 3: Add scroll methods before editEvent**

Insert these methods before `const editEvent = () => {`:

```js
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()
}
```

Expected result: 点击“分析”后 scroll-view 会向 AI 分析卡移动。

- [ ] **Step 4: Keep existing business methods unchanged**

Do not change the bodies of these existing methods:

```js
const editEvent = () => {
  if (!eventId.value) return
  uni.navigateTo({ url: `/pages/life-event/form?id=${eventId.value}` })
}

const toggleFavorite = async () => {
  if (!eventId.value) return
  const next = !isFavorite.value
  const result = await store.favoriteEvent({ id: eventId.value, favorite: next })
  if (!result.success) {
    uni.showToast({ title: result.error || '收藏失败', icon: 'none' })
    return
  }
  isFavorite.value = next
  if (next) uni.setStorageSync(`event_favorite_${eventId.value}`, '1')
  else uni.removeStorageSync(`event_favorite_${eventId.value}`)
  analytics.track('life_event_favorite', {
    life_event_id: eventId.value,
    favorite: next
  }, { eventType: 'life_event', pagePath })
  uni.showToast({ title: next ? '已收藏' : '已取消收藏', icon: 'success' })
}

const chatEvent = async () => {
  const result = await store.chatAboutEvent({
    id: eventId.value,
    title: displayEvent.value.title,
    content: displayEvent.value.content
  })
  uni.showModal({
    title: '和开开聊聊',
    content: result.success ? result.data?.reply || '聊天能力已准备' : result.error || '聊天服务暂不可用',
    showCancel: false
  })
}

const shareCurrent = async () => {
  const result = await store.shareEvent({
    id: eventId.value,
    title: displayEvent.value.title,
    content: displayEvent.value.content
  })
  if (!result.success) {
    uni.showToast({ title: result.error || '分享失败', icon: 'none' })
    return
  }
  analytics.track('life_event_share', {
    life_event_id: eventId.value,
    tag_count: Array.isArray(displayEvent.value.tags) ? displayEvent.value.tags.length : 0
  }, { eventType: 'life_event', pagePath })
  uni.setClipboardData({
    data: result.data?.shareText || `分享我的人生轨迹：${displayEvent.value.title || ''}`,
    success: () => uni.showToast({ title: '分享文案已复制', icon: 'success' })
  })
}
```

Expected result: 编辑、收藏、分享、聊天功能沿用现有行为。

---

### Task 3: Styles For Merged Card And Actions

**Files:**
- Modify: `mini-program/src/pages/life-event/detail.vue`

- [ ] **Step 1: Update z-index selector**

Change:

```css
.floating-nav,
.content,
.bottom-actions {
  position: relative;
  z-index: 1;
}
```

to:

```css
.content,
.bottom-actions {
  position: relative;
  z-index: 1;
}
```

Expected result: CSS no longer references removed `floating-nav`.

- [ ] **Step 2: Delete removed floating navigation styles**

Delete these style blocks:

```css
.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;
}
```

Expected result: no dead CSS for removed top icons.

- [ ] **Step 3: Adjust scroll content spacing**

Change:

```css
.content {
  flex: 1;
  height: 0;
  min-height: 0;
  box-sizing: border-box;
  padding: 0 32rpx 134rpx;
}
```

to:

```css
.content {
  flex: 1;
  height: 0;
  min-height: 0;
  box-sizing: border-box;
  padding: 36rpx 32rpx 154rpx;
}
```

Expected result: merged card starts with breathing room after top safe area removal, and bottom content is not covered by the chat button.

- [ ] **Step 4: Replace hero-card styles with event-content-card and event-hero-section**

Replace:

```css
.hero-card {
  position: relative;
  min-height: 264rpx;
  border-radius: 28rpx;
  display: grid;
  grid-template-columns: 70rpx 1fr 148rpx;
  gap: 20rpx;
  padding: 38rpx 34rpx 32rpx;
  box-sizing: border-box;
}
```

with:

```css
.event-content-card {
  position: relative;
  overflow: hidden;
  border-radius: 30rpx;
}

.event-hero-section {
  position: relative;
  min-height: 264rpx;
  display: grid;
  grid-template-columns: 70rpx 1fr 148rpx;
  gap: 20rpx;
  padding: 40rpx 34rpx 34rpx;
  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);
}
```

Expected result: top half of the merged card is visually deeper than the body area.

- [ ] **Step 5: Replace description-card styles with event body styles**

Replace:

```css
.description-card,
.tags-card {
  border-radius: 28rpx;
  margin-top: 18rpx;
  padding: 28rpx 30rpx;
  box-sizing: border-box;
}
```

with:

```css
.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 {
  border-radius: 28rpx;
  margin-top: 18rpx;
  padding: 28rpx 30rpx;
  box-sizing: border-box;
}
```

Expected result: description body sits inside the merged card and related tags keep their card styling.

- [ ] **Step 6: Add content action toolbar styles before `.section-title-row`**

Insert:

```css
.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;
}
```

Expected result: 收藏与分享同一水平线，分析按钮弱化并位于右上凹位。

- [ ] **Step 7: Replace description text styles with Markdown wrapper styles**

Replace:

```css
.description-text {
  display: block;
  margin-top: 24rpx;
  color: rgba(227, 218, 246, 0.73);
  font-size: 25rpx;
  line-height: 1.75;
}

.description-text.collapsed {
  display: -webkit-box;
  overflow: hidden;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
}

.collapse-action {
  display: block;
  margin-top: 14rpx;
  text-align: right;
  color: #a970ff;
  font-size: 24rpx;
  font-weight: 800;
}
```

with:

```css
.description-markdown {
  margin-top: 22rpx;
  overflow: hidden;
}

.description-markdown.collapsed {
  max-height: 168rpx;
}

.event-body-footer {
  margin-top: 18rpx;
}

.collapse-action {
  min-height: 64rpx;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  color: #a970ff;
  font-size: 24rpx;
  font-weight: 800;
}
```

Expected result: Markdown content can collapse without changing `Markdown.vue`.

- [ ] **Step 8: Replace bottom action layout styles**

Replace the current `.bottom-actions`, `.bottom-action`, and `.chat-action` blocks:

```css
.bottom-actions {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  min-height: 108rpx;
  box-sizing: border-box;
  display: grid;
  grid-template-columns: 0.76fr 0.76fr 2fr 0.76fr;
  align-items: center;
  gap: 8rpx;
  padding: 14rpx 32rpx 0;
  border-radius: 36rpx 36rpx 0 0;
  border-top: 1rpx solid rgba(126, 87, 255, 0.32);
  background: rgba(13, 13, 43, 0.94);
  box-shadow: 0 -14rpx 44rpx rgba(0, 0, 0, 0.45), inset 0 0 28rpx rgba(143, 92, 255, 0.1);
  backdrop-filter: blur(28rpx);
  -webkit-backdrop-filter: blur(28rpx);
}

.bottom-action,
.chat-action {
  height: 72rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10rpx;
  color: rgba(238, 231, 255, 0.9);
  font-size: 24rpx;
  font-weight: 800;
}

.bottom-action {
  min-width: 0;
}

.chat-action {
  border-radius: 999rpx;
  color: #fff;
  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);
}
```

with:

```css
.bottom-actions {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  min-height: 108rpx;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14rpx 32rpx 0;
  border-radius: 36rpx 36rpx 0 0;
  border-top: 1rpx solid rgba(126, 87, 255, 0.32);
  background: rgba(13, 13, 43, 0.94);
  box-shadow: 0 -14rpx 44rpx rgba(0, 0, 0, 0.45), inset 0 0 28rpx rgba(143, 92, 255, 0.1);
  backdrop-filter: blur(28rpx);
  -webkit-backdrop-filter: blur(28rpx);
}

.chat-action {
  width: min(420rpx, 100%);
  height: 72rpx;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12rpx;
  border-radius: 999rpx;
  color: #fff;
  font-size: 26rpx;
  font-weight: 900;
  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);
}
```

Expected result: “聊聊” button is centered and visually primary.

- [ ] **Step 9: Confirm shared icon styles still work in new locations**

Keep the existing `.edit-icon`, `.bookmark-icon`, `.share-icon`, and `.chat-badge` blocks unchanged.

Expected result: content toolbar and footer reuse the existing drawn icons with no new asset dependency.

---

### Task 4: Build Verification

**Files:**
- Verify: `mini-program/src/pages/life-event/detail.vue`
- Verify: `mini-program/package.json`

- [ ] **Step 1: Run H5 production build**

Run:

```bash
cd mini-program
npm run build:h5
```

Expected: command exits with code 0 and Vite/UniApp writes the H5 build output under `mini-program/dist/build/h5` or the configured UniApp output directory.

- [ ] **Step 2: Fix compile errors by editing only detail.vue**

If the build reports a compile error in `detail.vue`, fix the exact syntax issue in `mini-program/src/pages/life-event/detail.vue`, then rerun:

```bash
cd mini-program
npm run build:h5
```

Expected: command exits with code 0.

---

### Task 5: Browser Verification

**Files:**
- Verify: `mini-program/src/pages/life-event/detail.vue`

- [ ] **Step 1: Start H5 dev server**

Run:

```bash
cd mini-program
npm run dev:h5
```

Expected: dev server prints a local URL, usually `http://localhost:5173`.

- [ ] **Step 2: Open the app in a browser**

Use the available browser automation tool to open:

```text
http://localhost:5173
```

Expected: app loads without a blank screen.

- [ ] **Step 3: Navigate to a life event detail page**

Use the app UI to reach 人生轨迹详情页. If the local dev data has no event entry, open a detail URL with the current route format after creating or selecting an event through the UI.

Expected: the detail page renders and uses local store/cached fallback data when available.

- [ ] **Step 4: Verify visual requirements**

Check these exact outcomes:

- The left-top edit icon and right-top more icon are absent.
- Event title information and event description are inside one merged card.
- The upper title area is visibly darker than the body area.
- Event description is rendered through Markdown formatting.
- 收藏 is at the body area's left top.
- 分享 is horizontally aligned with 收藏.
- 分析 is a lighter pill at the right-top side of the body area.
- 编辑 is at the body area's left bottom.
- 编辑 and 收起 or 展开 are horizontally aligned.
- 分析 and 收起 or 展开 are vertically aligned on the right side.
- The bottom button is centered and displays “聊聊”.
- The page has no overlapping text at mobile width.

Expected: all checks pass.

- [ ] **Step 5: Verify button behavior and console health**

Click these controls:

- 收藏
- 分享
- 分析
- 编辑
- 收起 or 展开
- 聊聊

Expected:

- 收藏 toggles visual state or shows the existing failure toast when unauthenticated.
- 分享 copies or shows the existing failure toast.
- 分析 scrolls to the AI 分析 section.
- 编辑 navigates to the event form when `eventId` exists.
- 收起/展开 toggles Markdown body height.
- 聊聊 opens the existing chat modal or shows existing failure text.
- Browser Console contains no new errors from `detail.vue`.

---

## Self-Review

Spec coverage:

- Top icons removed: Task 1 Step 1, Task 3 Step 1-2.
- Title and content merged: Task 1 Step 3, Task 3 Step 4-5.
- Darker title area: Task 3 Step 4.
- Markdown description rendering: Task 1 Step 3, Task 3 Step 7.
- Button placement: Task 1 Step 3 and Step 5, Task 3 Step 6-8.
- Existing behavior preserved: Task 2 Step 4.
- Browser verification required by repository rules: Task 5.

Placeholder scan:

- This plan contains no placeholder or incomplete implementation steps.

Type and property consistency:

- `scrollTop` is declared as `ref(0)` and bound as `:scroll-top="scrollTop"`.
- `currentScrollTop` is declared as `ref(0)` and updated by `@scroll="onContentScroll"`.
- `scrollToAnalysis` is referenced by the template and defined in script.
- `analysisPanel` is the exact id used by selector query.
