|
@@ -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).
|