Files
happy-life-star/docs/superpowers/plans/2026-05-18-mini-program-script-home-redesign-plan.md
peanut ee5a6aba5d feat: 小程序脚本首页重构 + 社交数据导入 + TTS 播放优化
- 后端:新增社交数据导入/审批/洞察生成 API(SocialContent/SocialInsight)
- 后端:优化脚本上下文服务,TTS 服务增强
- 小程序:重构脚本首页布局,新增社交导入页面
- 小程序:新增 useTtsPlayer composable,移除旧 ScriptAudioPlayer 组件
- 小程序:新增社交导入服务,优化请求服务
- SQL:新增社交数据导入建表脚本
- 文档:补充设计文档和实施计划

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 07:18:02 +08:00

21 KiB
Raw Permalink Blame History

Mini Program Script Home Redesign 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: Make 爽文生成 the mini program home experience and rebuild the wish-to-script flow from the 0517 UI requirement.

Architecture: Keep backend APIs and store contracts mostly unchanged. Rework the mini program shell default tab and rewrite ScriptView.vue into a state-driven experience with home, generating, and result states, reusing existing inspiration, script generation, analytics, and TTS components where possible.

Tech Stack: uni-app, Vue 3 <script setup>, mini program APIs, existing useAppStore, existing analytics service, existing ScriptAudioPlayer.


File Map

  • Modify: mini-program/src/pages/main/index.vue
    • Owns main shell default tab, bottom navigation order, and tab-level analytics.
  • Modify: mini-program/src/pages/main/ScriptView.vue
    • Owns the new 如愿星球 home UI, voice/text wish input, inspiration cards, generation state, result card, and result actions.
  • Modify: mini-program/src/pages/main/ScriptDetailView.vue
    • Only if shared labels or TTS behavior need minor alignment after ScriptView.vue result actions are wired.
  • Modify: mini-program/src/stores/app.js
    • Only if ScriptView.vue needs a small helper to retrieve the just-generated script or normalize generate responses.
  • Modify: mini-program/src/services/analytics.js
    • Only if adding event-name helpers improves clarity. Direct analytics.track(...) calls are acceptable because the current code already uses that pattern.

Review Guardrails

  • Keep ScriptView.vue on one primary state machine: home, generating, result. Do not keep the old mode = inspiration/custom/list model as a second primary flow.
  • 我的剧本 should leave this home flow and open the existing script library/list surface. Do not duplicate a full list inside the new home UI.
  • Voice recognition is runtime-dependent. The implementation must be useful with typed input even if speech recognition is unavailable.
  • TTS should use one visible control surface. Prefer the existing ScriptAudioPlayer component; avoid adding a second independent audio player button that competes with it.
  • Any touched visible Chinese string must be verified as UTF-8 and must not appear as mojibake in the mini program UI.
  • Each task should be buildable. If a step removes old template variables, remove their script/style references in the same task.

Task 1: Main Tab Priority

Files:

  • Modify: mini-program/src/pages/main/index.vue

  • Step 1: Change default active tab

Set the default tab to script:

const activeTab = ref('script')
  • Step 2: Reorder rendered views if needed

Keep all three views mounted by v-if, but ensure the mental order in template follows product priority:

<ScriptView v-if="activeTab === 'script'" />
<RecordView v-if="activeTab === 'record'" />
<MineView v-if="activeTab === 'mine'" />
  • Step 3: Reorder bottom navigation

Make 爽文生成 the first nav item, followed by 人生轨迹, then 我的.

Use visible Chinese labels:

<text>爽文生成</text>
<text>人生轨迹</text>
<text>我的</text>
  • Step 4: Verify analytics initial page view

Confirm existing code now sends:

analytics.trackPageView(pagePath, { tab: activeTab.value })

with tab: 'script' on first load.

  • Step 5: Run build

Run:

cd mini-program
npm run build:mp-weixin

Expected: build completes without Vue template or syntax errors.

Task 2: ScriptView State Model

Files:

  • Modify: mini-program/src/pages/main/ScriptView.vue

  • Step 1: Replace mode-first state with flow state

Add these state refs near the top of <script setup>:

