2026-09-19-cognitive-game-assessment.md 35 KB

Seven-Dimension Cognitive Game Assessment — 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: Add a game-based cognitive assessment system that measures seven cognitive dimensions — perception, focus, memory, logic, spatial reasoning, processing speed, and language expression — via interactive mini-games in the mini-program, with scores written to the existing DanAssessmentResult entity.

Architecture: Seven standalone cognitive test pages live in pages/cognitive-test/, each implementing a distinct game type. Scores post to the existing POST /api/cognitive/self-test endpoint, which already handles both new-record creation and updates to DanAssessmentResult. One new database column (language_score) is added for the new language dimension; the other five dimensions already have columns. A central portal page (cognitive-hall.vue) lists all seven tests with completion status.

Tech Stack: Java 8 / Spring Boot 2.7.18 / MyBatis-Plus (backend); uni-app Vue 2 Options API (frontend); MySQL 8.0.

Spec: This plan implements the design described in the brainstorming session (2026-09-19): seven cognitive dimensions, one game per dimension, results written to existing dan_assessment_results via the existing self-test API. No new backend endpoints are required.

Global Constraints

  • Backend uses @PostMapping only — no @GetMapping, @PutMapping, @DeleteMapping.
  • All DI uses @Resource, field names must match default Bean Name.
  • All DB migrations are idempotent — wrapped in try-catch.
  • All DDL changes must be mirrored in schema.sql.
  • Frontend uses Vue 2 Options API — no Composition API.
  • Frontend cannot use optional chaining (?.), CSS Grid, or :key expressions — use methods instead.
  • All time display uses yyyy-MM-dd HH:mm:ss (full) or yyyy-MM-dd (date only).
  • Page titles must be set in pages.json via navigationBarTitleText, not in templates.
  • Existing 5 cognitive test pages (perception-test, logic-test, memory-test, spatial-test, speed-test) must remain untouched.
  • The new language-test page follows the same pattern as memory-test.vue (intro → play → result → submit).

File Map

Action Path Responsibility
Modify cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java Add languageScore field
Modify cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java Add languageScore case to submitSelfTestScore() switch
Modify cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java Add migration 323 to add language_score column
Modify cfc-backend/src/main/resources/schema.sql Add language_score column to dan_assessment_results CREATE TABLE
Create cfc-frontend/pages/cognitive-test/language-test.vue Language expression game (4 questions, word-relation matching)
Modify cfc-frontend/pages.json Register language-test page in the existing pages/cognitive-test subpackage
Create cfc-frontend/pages/cognitive-hall.vue Central portal listing all 7 tests with completion status
Modify cfc-frontend/pages/wisdom/cognitive-report.vue Add languageScore dimension row to the report bar chart

Task 1: Backend — Entity + Migration + Service

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java
  • Modify: cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java
  • Modify: cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  • Modify: cfc-backend/src/main/resources/schema.sql

Interfaces:

  • Consumes: existing DanAssessmentResult fields and CognitiveService.submitSelfTestScore() method
  • Produces: new languageScore field on entity; migration 323 that is idempotent

  • [ ] Step 1: Add languageScore to DanAssessmentResult

In cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java, add after the existing processingSpeedScore field (around line 46):

private Integer languageScore;  // 语言表达得分 (0-100)

The field goes between processingSpeedScore (line 46) and the // === EMI 心理子维度 comment (line 48).

  • Step 2: Add languageScore case to CognitiveService.submitSelfTestScore()

In cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java, within the switch (dimension) block starting around line 117, add before the default: case:

case "languageScore":
    latest.setLanguageScore(score);
    break;

Also update the overall score calculation loop (lines 139–144) to include languageScore:

