feat: 小程序脚本首页重构 + 社交数据导入 + TTS 播放优化
- 后端:新增社交数据导入/审批/洞察生成 API(SocialContent/SocialInsight) - 后端:优化脚本上下文服务,TTS 服务增强 - 小程序:重构脚本首页布局,新增社交导入页面 - 小程序:新增 useTtsPlayer composable,移除旧 ScriptAudioPlayer 组件 - 小程序:新增社交导入服务,优化请求服务 - SQL:新增社交数据导入建表脚本 - 文档:补充设计文档和实施计划 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,771 @@
|
||||
# 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`:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```vue
|
||||
<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:
|
||||
|
||||
```vue
|
||||
<text>爽文生成</text>
|
||||
<text>人生轨迹</text>
|
||||
<text>我的</text>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Verify analytics initial page view**
|
||||
|
||||
Confirm existing code now sends:
|
||||
|
||||
```js
|
||||
analytics.trackPageView(pagePath, { tab: activeTab.value })
|
||||
```
|
||||
|
||||
with `tab: 'script'` on first load.
|
||||
|
||||
- [ ] **Step 5: Run build**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
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>`:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```text
|
||||
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`:
|
||||
|
||||
```vue
|
||||
<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**
|
||||
|
||||
```js
|
||||
const voiceCopy = computed(() => {
|
||||
if (voiceState.value === 'pressing') return '松开后开始实现心愿'
|
||||
if (voiceState.value === 'recognizing') return '正在识别你的心愿……'
|
||||
if (voiceState.value === 'error') return '语音暂不可用,可以先输入文字'
|
||||
return '按住说话,即刻如愿'
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add library navigation**
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```css
|
||||
.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:
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```css
|
||||
.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:
|
||||
|
||||
```powershell
|
||||
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**
|
||||
|
||||
```js
|
||||
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**
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```powershell
|
||||
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**
|
||||
|
||||
```js
|
||||
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**
|
||||
|
||||
```vue
|
||||
<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:
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```js
|
||||
import ScriptAudioPlayer from '../../components/ScriptAudioPlayer.vue'
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add result template with one TTS surface**
|
||||
|
||||
```vue
|
||||
<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**
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```css
|
||||
.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:
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```js
|
||||
analytics.track('script_inspiration_select', {
|
||||
source: 'recommendation'
|
||||
}, { eventType: 'script', pagePath })
|
||||
```
|
||||
|
||||
Ensure refresh tracks:
|
||||
|
||||
```js
|
||||
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:
|
||||
|
||||
```text
|
||||
script_wish_submit -> script_generate_success -> script_result_view
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```text
|
||||
script_wish_submit -> script_generate_fail
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run build**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```powershell
|
||||
cd mini-program
|
||||
npm run build:mp-weixin
|
||||
```
|
||||
|
||||
Expected: success.
|
||||
|
||||
- [ ] **Step 2: Check generated files exist**
|
||||
|
||||
Run:
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```powershell
|
||||
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:
|
||||
|
||||
```powershell
|
||||
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.
|
||||
Reference in New Issue
Block a user