13 KiB
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 字符替换为帖子配置值
在锁定分支中使用以下逻辑:
$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: 静态复核后端契约
确认以下条件同时成立:
postDetail 仍是免登录接口
锁定用户只能拿到免费预览
free_content_len 缺失或非法时回落到 100
free_content_len 最终限制在 20~500
purchasePost 的 confirm=0/1 契约不变
锁定帖子不会生成或返回 translated_content
建议但本次不自动执行:
php -l server/app/api/controller/CommunityController.php
预期:No syntax errors detected。
- Step 3: 单独提交后端修改
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,并新增以下状态:
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())
积分展示使用:
const userPointsText = computed(() => {
if (!getToken()) return '登录后查看'
if (pointsLoading.value) return '加载中'
return userPoints.value === null ? '--' : String(userPoints.value)
})
- Step 2: 将免费预览拆分为标题和摘要
添加纯函数和计算属性,优先按换行、首句、冒号和长度降级拆分:
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;特朗普翻译改为非阻塞触发,避免翻译请求阻塞首屏:
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() 改为:
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: 防止点赞计数异常
帖子点赞与评论点赞均增加进行中保护,并统一使用非负数:
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: 重构顶部导航
模板固定显示“帖子详情”,并在右侧复用分享弹窗:
<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 控制三行收起:
<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: 重建解锁卡
解锁卡包含锁图形、积分信息、积分明细和说明:
<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 页面修改
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 热更新页面进行截图对比
重新加载:
http://localhost:5178/packages_community/pages/post_detail?id=8
检查固定标题、分享入口、作者内容卡、解锁卡、互动区、评论头像、底部输入栏和 430px 无横向溢出。此步骤不运行 H5 构建。
- Step 4: 审查改动范围
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: 执行项目标准部署脚本
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 提交、部署结果、未执行的构建/自动化测试,以及保留在工作区的用户原有改动。