feat: refresh paid post detail experience

This commit is contained in:
hajimi
2026-08-02 01:31:20 +08:00
parent db181482c6
commit ea0561e758
2 changed files with 1384 additions and 802 deletions
@@ -0,0 +1,356 @@
# 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 提交、部署结果、未执行的构建/自动化测试,以及保留在工作区的用户原有改动。