fix-discover-activity-list.md 6.6 KB

修复报告:发现活动列表 HTML 标签外露 + 返回详情页后列表翻倍

  • 日期:2026-10-24
  • 模块:cfc-frontend(uni-app / Vue2 微信小程序)
  • 定位说明:用户线索提示在 pages/discover/index.vue 或其 components,代码实际落地在 pages/activity/index.vue 的「发现活动」tab(getActivityList 接口 + 活动卡片),已按代码实际为准处理。

一、涉及文件清单

文件 是否改动 说明
cfc-frontend/pages/activity/index.vue ✅ 改动 两个 bug 的唯一文件
cfc-frontend/pages/activity/activity-detail/activity-detail.vue 已确认无需改动 详情页用 <mp-html :content="activity.description"> 渲染富文本(见 line 108-110),本来就是富文本正确渲染路径,不受本次改动影响
cfc-frontend/pages/guide/activities/index.vue 未改动(本次范围外) 该页 act.description 同样直出,但属于规划师「活动管理」列表而非发现活动列表;如需处理可另行跟进
cfc-frontend/components/DimensionActivities.vue 未改动 仅展示标题/时间/状态/价格,不展示正文富文本,无此问题

二、Bug 1:卡片摘要直接展示 HTML 标签/实体

根因

发现活动 tab 的卡片描述(pages/activity/index.vue 活动卡片模板)原来直接绑定后端返回的富文本字段:

<text class="card-desc" v-if="act.intro || act.description">{{ act.intro || act.description }}</text>

后端 description/intro 是富文本(含 <p>、<div>、<br>、&nbsp;、&#x27; 等标签与实体),小程序 <text> 不会解析 HTML,于是标签与实体按字面显示。此前没有做任何纯文本化处理。

改动

  1. 模板改为经 plainText() 处理后输出:

    <text class="card-desc" v-if="act.intro || act.description">{{ plainText(act.intro || act.description) }}</text>
    
  2. 新增 plainText(html) 方法(与 repo 内 pages/mind-detail/articles.vue 的 stripHtml 实现口径保持一致并增强):

    • 先去 <style>/<script> 块,再去所有 HTML 标签;
    • 解码常见命名实体(&nbsp;、&lt;、&gt;、&amp;、&quot;);
    • 解码数字实体(&#x27; → '、&#128197; → 📅 等 &#x / &# 形式,用 String.fromCharCode);
    • 归一化连续空白后 trim。
  3. 详情页无需改动:原本即用 <mp-html> 渲染富文本,保持正常。

三、Bug 2:进入详情返回后列表翻倍(重复追加)

根因

页面数据加载链:

onLoad → loadData() → loadDiscoverActivities()(无 refresh 参数 → 非 replace 模式)
onShow(_onLoadFired 已消费)→ loadData() → loadDiscoverActivities()(同样无 refresh)

loadDiscoverActivities(refresh) 只在 refresh === true 时 discoverPage = 1; discoverActivities = [] 并整体替换,否则走 concat 追加。onShow 里调用 loadData() 又不带 refresh 参数:

  1. 页面状态不重置:discoverActivities 原封不动,discoverPage 保持当前值;
  2. 并发竞态叠加:若用户之前下拉加载过分页,返回时可能仍有旧请求在飞,onShow 再发的请求与旧请求都走 concat,最终列表条目翻倍(同样活动重复出现一次)。

即「页面重新可见(onShow)再次调用加载,但没有重置页码或清空列表」。

改动(均只动 pages/activity/index.vue)

  1. loadData 增加 refresh 形参并透传给 loadMyActivities/loadDiscoverActivities/loadHistoryActivities;manage 分支本身是全量替换,不需要该参数。
  2. onShow 的 else 分支改传 this.loadData(true)——从子页返回时按「下拉刷新」语义重置(page=1、列表清空、整体替换),首页分支仍保持 loadData()(首次列表为空,语义不变)。
  3. 增强竞态防护(防止翻倍的第二个诱因):
    • 三个 loadXxxActivities 各自维护请求序号 _mineReqSeq / _discoverReqSeq / _historyReqSeq,then/catch 里校验序号,过期响应直接丢弃,不做 concat;
    • 三个 load* 改为 return promise,未登录等早退路径返回 Promise.resolve()——原来它们返回 undefined,而 onRefresh 依赖 .finally() 复位 isRefreshing,缺返回会让下拉刷新卡死。

四、是否影响原有翻页与刷新行为

  • 下拉加载更多(onLoadMore):仍走「翻页 + concat」路径,未改;序号机制只丢弃过期响应,不影响正常追加。
  • 下拉刷新(onRefresh):仍走 refresh=true 重置语义;因 load* 补齐 promise 返回,.finally 行为比修复前更可靠(不再有 undefined.finally 运行时报错导致 isRefreshing 卡在 true 的隐患)。
  • tab 切换(onTabChange)/筛选(onMineFilterChange/onDimensionChange)/switchToDiscover:原本就手动重置列表+页码后调用,不受影响。
  • 首次进入:onLoad 仍是 loadData(),无重复请求;_onLoadFired 双发防护逻辑不变。
  • 登录态早退(currentChildId 为空):改为返回 resolved promise,行为与原来直接 return 一致,仅多一个 promise 让 .finally 可复用。

五、建议的回归验证步骤

  1. 列表摘要纯净文本:进入「活动中心 → 发现活动」tab,检查卡片描述区不再出现 p / div / br / &nbsp; / &#x27; 等字面量,只显示纯文本(含中文标点、emoji 等数字实体被正确解码)。用一条已知含富文本正文的活动核对。
  2. 详情页富文本正常:点同一活动进入详情页,确认 description 仍以 mp-html 富文本渲染(加粗/换行/图片不丢),未被改动破坏。
  3. 返回不翻倍:
    • 进入发现活动列表,记录当前条目数(或直接目视);
    • 点击任一活动进入详情页,按左上角返回(或用安卓物理返回键);
    • 确认列表条目数与进入前一致,无同一活动重复出现;
    • 连续「进入详情 → 返回」三次,条目数量保持稳定。
  4. 分页加载更多仍可用:活动数量 ≥ 10(pageSize)时,在发现活动/我的活动/历史记录三个 tab 分别滚动到底触发加载更多,确认追加正常、无重复。
  5. 下拉刷新可用:在上述 tab 触发下拉刷新,确认列表正常刷新、loading 态正常关闭(不会卡在刷新中)。
  6. 其他入口不受影响:我的活动签到/取消报名后的 loadMyActivities(true);切维度筛选;switchToDiscover 空态按钮跳转,均保持原行为。