Files
sbnews/docs/superpowers/plans/2026-08-02-community-paid-post-detail-refresh.md
T

357 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Community Paid Post Detail Refresh 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:** 将社区付费帖子详情页高度还原到设计稿,并修复免费预览字数与两阶段积分解锁链路。
**Architecture:** 后端继续通过 `/api/community/postDetail``/api/community/purchasePost` 提供统一契约,仅修正锁定正文的预览长度。uni-app 详情页在单个页面组件内重组视觉层级,复用现有社区积分、关注、点赞、评论、分享和购买 API,不新增路由或第三方依赖。
**Tech Stack:** PHP 8 / ThinkPHP、uni-app、Vue 3 `<script setup>`、TypeScript、SCSS、uView 图标、Git、项目部署脚本。
---
## 工作区保护
- 当前工作区已有用户改动,尤其是 `uniapp/src/api/community.ts`、社区列表相关文件、`docs/业务进度管理.md``docs/设计稿映射.md``.playwright-mcp/`
- 只编辑本计划明确列出的源码;提交时逐个指定路径,禁止 `git add .`
- `docs/业务进度管理.md``docs/设计稿映射.md` 只追加本任务记录,不把原有未提交改动纳入源码提交。
- 用户没有要求构建或自动化测试,因此不执行 `npm run build:h5`、PHP 测试或 QA 测试;只进行代码复核、页面热更新对比和项目要求的 server 部署。
### Task 1: 修复后端免费预览契约
**Files:**
- Modify: `server/app/api/controller/CommunityController.php:85-97`
- [ ] **Step 1: 将硬编码 20 字符替换为帖子配置值**
在锁定分支中使用以下逻辑:
```php
$previewLength = (int) ($data['free_content_len'] ?? 100);
if ($previewLength <= 0) {
$previewLength = 100;
}
$previewLength = max(20, min(500, $previewLength));
if (mb_strlen($data['content']) > $previewLength) {
$data['content'] = mb_substr($data['content'], 0, $previewLength) . '...';
}
```
保留“锁定状态最多返回第一张图片”的现有逻辑,不改变已购买、VIP 免费和作者本人的全文权限。
同时将特朗普译文生成与 `translated_content` 返回移动到付费权限判断之后:仅当 `is_locked=false` 时生成或返回译文,防止锁定用户通过译文绕过付费权限。
- [ ] **Step 2: 静态复核后端契约**
确认以下条件同时成立:
```text
postDetail 仍是免登录接口
锁定用户只能拿到免费预览
free_content_len 缺失或非法时回落到 100
free_content_len 最终限制在 20500
purchasePost 的 confirm=0/1 契约不变
锁定帖子不会生成或返回 translated_content
```
建议但本次不自动执行:
```powershell
php -l server/app/api/controller/CommunityController.php
```
预期:`No syntax errors detected`
- [ ] **Step 3: 单独提交后端修改**
```powershell
git add -- server/app/api/controller/CommunityController.php
git commit -m "fix: honor paid post preview length"
```
### Task 2: 重构详情页数据状态与解锁流程
**Files:**
- Modify: `uniapp/src/packages_community/pages/post_detail.vue:179-635`
- [ ] **Step 1: 复用积分统计接口并新增页面状态**
在社区 API 导入中加入已有的 `getCommunityUserStats`,并新增以下状态:
```ts
const detailError = ref(false)
const commentError = ref(false)
const previewExpanded = ref(false)
const userPoints = ref<number | null>(null)
const pointsLoading = ref(false)
const likePending = ref(false)
const commentLikePending = ref<Set<number>>(new Set())
```
积分展示使用:
```ts
const userPointsText = computed(() => {
if (!getToken()) return '登录后查看'
if (pointsLoading.value) return '加载中'
return userPoints.value === null ? '--' : String(userPoints.value)
})
```
- [ ] **Step 2: 将免费预览拆分为标题和摘要**
添加纯函数和计算属性,优先按换行、首句、冒号和长度降级拆分:
```ts
const splitPostPreview = (value: unknown) => {
const content = String(value || '').replace(/\r\n/g, '\n').trim()
if (!content) return { title: '', summary: '' }
const lines = content.split('\n').map((line) => line.trim()).filter(Boolean)
if (lines.length > 1) {
return { title: lines[0], summary: lines.slice(1).join('\n') }
}
const sentence = content.match(/^(.+?[。!?.!?])\s*(.*)$/)
if (sentence && sentence[1].length <= 56) {
return { title: sentence[1], summary: sentence[2] || '' }
}
const colonIndex = content.search(/[:]/)
if (colonIndex >= 4 && colonIndex <= 36) {
return {
title: content.slice(0, colonIndex + 1),
summary: content.slice(colonIndex + 1).trim()
}
}
if (content.length > 42) {
return { title: content.slice(0, 42), summary: content.slice(42).trim() }
}
return { title: content, summary: '' }
}
const postPreview = computed(() => splitPostPreview(post.value?.content))
const previewCanExpand = computed(() => postPreview.value.summary.length > 90)
```
- [ ] **Step 3: 让详情和评论错误可恢复**
`fetchDetail()` 开始时清空 `detailError`,失败时设置为 `true`;特朗普翻译改为非阻塞触发,避免翻译请求阻塞首屏:
```ts
post.value = res
translatedText.value = res?.translated_content || ''
loading.value = false
if (isTrump && res?.content && !translatedText.value && !autoTranslateStarted.value) {
autoTranslateStarted.value = true
void requestTranslate()
}
```
`fetchComments()` 失败时设置 `commentError`,成功时清空,并保留正文正常展示。
- [ ] **Step 4: 使用两阶段购买流程**
`handlePurchase()` 改为:
```ts
const applyUnlockedContent = async (result: any) => {
if (!post.value || !result?.content) return
post.value.content = result.content
post.value.is_locked = false
post.value.is_purchased = true
if (!result.from_cache) {
post.value.paid_count = Number(post.value.paid_count || 0) + 1
}
previewExpanded.value = true
await loadUserPoints()
}
const handlePurchase = async () => {
if (!checkLogin() || purchasing.value || !post.value) return
purchasing.value = true
try {
const probe = await purchasePost({ post_id: postId.value, confirm: 0 })
if (probe?.content) {
await applyUnlockedContent(probe)
return
}
if (!probe?.needs_payment) return
uni.showModal({
title: '确认解锁',
content: `确定使用 ${probe.cost || post.value.price_points || 0} 积分解锁该内容吗?`,
confirmText: '确认解锁',
confirmColor: '#1468f5',
success: async (modalResult) => {
if (!modalResult.confirm) return
purchasing.value = true
try {
const paidResult = await purchasePost({ post_id: postId.value, confirm: 1 })
await applyUnlockedContent(paidResult)
uni.showToast({ title: '解锁成功', icon: 'success' })
} finally {
purchasing.value = false
}
}
})
} finally {
purchasing.value = false
}
}
```
实现时确保弹窗确认后的异步失败仍由请求层提示,并避免外层 `finally` 抢先影响确认按钮状态。
- [ ] **Step 5: 防止点赞计数异常**
帖子点赞与评论点赞均增加进行中保护,并统一使用非负数:
```ts
const nextLikeCount = (count: unknown, liked: boolean) =>
Math.max(0, Number(count || 0) + (liked ? 1 : -1))
```
评论成功后 `await fetchComments()`,再令 `post.value.comment_count = commentTotal.value`,避免本地累计与接口总数不一致。
### Task 3: 高度还原帖子详情视觉
**Files:**
- Modify: `uniapp/src/packages_community/pages/post_detail.vue:1-177`
- Modify: `uniapp/src/packages_community/pages/post_detail.vue:637-1240`
- [ ] **Step 1: 重构顶部导航**
模板固定显示“帖子详情”,并在右侧复用分享弹窗:
```vue
<view class="detail-topbar__nav">
<view class="detail-topbar__action detail-topbar__action--back" @tap="goBack">
<u-icon name="arrow-left" size="40" color="#111111" />
</view>
<text class="detail-topbar__title">帖子详情</text>
<view class="detail-topbar__action" @tap="handleShare">
<u-icon name="share" size="42" color="#111111" />
</view>
</view>
```
- [ ] **Step 2: 合并作者与内容卡**
使用 `detail-post-card` 承载作者、关注按钮、付费标签、标题和摘要。摘要通过 class 控制三行收起:
```vue
<text
v-if="postPreview.summary"
class="detail-post-card__summary"
:class="{ 'detail-post-card__summary--collapsed': previewCanExpand && !previewExpanded }"
>
{{ postPreview.summary }}
</text>
<text v-if="previewCanExpand" class="detail-post-card__expand" @tap="previewExpanded = !previewExpanded">
{{ previewExpanded ? '收起' : '展开' }}
</text>
```
关注按钮使用描边样式;已关注状态使用浅蓝底。现有私聊入口只在已关注时以紧凑次按钮保留。
- [ ] **Step 3: 重建解锁卡**
解锁卡包含锁图形、积分信息、积分明细和说明:
```vue
<view v-if="post.is_locked" class="detail-unlock-card">
<view class="detail-unlock-card__lock"><u-icon name="lock" size="58" color="#1468f5" /></view>
<view class="detail-unlock-card__content">
<text class="detail-unlock-card__title">解锁全文</text>
<view class="detail-unlock-card__points">
<text> </text><text class="is-primary">{{ post.price_points }} 积分</text>
<text class="detail-unlock-card__dot">·</text>
<text>当前余额 </text><text class="is-primary">{{ userPointsText }}</text>
</view>
<text class="detail-unlock-card__link" @tap="openPoints">积分明细 </text>
<view class="detail-unlock-card__btn" :class="{ 'is-disabled': purchasing }" @tap="handlePurchase">
<text>{{ purchasing ? '解锁中' : '确认解锁' }}</text>
</view>
</view>
<view class="detail-unlock-card__notice">
<u-icon name="info-circle" size="26" color="#7c8798" />
<text>解锁后永久可读内容一经解锁不支持退回积分</text>
</view>
</view>
```
视觉值采用浅蓝渐变、`24rpx` 圆角、蓝色渐变主按钮和足够的卡内留白,430px H5 下不横向溢出。
- [ ] **Step 4: 重构互动与评论区**
互动标题和三项数据同行,评论项恢复头像并将点赞、回复、删除放到元信息行。底部输入栏继续固定在 flex 布局末端,`scroll-view` 使用 `flex: 1; min-height: 0`
- [ ] **Step 5: 增加加载失败与评论重试状态**
详情失败区提供“重新加载”和“返回”按钮;评论失败区提供“重新加载评论”。按钮分别调用 `fetchDetail(postId)``fetchComments()`
- [ ] **Step 6: 提交 uni-app 页面修改**
```powershell
git add -- uniapp/src/packages_community/pages/post_detail.vue
git commit -m "feat: refresh paid post detail experience"
```
### Task 4: 更新项目记录并进行页面对比复核
**Files:**
- Modify: `docs/业务进度管理.md`
- Modify: `docs/设计稿映射.md`
- Existing: `docs/superpowers/plans/2026-08-02-community-paid-post-detail-refresh.md`
- [ ] **Step 1: 追加业务进度记录**
记录以下内容:付费详情视觉还原、两阶段解锁、积分余额、免费预览长度、错误状态和涉及文件。不覆盖文档中现有未提交改动。
- [ ] **Step 2: 更新设计稿映射**
新增或更新“社区帖子详情”映射到 `uniapp/src/packages_community/pages/post_detail.vue`,状态标记为“代码已修改,待构建验收”或与实际复核结果一致。
- [ ] **Step 3: 使用现有 H5 热更新页面进行截图对比**
重新加载:
```text
http://localhost:5178/packages_community/pages/post_detail?id=8
```
检查固定标题、分享入口、作者内容卡、解锁卡、互动区、评论头像、底部输入栏和 430px 无横向溢出。此步骤不运行 H5 构建。
- [ ] **Step 4: 审查改动范围**
```powershell
git diff --check
git status --short
git log -3 --oneline
```
只审查本任务文件;`.playwright-mcp/` 和用户已有社区改动不得纳入提交。
### Task 5: 同步 server 修改到测试服
**Files:**
- Deploy source commit containing `server/app/api/controller/CommunityController.php`
- [ ] **Step 1: 执行项目标准部署脚本**
```powershell
Set-Location D:\www\gs-sport-era
powershell -ExecutionPolicy Bypass -File .\scripts\deploy-server.ps1 -Sudo -AutoIncremental -AutoIncrementalCommits 3 -RemoteDir /www/wwwroot/test-server.sbnews.net
```
预期:脚本将已提交的 server 运行源码同步到 `/www/wwwroot/test-server.sbnews.net`,不部署到旧目录。
- [ ] **Step 2: 交付说明**
列出已提交的后端与 uni-app 提交、部署结果、未执行的构建/自动化测试,以及保留在工作区的用户原有改动。