Просмотр исходного кода

feat: add seven-dimension cognitive game assessment

- Backend: languageScore field, migration 323, CognitiveService switch update
- Frontend: language-test.vue new page, cognitive-hall.vue portal, pages.json registers both, cognitive-report.vue updated to 7 dimensions
E2E Test Bot 1 месяц назад
Родитель
Сommit
9da4cb07ca

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -11130,6 +11130,10 @@ log.info("迁移298: 已为无 openid 的 child 账号补齐 family_members 记
 
         // 迁移300: profile_snapshot 增加 quiz_summary 列(答题认知雷达汇总)
         ensureColumn("profile_snapshot", "quiz_summary", "JSON DEFAULT NULL COMMENT '答题认知雷达汇总'");
+
+        // 迁移323: dan_assessment_results 添加 language_score 列(游戏化认知测评:语言表达维度)
+        ensureColumn("dan_assessment_results", "language_score", "INT COMMENT '语言表达得分'");
+        log.info("迁移323: 已确保 language_score 列存在于 dan_assessment_results");
     }
 
     /**

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/DanAssessmentResult.java

@@ -45,6 +45,8 @@ public class DanAssessmentResult implements Serializable {
 
     private Integer processingSpeedScore;  // 加工速度得分
 
+    private Integer languageScore;  // 语言表达得分 (0-100)
+
     // === EMI 心理子维度(v1.2 新增) ===
     private Integer emotionScore;  // 情绪商数(0-100)
     private Integer resilienceScore;  // 心理韧性(0-100)

+ 4 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/CognitiveService.java

@@ -130,6 +130,9 @@ public class CognitiveService {
             case "processingSpeedScore":
                 latest.setProcessingSpeedScore(score);
                 break;
+            case "languageScore":
+                latest.setLanguageScore(score);
+                break;
             default:
                 log.warn("未知的认知自测维度: {}", dimension);
                 return false;
@@ -140,7 +143,7 @@ public class CognitiveService {
                 latest.getAttentionScore(), latest.getFocusScore(),
                 latest.getMemoryScore(), latest.getLogicScore(),
                 latest.getPerceptionScore(), latest.getSpatialScore(),
-                latest.getProcessingSpeedScore())) {
+                latest.getProcessingSpeedScore(), latest.getLanguageScore())) {
             if (s != null && s > 0) { total += s; validCount++; }
         }
         if (validCount > 0) {

+ 1 - 0
cfc-backend/src/main/resources/schema.sql

@@ -435,6 +435,7 @@ CREATE TABLE IF NOT EXISTS dan_assessment_results (
     perception_score INT COMMENT '感知得分',
     spatial_score INT COMMENT '空间思维得分',
     processing_speed_score INT COMMENT '加工速度得分',
+    language_score INT COMMENT '语言表达得分',
     emotion_score INT COMMENT '情绪商数(EMI)',
     resilience_score INT COMMENT '心理韧性(EMI)',
     stress_coping_score INT COMMENT '压力应对(EMI)',

+ 12 - 0
cfc-frontend/pages.json

@@ -7,6 +7,12 @@
         "navigationStyle": "custom"
       }
     },
+    {
+      "path": "pages/cognitive-hall",
+      "style": {
+        "navigationBarTitleText": "认知能力测评"
+      }
+    },
     {
       "path": "pages/index-home/index",
       "style": {
@@ -357,6 +363,12 @@
           "style": {
             "navigationBarTitleText": "加工速度测试"
           }
+        },
+        {
+          "path": "language-test",
+          "style": {
+            "navigationBarTitleText": "语言表达测试"
+          }
         }
       ]
     },

+ 332 - 0
cfc-frontend/pages/cognitive-hall.vue

@@ -0,0 +1,332 @@
+<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 && !hasData">
+      <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'
+import { getFamilyMemberList } from '../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      memberId: '',
+      members: [],
+      selectedMemberId: null,
+      scores: null,
+      loading: true
+    }
+  },
+  computed: {
+    hasData: function() {
+      if (!this.scores) return false
+      var s = this.scores
+      return !!(s.perceptionScore || s.focusScore || s.memoryScore ||
+        s.logicScore || s.spatialScore || s.processingSpeedScore || s.languageScore)
+    },
+    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.loadMembers()
+    this.loadScores()
+  },
+  methods: {
+    loadMembers: function() {
+      var self = this
+      getFamilyMemberList({ visibleOnly: true })
+        .then(function(res) {
+          if (res && res.code === 200 && res.data) {
+            self.members = res.data
+            if (!self.memberId && res.data.length > 0) {
+              self.memberId = res.data[0].id
+              self.selectedMemberId = res.data[0].id
+              self.loadScores()
+            }
+          }
+        })
+        .catch(function() {
+          // 静默失败
+        })
+    },
+    selectMember: function(id) {
+      this.selectedMemberId = id
+      this.loadScores()
+    },
+    loadScores: function() {
+      var self = this
+      var memberId = this.selectedMemberId
+      if (!memberId) {
+        this.loading = false
+        return
+      }
+      this.loading = true
+      getCognitiveChildScores(memberId)
+        .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;
+  box-sizing: border-box;
+}
+
+.header {
+  padding: 40rpx 30rpx 30rpx;
+  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: 24rpx 30rpx;
+  background: #fff;
+  margin: 20rpx 0;
+  border-radius: 16rpx;
+  margin-left: 30rpx;
+  margin-right: 30rpx;
+}
+
+.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>

+ 462 - 0
cfc-frontend/pages/cognitive-test/language-test.vue

@@ -0,0 +1,462 @@
+<template>
+  <view class="test-container">
+
+    <!-- 步骤一:测试说明 -->
+    <view class="step-panel" v-if="step === 'instruction'">
+      <view class="title-icon">🗣️</view>
+      <text class="test-title">语言表达测试</text>
+      <text class="test-desc">观察每组词语,找出与其它三个不同类的那一个。</text>
+      <text class="test-desc">考验你的词汇理解和分类归纳能力!</text>
+      <text class="test-meta">共 6 题 · 每题限时 12 秒 · 超时自动进入下一题</text>
+      <button class="btn-primary" @click="startTest">开始测试</button>
+    </view>
+
+    <!-- 步骤二:答题中 -->
+    <view class="step-panel" v-else-if="step === 'playing'">
+      <view class="progress-row">
+        <text class="progress-text">第 {{ currentIndex + 1 }}/6 题</text>
+        <text class="progress-text">答对 {{ correctCount }} 题</text>
+      </view>
+
+      <view class="timer-bar">
+        <view class="timer-fill" :style="timerBarStyle"></view>
+      </view>
+      <text class="timer-text">{{ timerLeft }}秒</text>
+
+      <text class="question-hint" v-if="currentQuestion">{{ currentQuestion.prompt }}</text>
+
+      <view class="option-wrap">
+        <view
+          class="option-cell"
+          v-for="(opt, index) in currentQuestionOptions"
+          :key="index"
+          :class="{
+            'cell-correct': lockInput && index === currentQuestion.answerIndex,
+            'cell-wrong': tappedIndex === index && index !== currentQuestion.answerIndex
+          }"
+          @click="handleTap(index)"
+        >
+          <text class="option-text">{{ opt }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 步骤三:结果 -->
+    <view class="step-panel" v-else>
+      <text class="result-title">测试完成</text>
+      <view class="score-circle">
+        <text class="score-num">{{ score }}</text>
+      </view>
+      <view class="score-row">
+        <text class="score-label">答对 {{ correctCount }}/6 题</text>
+        <text class="dimension-tag">语言表达</text>
+      </view>
+      <view class="result-tip" v-if="score >= 75">
+        <text class="result-tip-text">词汇丰富,语言表达能力强!</text>
+      </view>
+      <view class="result-tip" v-else-if="score >= 50">
+        <text class="result-tip-text">表现不错,多阅读会更好!</text>
+      </view>
+      <view class="result-tip" v-else>
+        <text class="result-tip-text">别灰心,平时多积累词汇!</text>
+      </view>
+      <button class="btn-primary" @click="submitResult">提交成绩</button>
+      <button class="btn-secondary" @click="retryTest">再测一次</button>
+    </view>
+
+  </view>
+</template>
+
+<script>
+import { submitCognitiveSelfTest } from '../../utils/api.js'
+
+// 题库:4 个词语,其中一个是"不同类"的
+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 }
+]
+
+var ROUND_COUNT = 6
+var TIMER_SECONDS = 12
+
+export default {
+  data() {
+    return {
+      step: 'instruction',
+      memberId: '',
+      roundQuestions: [],
+      currentIndex: 0,
+      currentQuestion: null,
+      correctCount: 0,
+      timerLeft: TIMER_SECONDS,
+      timer: null,
+      nextTimer: null,
+      lockInput: false,
+      tappedIndex: -1,
+      score: 0
+    }
+  },
+  computed: {
+    timerPercent: function() {
+      return Math.round(this.timerLeft / TIMER_SECONDS * 100)
+    },
+    timerBarStyle: function() {
+      return 'width:' + this.timerPercent + '%'
+    },
+    currentQuestionOptions: function() {
+      if (!this.currentQuestion) return []
+      return this.currentQuestion.options
+    }
+  },
+  onLoad: function(options) {
+    if (options && options.memberId) {
+      this.memberId = options.memberId
+    }
+  },
+  onUnload: function() {
+    this.stopTimer()
+    if (this.nextTimer) {
+      clearTimeout(this.nextTimer)
+      this.nextTimer = null
+    }
+  },
+  methods: {
+    startTest: function() {
+      this.correctCount = 0
+      this.currentIndex = 0
+      this.score = 0
+      this.lockInput = false
+      this.tappedIndex = -1
+      // 洗牌后取前 6 题
+      var bank = QUESTION_BANK.slice()
+      for (var i = bank.length - 1; i > 0; i--) {
+        var j = Math.floor(Math.random() * (i + 1))
+        var tmp = bank[i]
+        bank[i] = bank[j]
+        bank[j] = tmp
+      }
+      this.roundQuestions = bank.slice(0, ROUND_COUNT)
+      this.step = 'playing'
+      this.loadQuestion(0)
+    },
+    loadQuestion: function(index) {
+      this.currentIndex = index
+      this.currentQuestion = this.roundQuestions[index]
+      this.tappedIndex = -1
+      this.lockInput = false
+      this.timerLeft = TIMER_SECONDS
+      this.startTimer()
+    },
+    startTimer: function() {
+      this.stopTimer()
+      this.timer = setInterval(() => {
+        this.timerLeft--
+        if (this.timerLeft <= 0) {
+          this.timerLeft = 0
+          this.onTimeout()
+        }
+      }, 1000)
+    },
+    stopTimer: function() {
+      if (this.timer) {
+        clearInterval(this.timer)
+        this.timer = null
+      }
+    },
+    handleTap: function(index) {
+      if (this.lockInput || !this.currentQuestion) return
+      this.lockInput = true
+      this.stopTimer()
+      this.tappedIndex = index
+      if (index === this.currentQuestion.answerIndex) {
+        this.correctCount++
+      }
+      this.scheduleNext()
+    },
+    onTimeout: function() {
+      if (this.lockInput) return
+      this.lockInput = true
+      this.stopTimer()
+      this.tappedIndex = -1
+      this.scheduleNext()
+    },
+    scheduleNext: function() {
+      if (this.nextTimer) clearTimeout(this.nextTimer)
+      this.nextTimer = setTimeout(() => {
+        this.nextTimer = null
+        this.advance()
+      }, 700)
+    },
+    advance: function() {
+      if (this.currentIndex + 1 >= this.roundQuestions.length) {
+        this.finishTest()
+      } else {
+        this.loadQuestion(this.currentIndex + 1)
+      }
+    },
+    finishTest: function() {
+      this.stopTimer()
+      this.score = Math.round(this.correctCount / this.roundQuestions.length * 100)
+      this.step = 'result'
+    },
+    submitResult: function() {
+      var memberId = this.memberId || uni.getStorageSync('currentChildId') || ''
+      if (!memberId) {
+        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        return
+      }
+      submitCognitiveSelfTest(memberId, 'languageScore', this.score)
+        .then((res) => {
+          uni.showToast({ title: '提交成功', icon: 'success' })
+          setTimeout(() => {
+            uni.navigateBack()
+          }, 1500)
+        })
+        .catch((err) => {
+          console.error('提交语言表达测试成绩失败', err)
+          uni.showToast({ title: '提交失败,请重试', icon: 'none' })
+        })
+    },
+    retryTest: function() {
+      this.startTest()
+    }
+  }
+}
+</script>
+
+<style scoped>
+.test-container {
+  min-height: 100vh;
+  background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
+  padding: 60rpx 40rpx;
+  box-sizing: border-box;
+}
+
+.step-panel {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+/* ===== 说明页 ===== */
+.title-icon {
+  font-size: 88rpx;
+  margin-bottom: 24rpx;
+}
+
+.test-title {
+  font-size: 48rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-bottom: 32rpx;
+}
+
+.test-desc {
+  font-size: 30rpx;
+  color: rgba(255,255,255,0.7);
+  line-height: 1.7;
+  text-align: center;
+}
+
+.test-meta {
+  font-size: 24rpx;
+  color: rgba(255,255,255,0.45);
+  margin-top: 24rpx;
+  margin-bottom: 80rpx;
+  text-align: center;
+}
+
+.btn-primary {
+  background: linear-gradient(135deg, #F59E0B, #FBBF24);
+  color: #1a1a2e;
+  font-size: 34rpx;
+  font-weight: bold;
+  width: 480rpx;
+  height: 92rpx;
+  line-height: 92rpx;
+  border-radius: 46rpx;
+  border: none;
+  margin-top: 60rpx;
+  padding: 0;
+}
+
+.btn-primary::after {
+  border: none;
+}
+
+.btn-secondary {
+  background: rgba(255,255,255,0.1);
+  color: rgba(255,255,255,0.85);
+  font-size: 32rpx;
+  width: 480rpx;
+  height: 92rpx;
+  line-height: 92rpx;
+  border-radius: 46rpx;
+  border: 2rpx solid rgba(255,255,255,0.25);
+  margin-top: 32rpx;
+  padding: 0;
+}
+
+.btn-secondary::after {
+  border: none;
+}
+
+/* ===== 答题页 ===== */
+.progress-row {
+  display: flex;
+  justify-content: space-between;
+  width: 100%;
+  margin-bottom: 24rpx;
+}
+
+.progress-text {
+  font-size: 28rpx;
+  color: rgba(255,255,255,0.7);
+}
+
+.timer-bar {
+  width: 100%;
+  height: 16rpx;
+  background: rgba(255,255,255,0.12);
+  border-radius: 8rpx;
+  overflow: hidden;
+}
+
+.timer-fill {
+  height: 100%;
+  background: linear-gradient(90deg, #F59E0B, #FBBF24);
+  border-radius: 8rpx;
+  transition: width 0.9s linear;
+}
+
+.timer-text {
+  display: block;
+  font-size: 26rpx;
+  color: #F59E0B;
+  margin-top: 12rpx;
+  text-align: center;
+}
+
+.question-hint {
+  display: block;
+  font-size: 32rpx;
+  font-weight: bold;
+  color: #fff;
+  margin: 40rpx 0 32rpx;
+  text-align: center;
+}
+
+.option-wrap {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: space-between;
+  width: 100%;
+}
+
+.option-cell {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 47%;
+  min-height: 120rpx;
+  background: #2a2a4a;
+  border-radius: 24rpx;
+  border: 4rpx solid transparent;
+  margin-bottom: 24rpx;
+  padding: 24rpx;
+  box-sizing: border-box;
+}
+
+.option-cell:active {
+  background: #35406b;
+}
+
+.option-text {
+  font-size: 34rpx;
+  color: #fff;
+  font-weight: 500;
+}
+
+.cell-correct {
+  background: #3d4a2f;
+  border-color: #4ECDC4;
+}
+
+.cell-wrong {
+  background: #4a2f35;
+  border-color: #ff6b6b;
+}
+
+/* ===== 结果页 ===== */
+.result-title {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #fff;
+  margin-bottom: 48rpx;
+}
+
+.score-circle {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 260rpx;
+  height: 260rpx;
+  border-radius: 50%;
+  border: 10rpx solid #F59E0B;
+  background: rgba(245,158,11,0.1);
+  margin-bottom: 36rpx;
+}
+
+.score-num {
+  font-size: 88rpx;
+  font-weight: bold;
+  color: #F59E0B;
+}
+
+.score-row {
+  display: flex;
+  align-items: center;
+  margin-bottom: 24rpx;
+}
+
+.score-label {
+  font-size: 30rpx;
+  color: rgba(255,255,255,0.7);
+  margin-right: 20rpx;
+}
+
+.dimension-tag {
+  font-size: 24rpx;
+  color: #F59E0B;
+  border: 2rpx solid #F59E0B;
+  border-radius: 24rpx;
+  padding: 6rpx 24rpx;
+}
+
+.result-tip {
+  margin-bottom: 20rpx;
+}
+
+.result-tip-text {
+  font-size: 28rpx;
+  color: rgba(255,255,255,0.6);
+}
+</style>

+ 3 - 2
cfc-frontend/pages/wisdom/cognitive-report.vue

@@ -70,7 +70,7 @@
     <view class="empty-state" v-if="!report && !loading">
       <text class="empty-icon">&#x1F9E0;</text>
       <text class="empty-title">暂无认知测评数据</text>
-      <text class="empty-desc">完成 DAN 认知评估后,可在此查看详细的六维认知分析报告</text>
+      <text class="empty-desc">完成认知能力游戏测评或DAN评估后,可在此查看详细的七维认知分析报告</text>
       <view class="empty-btn" @click="goAssessment">预约测评</view>
     </view>
   </view>
@@ -92,7 +92,8 @@ export default {
         { key: 'memoryScore', label: '记忆力', icon: '\u{1F9E0}', desc: '信息编码和回忆能力' },
         { key: 'logicScore', label: '逻辑思维', icon: '\u{1F9E9}', desc: '推理和问题解决能力' },
         { key: 'spatialScore', label: '空间思维', icon: '\u{1F9CA}', desc: '空间想象和方位判断' },
-        { key: 'processingSpeedScore', label: '加工速度', icon: '\u26A1', desc: '信息处理和反应速度' }
+        { key: 'processingSpeedScore', label: '加工速度', icon: '\u26A1', desc: '信息处理和反应速度' },
+        { key: 'languageScore', label: '语言表达', icon: '\u{1F60B}', desc: '词汇理解与语言组织能力' }
       ]
     }
   },