const viewState = ref('home') // home | generating | result
const wishText = ref('')
const voiceState = ref('idle') // idle | pressing | recognizing | error
const generationStartedAt = ref(0)
const currentResult = ref(null)
const currentMessageTime = ref('')
const currentResultTime = ref('')
  • Step 2: Remove old primary mode state

Remove old primary UI state if the redesigned template no longer uses it:

const mode = ref('inspiration')

If a transitional implementation needs mode, restrict it to legacy navigation only and do not let it decide the first screen.

  • Step 3: Add time formatter

Add:

const formatMessageTime = () => {
  const date = new Date()
  const hours = String(date.getHours()).padStart(2, '0')
  const minutes = String(date.getMinutes()).padStart(2, '0')
  return `${hours}:${minutes}`
}
  • Step 4: Keep existing recommendation computed data

Retain the existing store-backed inspiration logic:

const recommendations = computed(() => {
  return randomRecommendations.value.length
    ? randomRecommendations.value
    : store.inspirationRecommendations.value.slice(0, 4)
})

If the actual file exposes store state differently, follow the current working computed value already in the file.

  • Step 5: Add result normalizer

Add a helper that tolerates the current API response shape:

const normalizeGeneratedScript = (data) => {
  const script = data?.script || data
  return {
    id: script?.id || '',
    title: script?.title || wishText.value || '我的人生剧本',
    theme: script?.theme || wishText.value,
    style: script?.style || '爽文',
    length: script?.length || 'medium',
    tags: script?.tags || [script?.style || '爽文', '成长', '被看见'],
    summary: script?.summary || '',
    content: script?.content || script?.summary || ''
  }
}
  • Step 6: Run build

Run:

cd mini-program
npm run build:mp-weixin

Expected: build completes.

Task 3: Wish Home UI

Files:

  • Modify: mini-program/src/pages/main/ScriptView.vue

  • Step 0: Decide whether to rewrite or extract

For this iteration, keep the implementation in ScriptView.vue unless the file becomes too hard to review. If extracting components, keep them local to the same page folder and use clear names:

mini-program/src/pages/main/components/WishHome.vue
mini-program/src/pages/main/components/WishGenerationState.vue
mini-program/src/pages/main/components/WishResultCard.vue

Do not extract components only for style preference; extract only if it reduces risk.

  • Step 1: Replace first-screen template with home state

Use this structure as the top-level content inside .script-view:

<view v-if="viewState === 'home'" class="wish-home">
  <view class="home-head">
    <view class="history-button" @click="openScriptLibrary">
      <text class="history-icon"></text>
      <text>历史</text>
    </view>
    <view class="script-list-btn" @click="openScriptLibrary">
      <text>我的剧本</text>
    </view>
  </view>

  <view class="hero-copy">
    <text class="hero-title">今天有什么</text>
    <text class="hero-title"><text class="hero-highlight">心愿</text>想实现</text>
  </view>

  <view
    class="mic-orb"
    :class="{ pressing: voiceState === 'pressing', recognizing: voiceState === 'recognizing' }"
    @touchstart.prevent="startVoicePress"
    @touchend.prevent="endVoicePress"
    @touchcancel.prevent="cancelVoicePress"
  >
    <view class="mic-core"></view>
    <text class="mic-icon">🎙</text>
  </view>

  <text class="voice-copy">{{ voiceCopy }}</text>

  <view class="wish-input-wrap">
    <input
      class="wish-input"
      v-model="wishText"
      confirm-type="send"
      placeholder="写下你的心愿,AI帮你重写人生"
      placeholder-class="placeholder"
      @confirm="submitWish('text')"
    />
    <view class="send-button" :class="{ disabled: !wishText.trim() }" @click="submitWish('text')">发送</view>
  </view>

  <view class="inspiration-section">
    <view class="section-line">
      <text class="section-title">灵感一下</text>
      <text class="refresh" @click="shuffleInspirations">换一换</text>
    </view>
    <view class="recommend-grid">
      <view
        v-for="item in recommendations"
        :key="item.text"
        class="recommend-card"
        @click="useRecommendation(item.text)"
      >
        <text>{{ item.text }}</text>
      </view>
    </view>
  </view>
</view>
  • Step 2: Add computed voice copy