for (Integer s : Arrays.asList(
        latest.getAttentionScore(), latest.getFocusScore(),
        latest.getMemoryScore(), latest.getLogicScore(),
        latest.getPerceptionScore(), latest.getSpatialScore(),
        latest.getProcessingSpeedScore(), latest.getLanguageScore())) {
  • Step 3: Add migration 323 to DatabaseInitializer.java

In cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java, insert before the closing } of the runMigrations() method (which ends at line 11133, right before line 11135's /**).

Add these lines (use the exact pattern from existing migrations):

// 迁移323: dan_assessment_results 添加 language_score 列(游戏化认知测评:语言表达维度)
ensureColumn("dan_assessment_results", "language_score", "INT COMMENT '语言表达得分'");
log.info("迁移323: 已确保 language_score 列存在于 dan_assessment_results");
  • Step 4: Update schema.sql

In cfc-backend/src/main/resources/schema.sql, find the dan_assessment_results CREATE TABLE (around line 421). After the processing_speed_score column definition (line 437), add:

language_score INT COMMENT '语言表达得分',
  • Step 5: Compile verification

Run: cd cfc-backend && mvn clean compile Expected: exit code 0, no errors.

  • [ ] Step 6: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java
    git add cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java
    git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
    git add cfc-backend/src/main/resources/schema.sql
    git commit -m "feat: add language_score dimension to cognitive assessment (migration 323)"
    

Task 2: Frontend — Language Test Page (language-test.vue)

Files:

  • Create: cfc-frontend/pages/cognitive-test/language-test.vue
  • Modify: cfc-frontend/pages.json

Interfaces:

  • Consumes: submitCognitiveSelfTest(memberId, 'languageScore', score) from utils/api.js
  • Produces: a new quiz page matching the visual and interaction style of existing cognitive tests

  • [ ] Step 1: Write the page

Create cfc-frontend/pages/cognitive-test/language-test.vue with this exact content:

<template>
  <view class="page">

    <!-- Intro -->
    <view class="intro" v-if="step === 'intro'">
      <view class="intro-icon"><text class="intro-icon-text">🗣️</text></view>
      <text class="intro-title">语言表达测试</text>
      <text class="intro-desc">观察每组词语,找出与其他词语关系不同的那个。测试你的语言理解和词汇联想能力。</text>
      <view class="intro-rule">
        <text class="rule-item">共 6 题</text>
        <text class="rule-item">每题限时 12 秒</text>
        <text class="rule-item">测评维度:语言表达</text>
      </view>
      <button class="primary-btn" @click="startTest">开始测试</button>
    </view>

    <!-- Playing -->
    <view class="playing" v-if="step === 'playing'">
      <view class="progress-header">
        <text class="progress-label">第 {{ currentQ + 1 }}/{{ totalQ }} 题</text>
        <text class="timer-display">{{ timerSeconds }}s</text>
      </view>
      <view class="progress-track">
        <view class="progress-fill" :style="{ width: progressPct + '%' }"></view>
      </view>
      <view class="question-card">
        <text class="question-text">{{ currentQuestion.prompt }}</text>
        <view class="options">
          <view
            class="option"
            v-for="(opt, oi) in currentQuestion.options"
            :key="oi"
            :class="{ 'option-selected': selectedIndex === oi, 'option-correct': showFeedback && oi === currentQuestion.answerIndex, 'option-wrong': showFeedback && selectedIndex === oi && selectedIndex !== currentQuestion.answerIndex }"
            @click="selectOption(oi)"
          >
            <text class="option-text">{{ opt }}</text>
          </view>
        </view>
      </view>
      <view class="feedback-overlay" v-if="showFeedback">
        <text class="feedback-text">{{ feedbackMessage }}</text>
      </view>
    </view>

    <!-- Result -->
    <view class="result" v-if="step === 'result'">
      <view class="score-circle">
        <text class="score-num">{{ score }}</text>
        <text class="score-unit">分</text>
      </view>
      <text class="score-comment">{{ scoreComment }}</text>
      <view class="result-info">
        <view class="info-row">
          <text class="info-label">答对题数</text>
          <text class="info-value">{{ correctCount }} / {{ totalQ }}</text>
        </view>
        <view class="info-row">
          <text class="info-label">测评维度</text>
          <view class="dim-tag"><text class="dim-tag-text">语言表达</text></view>
        </view>
      </view>
      <button class="primary-btn" :disabled="submitting" @click="submitScore">{{ submitting ? '提交中...' : '提交成绩' }}</button>
      <button class="ghost-btn" @click="startTest">再测一次</button>
    </view>

  </view>
</template>

<script>
import { submitCognitiveSelfTest } from '../../utils/api.js'

var TOTAL_Q = 6
var TIMER_SECONDS = 12

var QUESTION_BANK = [
  { prompt: '找出不同类的一项', options: ['苹果', '香蕉', '胡萝卜', '橘子'], answerIndex: 2 },
  { prompt: '找出不同类的一项', options: ['狗', '猫', '马', '鱼'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['红色', '蓝色', '绿色', '桌子'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['钢琴', '小提琴', '吉他', '足球'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['春天', '夏天', '秋天', '月亮'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['跑步', '游泳', '跳跃', '思考'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['父亲', '母亲', '姐姐', '老师'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['火车', '飞机', '轮船', '自行车'], answerIndex: 2 },
  { prompt: '找出不同类的一项', options: ['黄金', '白银', '铜', '铁'], answerIndex: 0 },
  { prompt: '找出不同类的一项', options: ['广州', '深圳', '香港', '北京'], answerIndex: 2 },
  { prompt: '找出不同类的一项', options: ['三角形', '圆形', '方形', '菱形'], answerIndex: 1 },
  { prompt: '找出不同类的一项', options: ['莎士比亚', '李白', '杜甫', '爱因斯坦'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['幸福', '快乐', '悲伤', '满足'], answerIndex: 2 },
  { prompt: '找出不同类的一项', options: ['书本', '报纸', '杂志', '电脑'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['太阳', '月亮', '星星', '云朵'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['钢笔', '铅笔', '毛笔', '画笔'], answerIndex: 0 },
  { prompt: '找出不同类的一项', options: ['苹果', '芒果', '榴莲', '土豆'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['大海', '河流', '湖泊', '雨水'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['红色', '橙色', '黄色', '白色'], answerIndex: 3 },
  { prompt: '找出不同类的一项', options: ['钢琴', '鼓', '喇叭', '篮球'], answerIndex: 3 }
]

export default {
  data() {
    return {
      step: 'intro',
      questions: [],
      currentQ: 0,
      selectedIndex: -1,
      showFeedback: false,
      feedbackMessage: '',
      correctCount: 0,
      timeLeft: TIMER_SECONDS,
      timer: null,
      score: 0,
      memberId: '',
      submitting: false,
      totalQ: TOTAL_Q
    }
  },
  computed: {
    currentQuestion: function() {
      if (this.questions.length === 0) return null
      return this.questions[this.currentQ]
    },
    progressPct: function() {
      return ((this.currentQ + 1) / this.totalQ) * 100
    },
    timerSeconds: function() {
      return Math.ceil(this.timeLeft)
    },
    scoreComment: function() {
      var s = this.score
      if (s >= 90) return '语言表达出色,词汇联想能力很强!'
      if (s >= 70) return '语言表达良好,继续锻炼会更强!'
      if (s >= 50) return '表现不错,多阅读和多表达会有帮助!'
      return '多练习词汇分类,表达能力会逐步提升!'
    }
  },
  onLoad: function(options) {
    var id = (options && options.memberId) || uni.getStorageSync('currentChildId') || ''
    this.memberId = id
  },
  onUnload: function() {
    this.clearTimer()
  },
  methods: {
    shuffle: function(arr) {
      var a = arr.slice()
      for (var i = a.length - 1; i > 0; i--) {
        var j = Math.floor(Math.random() * (i + 1))
        var tmp = a[i]
        a[i] = a[j]
        a[j] = tmp
      }
      return a
    },
    clearTimer: function() {
      if (this.timer) {
        clearInterval(this.timer)
        this.timer = null
      }
    },
    startTest: function() {
      this.clearTimer()
      var shuffled = this.shuffle(QUESTION_BANK)
      this.questions = shuffled.slice(0, this.totalQ)
      this.currentQ = 0
      this.correctCount = 0
      this.score = 0
      this.selectedIndex = -1
      this.showFeedback = false
      this.step = 'playing'
      this.startTimer()
    },
    startTimer: function() {
      this.clearTimer()
      this.timeLeft = TIMER_SECONDS
      var self = this
      this.timer = setInterval(function() {
        self.timeLeft -= 0.1
        if (self.timeLeft <= 0.01) {
          self.timeLeft = 0
          self.onTimeout()
        }
      }, 100)
    },
    selectOption: function(idx) {
      if (this.showFeedback) return
      this.showFeedback = true
      this.selectedIndex = idx
      this.clearTimer()
      var q = this.currentQuestion
      if (q && idx === q.answerIndex) {
        this.correctCount++
        this.feedbackMessage = '正确!'
      } else {
        this.feedbackMessage = '错误!'
      }
      var self = this
      setTimeout(function() {
        self.showFeedback = false
        self.selectedIndex = -1
        self.advance()
      }, 900)
    },
    onTimeout: function() {
      if (this.showFeedback) return
      this.showFeedback = true
      this.feedbackMessage = '超时!'
      this.clearTimer()
      var self = this
      setTimeout(function() {
        self.showFeedback = false
        self.advance()
      }, 900)
    },
    advance: function() {
      if (this.currentQ < this.totalQ - 1) {
        this.currentQ++
        this.startTimer()
      } else {
        this.finishTest()
      }
    },
    finishTest: function() {
      this.clearTimer()
      this.score = Math.round((this.correctCount / this.totalQ) * 100)
      this.step = 'result'
    },
    submitScore: function() {
      if (this.submitting) return
      var mid = this.memberId
      if (!mid) {
        uni.showToast({ title: '未获取到成员信息', icon: 'none' })
        return
      }
      this.submitting = true
      submitCognitiveSelfTest(mid, 'languageScore', this.score)
        .then(function(res) {
          if (res && res.code === 200) {
            uni.showToast({ title: '提交成功', icon: 'success' })
            var self = this
            setTimeout(function() {
              var pages = getCurrentPages()
              if (pages.length > 1) {
                uni.navigateBack()
              } else {
                uni.reLaunch({ url: '/pages/index/index' })
              }
            }, 800)
          } else {
            uni.showToast({ title: '提交失败', icon: 'none' })
          }
        }.bind(this))
        .catch(function() {
          uni.showToast({ title: '提交失败', icon: 'none' })
        })
        .finally(function() {
          this.submitting = false
        }.bind(this))
    }
  }
}
</script>

<style scoped>
.page {
  min-height: 100vh;
  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 60rpx 40rpx 80rpx;
  box-sizing: border-box;
}

.primary-btn {
  margin-top: 60rpx;
  width: 480rpx;
  height: 96rpx;
  line-height: 96rpx;
  border-radius: 48rpx;
  background: linear-gradient(135deg, #F59E0B 0%, #FBBF24 100%);
  color: #1a1a2e;
  font-size: 34rpx;
  font-weight: 600;
  border: none;
  box-shadow: 0 8rpx 24rpx rgba(245, 158, 11, 0.35);
}
.primary-btn::after { border: none; }
.primary-btn[disabled] { opacity: 0.6; }

.ghost-btn {
  margin-top: 30rpx;
  width: 480rpx;
  height: 96rpx;
  line-height: 96rpx;
  border-radius: 48rpx;
  background: transparent;
  color: #FBBF24;
  font-size: 32rpx;
  border: 2rpx solid rgba(245, 158, 11, 0.5);
}
.ghost-btn::after { border: none; }

.intro { display: flex; flex-direction: column; align-items: center; width: 100%; padding-top: 80rpx; }
.intro-icon {
  width: 160rpx; height: 160rpx; border-radius: 50%;
  background: rgba(245, 158, 11, 0.15); border: 3rpx solid #F59E0B;
  display: flex; align-items: center; justify-content: center; margin-bottom: 40rpx;
}
.intro-icon-text { font-size: 80rpx; }
.intro-title { font-size: 44rpx; font-weight: bold; color: #FBBF24; margin-bottom: 16rpx; display: block; }
.intro-desc { font-size: 26rpx; color: rgba(255,255,255,0.7); margin-bottom: 50rpx; display: block; text-align: center; padding: 0 20rpx; }
.intro-rule { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; }
.rule-item {
  font-size: 24rpx; color: #FBBF24;
  background: rgba(245, 158, 11, 0.12); border-radius: 24rpx;
  padding: 10rpx 24rpx; margin: 8rpx 12rpx;
}

.playing { width: 100%; display: flex; flex-direction: column; align-items: center; }
.progress-header { display: flex; justify-content: space-between; width: 100%; max-width: 620rpx; margin-bottom: 16rpx; }
.progress-label { font-size: 30rpx; color: #fff; font-weight: bold; }
.timer-display { font-size: 30rpx; color: #FBBF24; font-weight: bold; }
.progress-track {
  width: 100%; max-width: 620rpx; height: 12rpx;
  border-radius: 6rpx; background: rgba(255,255,255,0.1);
  overflow: hidden; margin-bottom: 50rpx;
}
.progress-fill { height: 100%; border-radius: 6rpx; background: linear-gradient(90deg, #F59E0B, #FBBF24); transition: width 0.3s ease; }

.question-card {
  background: rgba(255,255,255,0.06); border-radius: 24rpx;
  padding: 40rpx 30rpx; width: 100%; max-width: 620rpx; box-sizing: border-box;
}
.question-text { font-size: 34rpx; color: #fff; font-weight: bold; text-align: center; display: block; margin-bottom: 40rpx; }
.options { display: flex; flex-direction: column; }
.option {
  padding: 26rpx 0; border-radius: 16rpx; border: 3rpx solid rgba(255,255,255,0.2);
  background: #24243e; text-align: center; margin-bottom: 20rpx; transition: all 0.2s;
}
.option:last-child { margin-bottom: 0; }
.option-selected { border-color: #FBBF24; background: rgba(245, 158, 11, 0.12); }
.option-correct { border-color: #10B981; background: rgba(16, 185, 129, 0.15); }
.option-wrong { border-color: #EF4444; background: rgba(239, 68, 68, 0.15); }
.option:active { transform: scale(0.98); }
.option-text { font-size: 32rpx; color: #fff; font-weight: 500; }
.option-selected .option-text, .option-correct .option-text { color: #FBBF24; }
.option-correct .option-text { color: #10B981; }
.option-wrong .option-text { color: #EF4444; }

.feedback-overlay {
  position: fixed; left: 0; right: 0; top: 0; bottom: 0;
  display: flex; align-items: center; justify-content: center; z-index: 99;
  animation: feedbackFade 0.8s ease both;
}
.feedback-overlay:has(.feedback-text) { }
.feedback-text {
  font-size: 56rpx; font-weight: bold; color: #fff;
  padding: 24rpx 60rpx; border-radius: 24rpx; background: rgba(0,0,0,0.35);
}
@keyframes feedbackFade {
  0% { opacity: 0; }
  20% { opacity: 1; }
  80% { opacity: 1; }
  100% { opacity: 0; }
}

.result { display: flex; flex-direction: column; align-items: center; width: 100%; padding-top: 100rpx; }
.score-circle {
  width: 240rpx; height: 240rpx; border-radius: 50%;
  background: linear-gradient(135deg, #F59E0B, #FBBF24);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  box-shadow: 0 8rpx 40rpx rgba(245, 158, 11, 0.35);
}
.score-num { font-size: 80rpx; font-weight: bold; color: #1a1a2e; line-height: 1; }
.score-unit { font-size: 28rpx; color: rgba(26,26,46,0.7); margin-top: 4rpx; }
.score-comment { font-size: 30rpx; color: #FBBF24; margin-top: 30rpx; margin-bottom: 50rpx; display: block; }
.result-info {
  width: 100%; max-width: 620rpx; background: rgba(255,255,255,0.06);
  border-radius: 20rpx; padding: 20rpx 30rpx; margin-bottom: 60rpx; box-sizing: border-box;
}
.info-row { display: flex; justify-content: space-between; align-items: center; padding: 16rpx 0; }
.info-row + .info-row { border-top: 1rpx solid rgba(255,255,255,0.08); }
.info-label { font-size: 26rpx; color: rgba(255,255,255,0.7); }
.info-value { font-size: 28rpx; color: #fff; font-weight: bold; }
.dim-tag { padding: 6rpx 24rpx; border-radius: 24rpx; border: 3rpx solid #F59E0B; background: rgba(245, 158, 11, 0.12); }
.dim-tag-text { font-size: 24rpx; color: #FBBF24; font-weight: bold; }
</style>
  • Step 2: Register the page in pages.json

In cfc-frontend/pages.json, find the pages/cognitive-test subpackage entry (around line 328). Add the new page entry inside that subpackage's pages array, after the existing entries:

        {
          "path": "language-test",
          "style": {
            "navigationBarTitleText": "语言表达测试"
          }
        }

Insert before the closing ] of that subpackage's pages array (which currently ends at line 361 with the speed-test entry).

  • Step 3: Verify syntax

Run: node --check cfc-frontend/pages/cognitive-test/language-test.vue is not valid — Vue files don't support --check. Instead, verify the JS in the <script> block parses: Extract the script block and run node -e "require('fs').readFileSync('cfc-frontend/pages/cognitive-test/language-test.vue','utf8').match(/<script>([\s\S]*?)<\/script>/)[1]" | node --input-type=module 2>&1 | head -5 or simply visually inspect that there are no syntax errors.

Alternatively, verify the pages.json is valid JSON:

node -e "JSON.parse(require('fs').readFileSync('cfc-frontend/pages.json','utf8')); console.log('pages.json OK')"

Expected: pages.json OK

  • [ ] Step 4: Commit

    git add cfc-frontend/pages/cognitive-test/language-test.vue
    git add cfc-frontend/pages.json
    git commit -m "feat: add language expression cognitive test game (七维认知测评)"
    

Task 3: Frontend — Cognitive Hall Portal Page

Files:

  • Create: cfc-frontend/pages/cognitive-hall.vue
  • Modify: cfc-frontend/pages.json

Interfaces:

  • Consumes: getCognitiveFamilyScores() from utils/api.js (returns list of members with cognitive scores)
  • Produces: a portal page navigable from the wisdom (智) TabBar or a dedicated menu entry

  • [ ] Step 1: Create the cognitive hall page

Create cfc-frontend/pages/cognitive-hall.vue:

<template>
  <view class="page">
    <view class="header">
      <text class="title">认知能力测评</text>
      <text class="subtitle">通过小游戏评估七项核心认知能力</text>
    </view>

    <view class="member-selector" v-if="members.length > 1">
      <text class="selector-label">选择测评对象:</text>
      <view class="member-tabs">
        <view
          class="member-tab"
          :class="{ 'member-tab-active': selectedMemberId === m.id }"
          v-for="m in members"
          :key="m.id"
          @click="selectMember(m.id)"
        >
          <text>{{ m.nickname }}</text>
        </view>
      </view>
    </view>

    <view class="loading" v-if="loading">
      <text>加载中...</text>
    </view>

    <view class="game-list" v-if="!loading">
      <view
        class="game-card"
        v-for="game in games"
        :key="game.code"
        @click="navigateTo(game.path)"
      >
        <view class="game-icon-wrap" :style="{ background: game.colorBg }">
          <text class="game-icon">{{ game.icon }}</text>
        </view>
        <view class="game-info">
          <text class="game-name">{{ game.name }}</text>
          <text class="game-desc">{{ game.desc }}</text>
          <text class="game-status" v-if="game.completed">✓ 已完成</text>
          <text class="game-status-pending" v-else>未测评</text>
        </view>
        <text class="game-arrow">›</text>
      </view>
    </view>

    <view class="empty" v-if="!loading && scores === null">
      <text class="empty-icon">🧠</text>
      <text class="empty-text">暂无认知数据</text>
      <text class="empty-hint">完成任意一项测试后即可查看详细报告</text>
    </view>

    <view class="action-row">
      <view class="btn btn--outline" @click="goReport">查看完整报告</view>
    </view>
  </view>
</template>

<script>
import { getCognitiveChildScores } from '../../utils/api.js'

export default {
  data() {
    return {
      memberId: '',
      members: [],
      selectedMemberId: null,
      scores: null,
      loading: true
    }
  },
  computed: {
    games: function() {
      var s = this.scores
      return [
        { code: 'perception', name: '感知能力', desc: '感官信息处理能力', icon: '👁️', colorBg: 'rgba(99,102,241,0.15)', path: '/pages/cognitive-test/perception-test', completed: s && s.perceptionScore != null },
        { code: 'focus', name: '专注力', desc: '持续集中和抗干扰能力', icon: '🎯', colorBg: 'rgba(239,68,68,0.15)', path: '/pages/games/schulte', completed: s && s.focusScore != null },
        { code: 'memory', name: '记忆力', desc: '信息编码和回忆能力', icon: '🧠', colorBg: 'rgba(99,102,241,0.15)', path: '/pages/cognitive-test/memory-test', completed: s && s.memoryScore != null },
        { code: 'logic', name: '逻辑推理', desc: '推理和问题解决能力', icon: '💡', colorBg: 'rgba(251,191,36,0.15)', path: '/pages/cognitive-test/logic-test', completed: s && s.logicScore != null },
        { code: 'spatial', name: '空间思维', desc: '空间想象和方位判断', icon: '🗺️', colorBg: 'rgba(16,185,129,0.15)', path: '/pages/cognitive-test/spatial-test', completed: s && s.spatialScore != null },
        { code: 'speed', name: '加工速度', desc: '信息处理和反应速度', icon: '⚡', colorBg: 'rgba(245,158,11,0.15)', path: '/pages/cognitive-test/speed-test', completed: s && s.processingSpeedScore != null },
        { code: 'language', name: '语言表达', desc: '词汇理解与语言组织', icon: '🗣️', colorBg: 'rgba(236,72,153,0.15)', path: '/pages/cognitive-test/language-test', completed: s && s.languageScore != null }
      ]
    }
  },
  onLoad: function(options) {
    if (options && options.memberId) {
      this.memberId = options.memberId
    } else {
      this.memberId = uni.getStorageSync('currentChildId') || ''
    }
    this.selectedMemberId = this.memberId
    this.loadScores()
  },
  methods: {
    selectMember: function(id) {
      this.selectedMemberId = id
      this.loadScores()
    },
    loadScores: function() {
      var self = this
      if (!this.selectedMemberId) {
        this.loading = false
        return
      }
      this.loading = true
      getCognitiveChildScores(this.selectedMemberId)
        .then(function(res) {
          if (res && res.code === 200 && res.data) {
            self.scores = res.data
          } else {
            self.scores = null
          }
        })
        .catch(function() {
          self.scores = null
        })
        .finally(function() {
          self.loading = false
        })
    },
    navigateTo: function(path) {
      var url = path + '?memberId=' + this.selectedMemberId
      uni.navigateTo({ url: url })
    },
    goReport: function() {
      uni.navigateTo({ url: '/pages/wisdom/cognitive-report?memberId=' + (this.selectedMemberId || '') })
    }
  }
}
</script>

<style scoped>
.page { min-height: 100vh; background: #f5f7fa; padding-bottom: 40rpx; }
.header { padding: 40rpx 30rpx 20rpx; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); color: #fff; }
.title { font-size: 44rpx; font-weight: bold; display: block; }
.subtitle { font-size: 26rpx; opacity: 0.85; margin-top: 10rpx; display: block; }
.member-selector { padding: 20rpx 30rpx; background: #fff; margin: 20rpx 0; border-radius: 16rpx; }
.selector-label { font-size: 26rpx; color: #666; }
.member-tabs { display: flex; flex-direction: row; flex-wrap: wrap; margin-top: 16rpx; }
.member-tab {
  padding: 10rpx 28rpx; border-radius: 30rpx; margin: 8rpx 8rpx 0 0;
  background: #eee; font-size: 26rpx; color: #666;
}
.member-tab-active { background: #6366f1; color: #fff; }
.game-list { padding: 0 30rpx; }
.game-card {
  display: flex; flex-direction: row; align-items: center;
  background: #fff; border-radius: 20rpx; padding: 28rpx; margin-bottom: 20rpx;
  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.game-icon-wrap {
  width: 88rpx; height: 88rpx; border-radius: 20rpx;
  display: flex; align-items: center; justify-content: center; flex-shrink: 0;
}
.game-icon { font-size: 44rpx; }
.game-info { flex: 1; margin-left: 24rpx; }
.game-name { font-size: 30rpx; font-weight: bold; color: #1a1a2e; display: block; }
.game-desc { font-size: 24rpx; color: #888; margin-top: 6rpx; display: block; }
.game-status { font-size: 22rpx; color: #10B981; margin-top: 8rpx; display: block; }
.game-status-pending { font-size: 22rpx; color: #aaa; margin-top: 8rpx; display: block; }
.game-arrow { font-size: 40rpx; color: #ccc; margin-left: 16rpx; }
.loading, .empty { text-align: center; padding: 80rpx 40rpx; color: #999; font-size: 28rpx; }
.empty-icon { font-size: 80rpx; display: block; margin-bottom: 20rpx; }
.empty-hint { font-size: 24rpx; color: #bbb; margin-top: 10rpx; display: block; }
.action-row { padding: 30rpx; }
.btn { width: 100%; padding: 24rpx 0; border-radius: 50rpx; text-align: center; font-size: 30rpx; font-weight: bold; box-sizing: border-box; }
.btn--outline { border: 2rpx solid #6366f1; color: #6366f1; background: transparent; }
.btn--outline:active { background: rgba(99,102,241,0.08); }
</style>
  • Step 2: Register in pages.json

Add the new page to the main pages array in cfc-frontend/pages.json. Insert before the first subpackage entry. Find a good spot — add it near the other wisdom-related pages or as a standalone entry:

{
  "path": "pages/cognitive-hall",
  "style": {
    "navigationBarTitleText": "认知能力测评"
  }
}
  • [ ] Step 3: Verify JSON

    node -e "JSON.parse(require('fs').readFileSync('cfc-frontend/pages.json','utf8')); console.log('pages.json OK')"
    

Expected: pages.json OK

  • [ ] Step 4: Commit

    git add cfc-frontend/pages/cognitive-hall.vue
    git add cfc-frontend/pages.json
    git commit -m "feat: add cognitive assessment hall portal (七维认知能力总览)"
    

Task 4: Frontend — Cognitive Report Update

Files:

  • Modify: cfc-frontend/pages/wisdom/cognitive-report.vue

Interfaces:

  • Consumes: existing dimensions array and report object
  • Produces: a report showing all 7 dimensions including the new languageScore

  • [ ] Step 1: Add language dimension to the report

In cfc-frontend/pages/wisdom/cognitive-report.vue, in the data() function, find the dimensions array (around line 89) and add the new language dimension at the end:

{ key: 'languageScore', label: '语言表达', icon: '🗣️', desc: '词汇理解与语言组织能力' }

The full updated array should be:

dimensions: [
  { key: 'perceptionScore', label: '感知能力', icon: '👁️', desc: '感官信息处理能力' },
  { key: 'focusScore', label: '专注力', icon: '🎯', desc: '持续集中和抗干扰能力' },
  { key: 'memoryScore', label: '记忆力', icon: '🧠', desc: '信息编码和回忆能力' },
  { key: 'logicScore', label: '逻辑思维', icon: '💡', desc: '推理和问题解决能力' },
  { key: 'spatialScore', label: '空间思维', icon: '🗺️', desc: '空间想象和方位判断' },
  { key: 'processingSpeedScore', label: '加工速度', icon: '⚡', desc: '信息处理和反应速度' },
  { key: 'languageScore', label: '语言表达', icon: '🗣️', desc: '词汇理解与语言组织能力' }
]
  • Step 2: Update the empty state message

In the same file, find the empty state text around line 73 and update the description to mention 7 dimensions:

Change:

<text class="empty-desc">完成 DAN 认知评估后,可在此查看详细的六维认知分析报告</text>

To:

<text class="empty-desc">完成认知能力游戏测评或DAN评估后,可在此查看详细的七维认知分析报告</text>
  • [ ] Step 3: Commit

    git add cfc-frontend/pages/wisdom/cognitive-report.vue
    git commit -m "feat: add language dimension to cognitive report (七维认知报告)"
    

Task 5: Compile & Verification

  • Step 1: Backend compilation

Run: cd cfc-backend && mvn clean compile Expected: exit code 0, no compilation errors.

  • Step 2: Frontend JSON validation

Run: node -e "JSON.parse(require('fs').readFileSync('cfc-frontend/pages.json','utf8')); console.log('pages.json OK')" Expected: pages.json OK

  • [ ] Step 3: Quick JS syntax check on new Vue files

    node -e "
    var fs = require('fs');
    ['cfc-frontend/pages/cognitive-test/language-test.vue','cfc-frontend/pages/cognitive-hall.vue'].forEach(function(f) {
    var content = fs.readFileSync(f, 'utf8');
    var scriptMatch = content.match(/<script>([\\s\\S]*?)<\\/script>/);
    if (scriptMatch) {
    try { new Function(scriptMatch[1]); console.log(f + ': JS OK'); }
    catch(e) { console.error(f + ': JS ERROR: ' + e.message); }
    }
    });
    "
    

Expected: both files print JS OK.

  • Step 4: Final commit (if any changes from verification)

If verification revealed issues, fix and commit. Otherwise no additional commit needed.


Notes for Implementer

  • No new backend endpoints are needed. The existing POST /api/cognitive/self-test already handles score submission.
  • No new entity or migration for test metadata — each game page tracks state client-side and posts the final score.
  • attentionScore and focusScore exist in the entity but have no game pages. They can be set via self-test or manual entry but are not part of this deliverable. If needed later, add a similar page.
  • The existing 5 test pages use submitCognitiveSelfTest with dimension keys: perceptionScore, logicScore, memoryScore, spatialScore, processingSpeedScore. The new page adds languageScore.
  • The overallScore calculation in CognitiveService.submitSelfTestScore() iterates over all 7 dimension fields, so adding languageScore automatically includes it in the composite average.
  • Page flow: user opens cognitive-hall.vue → sees 7 cards → taps one → plays game → submits score → returns to hall (hall refreshes status).