|
|
@@ -0,0 +1,543 @@
|
|
|
+# 文章阅读追踪增强 实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 完善文章阅读追踪——前端列表/详情页显示阅读数量,匿名用户不显示阅读时长;后端新增 admin 接口按文章查询阅读记录(含昵称和时长),供后台界面展示。
|
|
|
+
|
|
|
+**架构:** 现有 `article_reading_records` 表已有 `article_id` 和累加逻辑,本次补充:1) `reportReadingTime` 首次上报时递增 `articles.view_count`(覆盖匿名/登录用户);2) 新增 `POST /api/admin/articles/reading-records` 返回每篇文章的阅读用户明细(含昵称);3) 前端列表/详情页补上阅读数显示;4) 后台弹窗展示阅读记录查询界面。
|
|
|
+
|
|
|
+**技术栈:** Spring Boot 2.7 + MyBatis-Plus / uni-app Vue 2 小程序 / Vue 2 + Element UI 管理端
|
|
|
+
|
|
|
+**设计文档:** `docs/superpowers/specs/2026-07-28-article-reading-time-tracking-design.md`
|
|
|
+
|
|
|
+## 文件变更清单
|
|
|
+
|
|
|
+| 文件 | 变更类型 | 说明 |
|
|
|
+|------|---------|------|
|
|
|
+| `cfc-backend/.../service/ArticleService.java` | 修改 | `reportReadingTime` 首次上报时递增 `view_count`;新增 `getArticleReadingRecords` |
|
|
|
+| `cfc-backend/.../controller/admin/AdminArticleController.java` | 修改 | 新增 `POST /api/admin/articles/reading-records` |
|
|
|
+| `cfc-backend/.../mapper/ArticleReadingRecordMapper.java` | 修改 | 新增 `selectByArticle` 查询方法 |
|
|
|
+| `docs/superpowers/api/API_REFERENCE.md` | 修改 | 新增 `POST /api/admin/articles/reading-records` 接口文档 |
|
|
|
+| `cfc-web/src/api/article.js` | 修改 | 新增 `adminArticleReadingRecords` 函数 |
|
|
|
+| `cfc-web/src/views/admin/ArticleManage.vue` | 修改 | 每行加"阅读记录"按钮 + 阅读记录弹窗 |
|
|
|
+| `cfc-frontend/pages/article-center/index.vue` | 修改 | 列表卡片底部显示阅读数 |
|
|
|
+| `cfc-frontend/pages/article-center/article-detail.vue` | 修改 | 详情页 meta 显示阅读数;匿名用户不启动计时器 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 1: 后端 — `reportReadingTime` 首次上报时递增 `view_count`
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java` (第 318-345 行)
|
|
|
+
|
|
|
+- [ ] **步骤 1:修改 `reportReadingTime` 方法,在 insert 路径递增 view_count**
|
|
|
+
|
|
|
+在 `ArticleService.reportReadingTime` 方法中,当创建新记录(else 分支)时,调用 `articleMapper` 递增 `view_count`:
|
|
|
+
|
|
|
+```java
|
|
|
+public void reportReadingTime(Long articleId, Long userId, Long memberId, int durationSeconds) {
|
|
|
+ if (memberId == null) return;
|
|
|
+
|
|
|
+ // 查找当日已有记录(同文章+同孩子)
|
|
|
+ LambdaQueryWrapper<ArticleReadingRecord> wrapper = new LambdaQueryWrapper<ArticleReadingRecord>()
|
|
|
+ .eq(ArticleReadingRecord::getArticleId, articleId)
|
|
|
+ .eq(ArticleReadingRecord::getChildId, memberId)
|
|
|
+ .apply("DATE(read_at) = CURDATE()")
|
|
|
+ .last("LIMIT 1");
|
|
|
+ ArticleReadingRecord existing = articleReadingRecordMapper.selectOne(wrapper);
|
|
|
+
|
|
|
+ if (existing != null) {
|
|
|
+ existing.setDurationSeconds(existing.getDurationSeconds() + durationSeconds);
|
|
|
+ existing.setUpdatedAt(new Date());
|
|
|
+ articleReadingRecordMapper.updateById(existing);
|
|
|
+ } else {
|
|
|
+ ArticleReadingRecord record = new ArticleReadingRecord();
|
|
|
+ record.setArticleId(articleId);
|
|
|
+ record.setUserId(userId);
|
|
|
+ record.setChildId(memberId);
|
|
|
+ record.setDurationSeconds(durationSeconds);
|
|
|
+ record.setReadAt(new Date());
|
|
|
+ record.setCreatedAt(new Date());
|
|
|
+ articleReadingRecordMapper.insert(record);
|
|
|
+ // 首次阅读:递增文章 view_count(匿名或登录用户均计)
|
|
|
+ articleMapper.update(null, Wrappers.<Article>lambdaUpdate()
|
|
|
+ .eq(Article::getId, articleId)
|
|
|
+ .setSql("view_count = view_count + 1"));
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+注意:`articleMapper` 已通过 `@Resource` 注入,`Wrappers` 已通过 `import com.baomidou.mybatisplus.core.toolkit.Wrappers` 引入(ArticleService 已有该 import)。
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+
|
|
|
+预期输出:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java && \
|
|
|
+ git commit -m "feat(article): reportReadingTime 首次上报时递增 view_count"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 2: 后端 — 新增文章阅读记录查询 API + 接口文档
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleReadingRecordMapper.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java`
|
|
|
+- 修改:`docs/superpowers/api/API_REFERENCE.md`
|
|
|
+
|
|
|
+- [ ] **步骤 1:新增 Mapper 查询方法**
|
|
|
+
|
|
|
+在 `ArticleReadingRecordMapper.java` 末尾新增方法(替换现有内容):
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.mapper;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import com.etotem.cfc.entity.ArticleReadingRecord;
|
|
|
+import org.apache.ibatis.annotations.Mapper;
|
|
|
+import org.apache.ibatis.annotations.Param;
|
|
|
+import org.apache.ibatis.annotations.ResultMap;
|
|
|
+import org.apache.ibatis.annotations.Select;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface ArticleReadingRecordMapper extends BaseMapper<ArticleReadingRecord> {
|
|
|
+
|
|
|
+ @Select("SELECT COALESCE(SUM(duration_seconds), 0) FROM article_reading_records " +
|
|
|
+ "WHERE child_id = #{childId} AND read_at >= #{since}")
|
|
|
+ Integer selectTotalDurationByChildSince(@Param("childId") Long childId,
|
|
|
+ @Param("since") java.util.Date since);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按文章 ID 分页查询阅读记录,JOIN family_members 获取昵称,按最后阅读时间倒序
|
|
|
+ */
|
|
|
+ @Select("SELECT " +
|
|
|
+ " r.id, " +
|
|
|
+ " r.user_id, " +
|
|
|
+ " r.child_id, " +
|
|
|
+ " r.duration_seconds, " +
|
|
|
+ " r.read_at, " +
|
|
|
+ " r.updated_at, " +
|
|
|
+ " m.nickname, " +
|
|
|
+ " DATE(r.read_at) AS read_date " +
|
|
|
+ "FROM article_reading_records r " +
|
|
|
+ "LEFT JOIN family_members m ON r.child_id = m.id " +
|
|
|
+ "WHERE r.article_id = #{articleId} " +
|
|
|
+ "ORDER BY r.updated_at DESC, r.read_at DESC " +
|
|
|
+ "LIMIT #{limit} OFFSET #{offset}")
|
|
|
+ @ResultMap("readRecordResultMap")
|
|
|
+ List<Map<String, Object>> selectByArticle(
|
|
|
+ @Param("articleId") Long articleId,
|
|
|
+ @Param("offset") int offset,
|
|
|
+ @Param("limit") int limit);
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 统计某文章总阅读记录数(用于分页)
|
|
|
+ */
|
|
|
+ @Select("SELECT COUNT(*) FROM article_reading_records WHERE article_id = #{articleId}")
|
|
|
+ long countByArticle(@Param("articleId") Long articleId);
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:新增 Service 方法**
|
|
|
+
|
|
|
+在 `ArticleService.java` 中新增方法(放在 `reportReadingTime` 方法之后):
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 按文章 ID 分页查询阅读记录,返回用户昵称、时长、最后阅读时间
|
|
|
+ */
|
|
|
+public Page<Map<String, Object>> getArticleReadingRecords(Long articleId, int page, int size) {
|
|
|
+ long total = articleReadingRecordMapper.countByArticle(articleId);
|
|
|
+ if (total == 0) {
|
|
|
+ Page<Map<String, Object>> empty = new Page<>(page, size);
|
|
|
+ empty.setTotal(0);
|
|
|
+ empty.setRecords(Collections.emptyList());
|
|
|
+ return empty;
|
|
|
+ }
|
|
|
+ int offset = (page - 1) * size;
|
|
|
+ List<Map<String, Object>> records = articleReadingRecordMapper.selectByArticle(articleId, offset, size);
|
|
|
+ Page<Map<String, Object>> result = new Page<>(page, size);
|
|
|
+ result.setTotal(total);
|
|
|
+ result.setRecords(records);
|
|
|
+ return result;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+注意:`Page` 来自 `com.baomidou.mybatisplus.extension.plugins.pagination.Page`,`Collections` 来自 `java.util.Collections`,均已可复用。
|
|
|
+
|
|
|
+- [ ] **步骤 3:新增 Admin Controller 端点**
|
|
|
+
|
|
|
+在 `AdminArticleController.java` 末尾的 `categoryDelete` 方法之前插入:
|
|
|
+
|
|
|
+```java
|
|
|
+ @PostMapping("/reading-records")
|
|
|
+ public Result<Page<Map<String, Object>>> readingRecords(
|
|
|
+ @RequestBody Map<String, Object> body) {
|
|
|
+ Long articleId = ParamUtils.getLong(body.get("articleId"));
|
|
|
+ if (articleId == null) return Result.error("articleId不能为空");
|
|
|
+ int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
|
|
|
+ int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 20;
|
|
|
+ return Result.success(articleService.getArticleReadingRecords(articleId, page, size));
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:更新 API_REFERENCE.md**
|
|
|
+
|
|
|
+在 `docs/superpowers/api/API_REFERENCE.md` 的"📰 文章管理 API"章节(搜索 `## 📰 文章管理 API` 或类似标题)末尾追加:
|
|
|
+
|
|
|
+```markdown
|
|
|
+### 文章阅读记录
|
|
|
+
|
|
|
+| 方法 | 路径 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| POST | `/api/admin/articles/reading-records` | 按文章查询阅读记录明细 |
|
|
|
+
|
|
|
+**请求体:**
|
|
|
+```json
|
|
|
+{ "articleId": 123, "page": 1, "size": 20 }
|
|
|
+```
|
|
|
+
|
|
|
+**响应 data 字段(分页):**
|
|
|
+```json
|
|
|
+{
|
|
|
+ "total": 5,
|
|
|
+ "records": [
|
|
|
+ {
|
|
|
+ "id": 10,
|
|
|
+ "user_id": 42,
|
|
|
+ "child_id": 88,
|
|
|
+ "nickname": "小明",
|
|
|
+ "duration_seconds": 450,
|
|
|
+ "read_at": "2026-08-30T10:30:00",
|
|
|
+ "updated_at": "2026-08-30T10:30:00",
|
|
|
+ "read_date": "2026-08-30"
|
|
|
+ }
|
|
|
+ ]
|
|
|
+}
|
|
|
+```
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 6:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleReadingRecordMapper.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java \
|
|
|
+ docs/superpowers/api/API_REFERENCE.md && \
|
|
|
+ git commit -m "feat(article): 新增文章阅读记录 admin API"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 3: cfc-web 管理端 — 阅读记录弹窗
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-web/src/api/article.js`
|
|
|
+- 修改:`cfc-web/src/views/admin/ArticleManage.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:新增 API 封装函数**
|
|
|
+
|
|
|
+在 `cfc-web/src/api/article.js` 末尾追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+// ===== 文章阅读记录 =====
|
|
|
+export function adminArticleReadingRecords(data) {
|
|
|
+ return request({ url: '/api/admin/articles/reading-records', method: 'post', data })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:修改 ArticleManage.vue**
|
|
|
+
|
|
|
+**a) data 中新增状态变量**(`rejectDialogVisible` 之后):
|
|
|
+
|
|
|
+```javascript
|
|
|
+ rejectDialogVisible: false,
|
|
|
+ rejectReason: '',
|
|
|
+ currentAuditRow: null,
|
|
|
+ // 阅读记录弹窗
|
|
|
+ readRecordVisible: false,
|
|
|
+ readRecordArticleId: null,
|
|
|
+ readRecordList: [],
|
|
|
+ readRecordTotal: 0,
|
|
|
+ readRecordPage: 1,
|
|
|
+ readRecordSize: 20,
|
|
|
+ readRecordLoading: false
|
|
|
+```
|
|
|
+
|
|
|
+**b) template:在"操作"列的操作按钮之后、`rejectDialog` 之前新增阅读记录弹窗**
|
|
|
+
|
|
|
+在 `<el-dialog title="驳回原因" ...>` 之前插入:
|
|
|
+
|
|
|
+```html
|
|
|
+<!-- 阅读记录弹窗 -->
|
|
|
+<el-dialog title="文章阅读记录" :visible.sync="readRecordVisible" width="800px" destroy-on-close>
|
|
|
+ <div v-loading="readRecordLoading" style="min-height: 200px;">
|
|
|
+ <el-table :data="readRecordList" border stripe size="small">
|
|
|
+ <el-table-column prop="nickname" label="孩子昵称" width="120" />
|
|
|
+ <el-table-column prop="duration_seconds" label="累计时长(秒)" width="120">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ formatDuration(row.duration_seconds) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="read_date" label="最后阅读日期" width="130">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ row.read_date || '-' }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="read_at" label="最后阅读时间" width="170">
|
|
|
+ <template slot-scope="{ row }">
|
|
|
+ {{ formatTime(row.read_at) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ <el-pagination
|
|
|
+ v-if="readRecordTotal > 0"
|
|
|
+ layout="total, prev, pager, next"
|
|
|
+ :current-page="readRecordPage"
|
|
|
+ :page-size="readRecordSize"
|
|
|
+ :total="readRecordTotal"
|
|
|
+ @current-change="onReadRecordPageChange"
|
|
|
+ style="margin-top: 16px; text-align: right;"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <span slot="footer">
|
|
|
+ <el-button @click="readRecordVisible = false">关闭</el-button>
|
|
|
+ </span>
|
|
|
+</el-dialog>
|
|
|
+```
|
|
|
+
|
|
|
+**c) methods:新增方法**
|
|
|
+
|
|
|
+在现有 methods 末尾插入:
|
|
|
+
|
|
|
+```javascript
|
|
|
+ async openReadRecords(row) {
|
|
|
+ this.readRecordArticleId = row.id
|
|
|
+ this.readRecordPage = 1
|
|
|
+ this.readRecordList = []
|
|
|
+ this.readRecordTotal = 0
|
|
|
+ this.readRecordVisible = true
|
|
|
+ await this.loadReadRecords()
|
|
|
+ },
|
|
|
+ async loadReadRecords() {
|
|
|
+ this.readRecordLoading = true
|
|
|
+ try {
|
|
|
+ const res = await adminArticleReadingRecords({
|
|
|
+ articleId: this.readRecordArticleId,
|
|
|
+ page: this.readRecordPage,
|
|
|
+ size: this.readRecordSize
|
|
|
+ })
|
|
|
+ if (res.data) {
|
|
|
+ this.readRecordList = res.data.records || []
|
|
|
+ this.readRecordTotal = res.data.total || 0
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ this.$message.error('加载阅读记录失败')
|
|
|
+ } finally {
|
|
|
+ this.readRecordLoading = false
|
|
|
+ }
|
|
|
+ },
|
|
|
+ onReadRecordPageChange(page) {
|
|
|
+ this.readRecordPage = page
|
|
|
+ this.loadReadRecords()
|
|
|
+ },
|
|
|
+ formatDuration(seconds) {
|
|
|
+ if (!seconds) return '0秒'
|
|
|
+ var min = Math.floor(seconds / 60)
|
|
|
+ var sec = seconds % 60
|
|
|
+ if (min > 0 && sec > 0) return min + '分' + sec + '秒'
|
|
|
+ if (min > 0) return min + '分钟'
|
|
|
+ return sec + '秒'
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+**d) template:在操作列每个状态模板的"复制"按钮旁边新增"阅读记录"按钮**
|
|
|
+
|
|
|
+在每个状态的 `<el-button size="mini" type="primary">` 或操作区末尾插入(以发布中状态为例,其他状态按需添加):
|
|
|
+
|
|
|
+```html
|
|
|
+<el-button size="mini" @click="openReadRecords(row)">阅读记录</el-button>
|
|
|
+```
|
|
|
+
|
|
|
+注意:所有状态下(草稿箱、待审核、已驳回、发布中、已撤回、其他)都应有此按钮,统一放在操作区域。
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-web && npm run build
|
|
|
+```
|
|
|
+
|
|
|
+预期:构建成功(无 TypeScript/ESLint 错误)
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add \
|
|
|
+ cfc-web/src/api/article.js \
|
|
|
+ cfc-web/src/views/admin/ArticleManage.vue && \
|
|
|
+ git commit -m "feat(web): 文章管理页新增阅读记录弹窗"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 4: 小程序前端 — 阅读数显示 + 匿名用户处理
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/article-center/index.vue`
|
|
|
+- 修改: `cfc-frontend/pages/article-center/article-detail.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:列表页显示阅读数**
|
|
|
+
|
|
|
+在 `cfc-frontend/pages/article-center/index.vue` 的卡片 footer(第 105-110 行附近)中,在 `ac-card-fav` 后追加阅读数:
|
|
|
+
|
|
|
+将:
|
|
|
+```html
|
|
|
+ <text class="ac-card-fav">⭐ {{ item.favCount || 0 }}</text>
|
|
|
+```
|
|
|
+改为:
|
|
|
+```html
|
|
|
+ <text class="ac-card-fav">⭐ {{ item.favCount || 0 }}</text>
|
|
|
+ <text class="ac-card-sep">|</text>
|
|
|
+ <text class="ac-card-readcount">👁 {{ item.viewCount || 0 }}</text>
|
|
|
+```
|
|
|
+
|
|
|
+追加 CSS(在 `.ac-card-fav` 样式块之后):
|
|
|
+```css
|
|
|
+.ac-card-readcount {
|
|
|
+ color: #ccc;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:详情页 meta 显示阅读数**
|
|
|
+
|
|
|
+在 `cfc-frontend/pages/article-center/article-detail.vue` 的 meta 行(第 16-24 行)中,在现有 meta 项之后追加:
|
|
|
+
|
|
|
+将:
|
|
|
+```html
|
|
|
+ <text class="reading-time-badge completed" v-else>✅ 阅读完成</text>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+改为:
|
|
|
+```html
|
|
|
+ <text class="reading-time-badge completed" v-else>✅ 阅读完成</text>
|
|
|
+ <text class="meta-sep">|</text>
|
|
|
+ <text class="meta-readcount">{{ article.viewCount || 0 }}人阅读</text>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+追加 CSS:
|
|
|
+```css
|
|
|
+.meta-readcount {
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #bbb;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:匿名用户不启动计时器**
|
|
|
+
|
|
|
+在 `article-detail.vue` 中,`startReadingTimer` 方法加一个匿名用户检查。当前 `startReadingTimer` 已有 `if (this.isTimerRunning || this.readingCompleted) return`,在其前追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+ startReadingTimer: function() {
|
|
|
+ var memberId = uni.getStorageSync('currentChildId')
|
|
|
+ if (!memberId) return
|
|
|
+ if (this.isTimerRunning || this.readingCompleted) return
|
|
|
+ // ... 原有逻辑
|
|
|
+```
|
|
|
+
|
|
|
+同样,在 `onShow` 和 `startNewReadingSession` 中也加入此检查,防止匿名用户启动计时:
|
|
|
+
|
|
|
+```javascript
|
|
|
+ onShow: function() {
|
|
|
+ var memberId = uni.getStorageSync('currentChildId')
|
|
|
+ if (!this.article || this.error || !memberId) return
|
|
|
+ this.startNewReadingSession()
|
|
|
+ },
|
|
|
+```
|
|
|
+
|
|
|
+同时,在模板中阅读时长徽章(第 22-23 行)已有 `v-if="!readingCompleted"` 条件,但匿名用户应完全不显示该徽章,改为:
|
|
|
+
|
|
|
+```html
|
|
|
+ <text class="reading-time-badge" v-if="!readingCompleted && currentChildId">{{ formatReadingTime(readingSeconds) }}</text>
|
|
|
+ <text class="reading-time-badge completed" v-else>✅ 阅读完成</text>
|
|
|
+```
|
|
|
+
|
|
|
+在 data 中新增一个计算属性或直接用已存储的值,这里利用已存在逻辑——匿名用户 `memberId` 为空时计时器不启动,`readingSeconds` 始终为 0,徽章本身也不应显示。最简单:在 data 中加一个 `currentChildId` 字段,或在模板中直接读取 storage:
|
|
|
+
|
|
|
+```html
|
|
|
+ <text class="reading-time-badge" v-if="!readingCompleted && !!currentChildId">{{ formatReadingTime(readingSeconds) }}</text>
|
|
|
+```
|
|
|
+
|
|
|
+> 注意:小程序模板中不能直接调用 `uni.getStorageSync`,需从 `data` 中取值。在 `onLoad` 中赋值:
|
|
|
+```javascript
|
|
|
+ onLoad(options) {
|
|
|
+ // ... 原有代码
|
|
|
+ this.currentChildId = uni.getStorageSync('currentChildId') || null
|
|
|
+ },
|
|
|
+```
|
|
|
+在 data 中加 `currentChildId: null`。
|
|
|
+
|
|
|
+- [ ] **步骤 4:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-frontend && node -e "require('fs').readFileSync('pages/article-center/index.vue','utf8')" && \
|
|
|
+ node -e "require('fs').readFileSync('pages/article-center/article-detail.vue','utf8')"
|
|
|
+```
|
|
|
+
|
|
|
+预期:文件内容可读,无语法错误。
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add \
|
|
|
+ cfc-frontend/pages/article-center/index.vue \
|
|
|
+ cfc-frontend/pages/article-center/article-detail.vue && \
|
|
|
+ git commit -m "feat(frontend): 文章列表/详情页显示阅读数,匿名用户不启动计时"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### Task 5: 规格文档同步更新 + 最终验证
|
|
|
+
|
|
|
+- [ ] **步骤 1:更新 PROJECT-OVERVIEW.md**
|
|
|
+
|
|
|
+在 `docs/superpowers/PROJECT-OVERVIEW.md` 的"4.1 文章与内容运营"章节追加新条目:
|
|
|
+
|
|
|
+```markdown
|
|
|
+| 文章阅读追踪增强(阅读数+后台查询) | Phase 6 | 🚧 实施中 | 本文档(2026-08-30) | `specs/2026-07-28-article-reading-time-tracking-design.md` + 本计划 |
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:最终编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile
|
|
|
+```
|
|
|
+
|
|
|
+预期:`BUILD SUCCESS`
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit 文档变更**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd /sc-data/cfc && git add docs/superpowers/PROJECT-OVERVIEW.md && \
|
|
|
+ git commit -m "docs: 更新 PROJECT-OVERVIEW.md 文章阅读追踪增强条目"
|
|
|
+```
|