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

13 KiB
Raw Blame History

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/业务进度管理.mddocs/设计稿映射.md.playwright-mcp/
  • 只编辑本计划明确列出的源码;提交时逐个指定路径,禁止 git add .
  • docs/业务进度管理.mddocs/设计稿映射.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 最终限制在 20500
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 提交、部署结果、未执行的构建/自动化测试,以及保留在工作区的用户原有改动。