+ 872 - 0
docs/superpowers/plans/2026-09-19-cognitive-game-assessment.md

@@ -0,0 +1,872 @@
+# 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):
+
+```java
+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:
+
+```java
+case "languageScore":
+    latest.setLanguageScore(score);
+    break;
+```
+
+Also update the overall score calculation loop (lines 139–144) to include `languageScore`:
+
+```java
+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):
+
+```java
+// 迁移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:
+
+```sql
+language_score INT COMMENT '语言表达得分',
+```
+
+- [ ] **Step 5: Compile verification**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: exit code 0, no errors.
+
+- [ ] **Step 6: Commit**
+
+```bash
+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:
+
+```vue
+<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:
+
+```json
+        {
+          "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:
+```bash
+node -e "JSON.parse(require('fs').readFileSync('cfc-frontend/pages.json','utf8')); console.log('pages.json OK')"
+```
+Expected: `pages.json OK`
+
+- [ ] **Step 4: Commit**
+
+```bash
+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`:
+
+```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:
+
+```json
+{
+  "path": "pages/cognitive-hall",
+  "style": {
+    "navigationBarTitleText": "认知能力测评"
+  }
+}
+```
+
+- [ ] **Step 3: Verify JSON**
+
+```bash
+node -e "JSON.parse(require('fs').readFileSync('cfc-frontend/pages.json','utf8')); console.log('pages.json OK')"
+```
+Expected: `pages.json OK`
+
+- [ ] **Step 4: Commit**
+
+```bash
+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:
+
+```javascript
+{ key: 'languageScore', label: '语言表达', icon: '🗣️', desc: '词汇理解与语言组织能力' }
+```
+
+The full updated array should be:
+```javascript
+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:
+```html
+<text class="empty-desc">完成 DAN 认知评估后,可在此查看详细的六维认知分析报告</text>
+```
+To:
+```html
+<text class="empty-desc">完成认知能力游戏测评或DAN评估后,可在此查看详细的七维认知分析报告</text>
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+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**
+
+```bash
+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).