const voiceCopy = computed(() => {
  if (voiceState.value === 'pressing') return '松开后开始实现心愿'
  if (voiceState.value === 'recognizing') return '正在识别你的心愿……'
  if (voiceState.value === 'error') return '语音暂不可用,可以先输入文字'
  return '按住说话,即刻如愿'
})
  • Step 3: Add library navigation
const openScriptLibrary = () => {
  analytics.track('script_my_scripts_click', {}, { eventType: 'script', pagePath })
  uni.$emit('switchTab', 'mine')
}

If product decides 我的剧本 should stay inside the script tab, replace the event with the existing list route or state used by the app. Do not reintroduce the old form/list mode as the default home flow.

  • Step 4: Add home styles

Use the tokens from the spec:

.wish-home {
  min-height: 100%;
  display: flex;
  flex-direction: column;
  gap: 32rpx;
  color: #fff;
}

Continue with the existing CSS block below, adapting class names if keeping some current styles.

  • Step 5: Verify touched labels are readable Chinese

Search touched files for common mojibake fragments before building:

rg -n "鐖|浜虹|鎴戠|蹇冩|璇|鍓ф" mini-program/src/pages/main/index.vue mini-program/src/pages/main/ScriptView.vue

Expected: no matches in newly touched visible labels. Existing untouched files can be handled in a separate cleanup if they are outside this change.

  • Step 6: Add full home styles

Use the remaining tokens from the spec:

.hero-title {
  display: block;
  font-size: 76rpx;
  font-weight: 800;
  line-height: 1.25;
}

.hero-highlight {
  color: #d18aff;
  text-shadow: 0 0 28rpx rgba(209, 138, 255, 0.52);
}

.mic-orb {
  width: 260rpx;
  height: 260rpx;
  border-radius: 50%;
  align-self: center;
  display: flex;
  align-items: center;
  justify-content: center;
  background: linear-gradient(145deg, #f1a0ff 0%, #934dff 48%, #4d1ccb 100%);
  box-shadow: 0 0 72rpx rgba(169, 85, 247, 0.75), 0 0 180rpx rgba(102, 41, 201, 0.55);
}

.mic-orb.pressing {
  transform: scale(1.06);
}

Adapt existing class names if keeping some current styles.

  • Step 7: Run build

Run:

cd mini-program
npm run build:mp-weixin

Expected: build completes and ScriptView.vue template compiles.

Task 4: Voice Press Interaction

Files:

  • Modify: mini-program/src/pages/main/ScriptView.vue

  • Step 1: Add press handlers

const startVoicePress = () => {
  voiceState.value = 'pressing'
  analytics.track('script_voice_press_start', {}, { eventType: 'script', pagePath })
}

const cancelVoicePress = () => {
  voiceState.value = 'idle'
}
  • Step 2: Add release handler with fallback
const endVoicePress = async () => {
  analytics.track('script_voice_press_end', {}, { eventType: 'script', pagePath })
  voiceState.value = 'recognizing'

  // First version fallback. Replace with WeChat speech plugin/API only when configured.
  setTimeout(() => {
    voiceState.value = 'error'
    analytics.track('script_voice_recognize_fail', {
      reason: 'speech_recognition_not_configured'
    }, { eventType: 'script', pagePath })
    uni.showToast({ title: '语音识别暂未配置,请先输入文字', icon: 'none' })
  }, 300)
}
  • Step 3: Add explicit typed-input recovery

When voice recognition is unavailable, return to an idle state after the toast so the microphone can be tried again:

setTimeout(() => {
  if (voiceState.value === 'error') {
    voiceState.value = 'idle'
  }
}, 1800)
  • Step 4: Keep old voice modal removed

Remove the old handleVoiceInput modal if it is no longer used by the template.

  • Step 5: Run build

Run:

cd mini-program
npm run build:mp-weixin

Expected: no unused-template handler errors.

Task 5: Submit And Generation State

Files:

  • Modify: mini-program/src/pages/main/ScriptView.vue

  • Step 1: Add submit function

const submitWish = async (source = 'text') => {
  const text = wishText.value.trim()
  if (!text || generating.value) return

  analytics.track('script_wish_submit', {
    source,
    prompt_length: text.length
  }, { eventType: 'script', pagePath })

  currentMessageTime.value = formatMessageTime()
  generationStartedAt.value = Date.now()
  generating.value = true
  viewState.value = 'generating'
  analytics.track('script_generation_progress_view', {
    source,
    prompt_length: text.length
  }, { eventType: 'script', pagePath })

  const res = await store.generateScriptFromInspiration({
    prompt: text,
    style: style.value,
    length: 'medium'
  })

  generating.value = false

  if (!res.success) {
    analytics.track('script_generate_fail', {
      source,
      error: res.error || 'unknown',
      duration_ms: Date.now() - generationStartedAt.value
    }, { eventType: 'script', pagePath })
    viewState.value = 'home'
    uni.showToast({ title: res.error || '生成失败', icon: 'none' })
    return
  }

  currentResult.value = normalizeGeneratedScript(res.data)
  currentResultTime.value = formatMessageTime()
  if (typeof res.data?.remainingCount === 'number') remainingCount.value = res.data.remainingCount

  analytics.track('script_generate_success', {
    source,
    duration_ms: Date.now() - generationStartedAt.value
  }, { eventType: 'script', pagePath })
  analytics.track('script_result_view', {
    script_id: currentResult.value.id,
    style: currentResult.value.style || '',
    length: currentResult.value.length || ''
  }, { eventType: 'script', pagePath })

  await store.fetchScripts()
  viewState.value = 'result'
}
  • Step 2: Add generating template
<view v-else-if="viewState === 'generating'" class="generation-view">
  <view class="chat-bubble user">
    <text>{{ wishText }}</text>
    <text class="bubble-time">{{ currentMessageTime }}</text>
  </view>
  <view class="chat-bubble system">
    <text>心愿实现中……</text>
    <text class="bubble-time">{{ currentMessageTime }}</text>
  </view>
  <view class="loading-orbit"></view>
</view>
  • Step 3: Run build

Run:

cd mini-program
npm run build:mp-weixin

Expected: build completes.

Task 6: Result Card And Actions

Files:

  • Modify: mini-program/src/pages/main/ScriptView.vue

  • Step 1: Import audio player

Add:

import ScriptAudioPlayer from '../../components/ScriptAudioPlayer.vue'
  • Step 2: Add result template with one TTS surface
<view v-else-if="viewState === 'result'" class="result-view">
  <view class="chat-bubble user">
    <text>{{ wishText }}</text>
    <text class="bubble-time">{{ currentMessageTime }}</text>
  </view>
  <view class="chat-bubble system done">
    <text>心愿已实现故事已为你展开</text>
    <text class="bubble-time">{{ currentResultTime }}</text>
  </view>

  <view class="story-card">
    <view class="story-head">
      <view>
        <text class="story-title">{{ currentResult?.title }}</text>
        <view class="tag-row">
          <text v-for="tag in currentResult?.tags || []" :key="tag" class="tag">{{ tag }}</text>
        </view>
      </view>
      <button class="close-icon" @click="closeResult">×</button>
    </view>

    <text class="story-body">{{ currentResult?.content || currentResult?.summary }}</text>

    <view class="audio-section" @click="trackTtsClick">
      <ScriptAudioPlayer v-if="currentResult?.id" :script-id="currentResult.id" />
      <text v-else class="audio-unavailable">生成保存后可语音播放</text>
    </view>

    <view class="result-actions">
      <button @click="changeDirection">换个方向</button>
      <button @click="notLikeMe">不像我</button>
      <button @click="trackTtsClick">语音播放</button>
    </view>
  </view>
</view>

The 语音播放 action should focus/scroll to .audio-section if possible. It should not create a separate audio implementation.

  • Step 3: Add action handlers
const closeResult = () => {
  viewState.value = 'home'
  currentResult.value = null
}

const changeDirection = () => {
  analytics.track('script_result_change_direction_click', {
    script_id: currentResult.value?.id || ''
  }, { eventType: 'script', pagePath })
  wishText.value = `${wishText.value},换一个方向重新展开`
  viewState.value = 'home'
}

const notLikeMe = () => {
  analytics.track('script_result_not_like_me_click', {
    script_id: currentResult.value?.id || ''
  }, { eventType: 'script', pagePath })
  uni.showToast({ title: '已记录反馈,可以调整心愿后再试', icon: 'none' })
}

const trackTtsClick = () => {
  analytics.track('script_result_tts_click', {
    script_id: currentResult.value?.id || ''
  }, { eventType: 'tts', pagePath })
  uni.showToast({ title: currentResult.value?.id ? '可在朗读控件中播放' : '生成保存后可播放', icon: 'none' })
}
  • Step 4: Style story card

Use:

.story-card {
  border-radius: 52rpx;
  padding: 34rpx;
  background: rgba(16, 8, 34, 0.72);
  border: 1rpx solid rgba(192, 132, 252, 0.55);
  box-shadow: 0 0 60rpx rgba(125, 55, 205, 0.18);
}

.story-title {
  font-size: 60rpx;
  font-weight: 700;
  line-height: 1.4;
}

.story-body {
  display: block;
  margin-top: 28rpx;
  font-size: 32rpx;
  line-height: 1.78;
  color: rgba(255, 255, 255, 0.92);
  white-space: pre-wrap;
}
  • Step 5: Run build

Run:

cd mini-program
npm run build:mp-weixin

Expected: build completes.

Task 7: Analytics Pass

Files:

  • Modify: mini-program/src/pages/main/ScriptView.vue

  • Modify: mini-program/src/pages/main/index.vue

  • Step 1: Add script home view event

In ScriptView.vue, add onMounted if not already present:

onMounted(() => {
  analytics.track('script_home_view', {}, { eventType: 'script', pagePath })
})

If onMounted already exists, append the track call to it.

  • Step 2: Update inspiration events

Ensure recommendation click tracks:

analytics.track('script_inspiration_select', {
  source: 'recommendation'
}, { eventType: 'script', pagePath })

Ensure refresh tracks:

analytics.track('script_inspiration_refresh', {
  source: 'home'
}, { eventType: 'script', pagePath })
  • Step 3: Verify one success/fail per submission

Read the final submitWish implementation and confirm every accepted submission emits exactly one terminal event:

script_wish_submit -> script_generate_success -> script_result_view

or:

script_wish_submit -> script_generate_fail
  • Step 4: Run build

Run:

cd mini-program
npm run build:mp-weixin

Expected: build completes.

Task 8: Regression Checks

Files:

  • Verify: mini-program/src/pages/main/index.vue

  • Verify: mini-program/src/pages/main/RecordView.vue

  • Verify: mini-program/src/pages/main/MineView.vue

  • Verify: mini-program/src/pages/main/ScriptDetailView.vue

  • Step 1: Build mini program

Run:

cd mini-program
npm run build:mp-weixin

Expected: success.

  • Step 2: Check generated files exist

Run:

Test-Path .\unpackage\dist\build\mp-weixin\app.json

Expected: True.

  • Step 3: Manual QA in WeChat DevTools

Open mini-program/unpackage/dist/build/mp-weixin.

Verify:

  • App opens on 爽文生成.

  • Bottom nav can switch to 人生轨迹.

  • 人生轨迹 existing list and actions still work.

  • 我的剧本 opens an existing script library/list.

  • Inspiration card fills the wish input.

  • 换一换 refreshes inspiration cards.

  • Text submit shows 心愿实现中…….

  • Success result shows title, tags, body, close icon, and action buttons.

  • Close icon returns to the input page and does not delete the script.

  • Existing script detail page still opens.

  • TTS control appears for generated scripts with an id.

  • No touched label appears as mojibake.

  • Voice unavailable fallback returns to a usable text-input state.

  • Long generated content scrolls and action buttons remain reachable.

  • Step 4: Capture final diff review

Run:

git diff -- mini-program/src/pages/main/index.vue mini-program/src/pages/main/ScriptView.vue mini-program/src/pages/main/ScriptDetailView.vue mini-program/src/stores/app.js mini-program/src/services/analytics.js

Check specifically for:

  • accidental deletion behavior on close icon,

  • duplicate audio players,

  • old mode state still controlling the first screen,

  • visible mojibake in touched UI strings,

  • missing analytics terminal events.

  • Step 5: Commit implementation

After code and QA pass:

git add mini-program/src/pages/main/index.vue mini-program/src/pages/main/ScriptView.vue mini-program/src/pages/main/ScriptDetailView.vue mini-program/src/stores/app.js mini-program/src/services/analytics.js
git commit -m "feat: redesign mini program script home"

Only add files actually modified.