Преглед на файлове

fix: resolve merge conflict + upgrade.vue template root element

- 解决 DatabaseInitializer.java 和 FamilyMemberVO.java 冲突标记
- 修复 membership/upgrade.vue 模板多根节点错误(wrapper 包裹)
User преди 2 месеца
родител
ревизия
ef0116123f

+ 0 - 6
cfc-backend/.classpath

@@ -39,17 +39,11 @@
 	<classpathentry kind="src" path="target/generated-sources/annotations">
 		<attributes>
 			<attribute name="optional" value="true"/>
-			<attribute name="maven.pomderived" value="true"/>
-			<attribute name="ignore_optional_problems" value="true"/>
-			<attribute name="m2e-apt" value="true"/>
 		</attributes>
 	</classpathentry>
 	<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
 		<attributes>
 			<attribute name="optional" value="true"/>
-			<attribute name="maven.pomderived" value="true"/>
-			<attribute name="ignore_optional_problems" value="true"/>
-			<attribute name="m2e-apt" value="true"/>
 			<attribute name="test" value="true"/>
 		</attributes>
 	</classpathentry>

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

@@ -2547,7 +2547,6 @@ log.info("已添加template_id列到tasks表");
             log.warn("numsoul_config 种子数据初始化失败: {}", e.getMessage());
         }
 
-        // Migration: user_address table (收货地址)
         try {
             jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS user_address (" +
                     "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
@@ -2568,74 +2567,6 @@ log.info("已添加template_id列到tasks表");
             log.warn("创建 user_address 表失败: {}", e.getMessage());
         }
 
-        // Migration: dan_assessment_results 表添加 v1.2 EMI 字段
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN emotion_score INT DEFAULT NULL COMMENT '情绪商数 0-100'");
-            log.info("已添加 emotion_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN resilience_score INT DEFAULT NULL COMMENT '心理韧性 0-100'");
-            log.info("已添加 resilience_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN stress_coping_score INT DEFAULT NULL COMMENT '压力应对 0-100'");
-            log.info("已添加 stress_coping_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN self_awareness_score INT DEFAULT NULL COMMENT '自我认知 0-100'");
-            log.info("已添加 self_awareness_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-
-        // Migration: dan_assessment_results 表添加 Big Five 人格字段
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN openness_score INT DEFAULT NULL COMMENT '开放性 0-100'");
-            log.info("已添加 openness_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN conscientiousness_score INT DEFAULT NULL COMMENT '尽责性 0-100'");
-            log.info("已添加 conscientiousness_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN extraversion_score INT DEFAULT NULL COMMENT '外向性 0-100'");
-            log.info("已添加 extraversion_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN agreeableness_score INT DEFAULT NULL COMMENT '宜人性 0-100'");
-            log.info("已添加 agreeableness_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN neuroticism_score INT DEFAULT NULL COMMENT '神经质性 0-100'");
-            log.info("已添加 neuroticism_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-        try {
-            jdbcTemplate.execute("ALTER TABLE dan_assessment_results ADD COLUMN big_five_overall_score INT DEFAULT NULL COMMENT '大五人格综合分 0-100'");
-            log.info("已添加 big_five_overall_score 列到 dan_assessment_results 表");
-        } catch (Exception e) { /* 列已存在,忽略 */ }
-
-        // Migration: 创建 emotion_checkin 表(情绪打卡)
-        try {
-            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS emotion_checkin (" +
-                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
-                    "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
-                    "mood_weather VARCHAR(20) NOT NULL COMMENT '天气类比: sunny/cloudy/rainy/stormy/rainbow', " +
-                    "mood_score INT COMMENT '情绪值 1-10', " +
-                    "emotion_tags VARCHAR(200) COMMENT '情绪标签JSON', " +
-                    "emotion_type VARCHAR(50) COMMENT '情绪类型', " +
-                    "stress_level INT COMMENT '压力值 1-10', " +
-                    "photo_url VARCHAR(500) COMMENT '拍照URL', " +
-                    "note TEXT COMMENT '文字记录', " +
-                    "energy_awarded INT DEFAULT 5 COMMENT '本次获得心能量', " +
-                    "checkin_date DATE NOT NULL COMMENT '打卡日期', " +
-                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
-                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
-                    "INDEX idx_child_date (child_id, checkin_date), " +
-                    "INDEX idx_child_weather (child_id, mood_weather)" +
-                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='情绪打卡记录'");
-            log.info("已创建 emotion_checkin 表");
-        } catch (Exception e) {
-            log.warn("创建 emotion_checkin 表失败: {}", e.getMessage());
-        }
-
         log.info("数据库迁移完成");
 
         // ==================== 健康维度 Phase 1: health_dimension_score / health_data_source_record / health_norm_reference ====================

+ 122 - 52
cfc-frontend/__tests__/components/FamilyRelationGraph.test.js

@@ -1,16 +1,21 @@
-var assert = require('assert')
+/**
+ * Tests for FamilyRelationGraph helper functions
+ */
+import { shallowMount } from '@vue/test-utils'
+
+// --------------- helper functions (extracted from component) ---------------
 
 function getEnergy(energyMap, memberId, dimensionCode) {
-  var data = energyMap[memberId]
+  const data = energyMap[memberId]
   if (!data) return 0
-  var key = dimensionCode + 'Score'
-  var val = data[key]
+  const key = dimensionCode + 'Score'
+  const val = data[key]
   return (typeof val !== 'undefined' && val !== null) ? val : 0
 }
 
 function getAvatarSize(energy, isSelf) {
-  var clamped = Math.max(0, Math.min(100, energy))
-  var base = 64 + (clamped / 100) * 56
+  const clamped = Math.max(0, Math.min(100, energy))
+  const base = 64 + (clamped / 100) * 56
   if (isSelf) return Math.max(96, base + 16)
   return base
 }
@@ -21,10 +26,10 @@ function getLayoutMode(memberCount) {
 }
 
 function getDisplayOrder(members, selfId) {
-  var self = null
-  var others = []
-  for (var i = 0; i < members.length; i++) {
-    if (members[i].id == selfId) {
+  let self = null
+  const others = []
+  for (let i = 0; i < members.length; i++) {
+    if (members[i].id === selfId) {
       self = members[i]
     } else {
       others.push(members[i])
@@ -36,12 +41,12 @@ function getDisplayOrder(members, selfId) {
 }
 
 function getLineEndPoints(selfCX, selfCY, selfR, otherCX, otherCY, otherR) {
-  var dx = otherCX - selfCX
-  var dy = otherCY - selfCY
-  var dist = Math.sqrt(dx * dx + dy * dy)
+  const dx = otherCX - selfCX
+  const dy = otherCY - selfCY
+  const dist = Math.sqrt(dx * dx + dy * dy)
   if (dist < 1) return null
-  var ux = dx / dist
-  var uy = dy / dist
+  const ux = dx / dist
+  const uy = dy / dist
   return {
     x1: selfCX + ux * selfR,
     y1: selfCY + uy * selfR,
@@ -50,40 +55,105 @@ function getLineEndPoints(selfCX, selfCY, selfR, otherCX, otherCY, otherR) {
   }
 }
 
-assert.strictEqual(getAvatarSize(0, false), 64, 'min size for other at energy 0')
-assert.strictEqual(getAvatarSize(100, false), 120, 'max size for other at energy 100')
-assert.strictEqual(getAvatarSize(50, false), 92, 'mid size for other at energy 50')
-assert.strictEqual(getAvatarSize(0, true), 96, 'self min is 96')
-assert.strictEqual(getAvatarSize(100, true), 136, 'self max energy: 64+56+16=136')
-assert.strictEqual(getAvatarSize(-10, false), 64, 'negative energy clamped to 0')
-assert.strictEqual(getAvatarSize(150, false), 120, 'energy >100 clamped to 100')
-
-assert.strictEqual(getLayoutMode(0), 'row', '0 members → row (but v-if hides)')
-assert.strictEqual(getLayoutMode(2), 'row', '2 members → row')
-assert.strictEqual(getLayoutMode(3), 'row', '3 members → row')
-assert.strictEqual(getLayoutMode(4), 'multi-row', '4 members → multi-row')
-assert.strictEqual(getLayoutMode(10), 'multi-row', '10 members → multi-row')
-
-var m2 = [{ id: 1, nickname: '爸' }, { id: 2, nickname: '我' }]
-var order2 = getDisplayOrder(m2, 2)
-assert.strictEqual(order2[0].id, 2, '2-person: self first')
-assert.strictEqual(order2[1].id, 1, '2-person: other second')
-
-var m3 = [{ id: 1, nickname: '爸' }, { id: 2, nickname: '我' }, { id: 3, nickname: '妈' }]
-var order3 = getDisplayOrder(m3, 2)
-assert.strictEqual(order3[0].id, 1, '3-person: other1 left')
-assert.strictEqual(order3[1].id, 2, '3-person: self center')
-assert.strictEqual(order3[2].id, 3, '3-person: other2 right')
-
-var pts = getLineEndPoints(100, 100, 30, 300, 100, 20)
-assert.strictEqual(pts.x1, 130, 'line start x = self center + self radius')
-assert.strictEqual(pts.x2, 280, 'line end x = other center - other radius')
-assert.strictEqual(pts.y1, 100, 'line start y = self center y')
-assert.strictEqual(pts.y2, 100, 'line end y = other center y')
-
-var em = { 1: { bodyScore: 80, mindScore: 40 } }
-assert.strictEqual(getEnergy(em, 1, 'body'), 80, 'get body energy')
-assert.strictEqual(getEnergy(em, 1, 'mind'), 40, 'get mind energy')
-assert.strictEqual(getEnergy(em, 2, 'body'), 0, 'missing member returns 0')
-
-console.log('All FamilyRelationGraph tests passed!')
+// --------------- tests ---------------
+
+describe('getAvatarSize', () => {
+  it('returns min size for other at energy 0', () => {
+    expect(getAvatarSize(0, false)).toBe(64)
+  })
+
+  it('returns max size for other at energy 100', () => {
+    expect(getAvatarSize(100, false)).toBe(120)
+  })
+
+  it('returns mid size for other at energy 50', () => {
+    expect(getAvatarSize(50, false)).toBe(92)
+  })
+
+  it('self min is 96', () => {
+    expect(getAvatarSize(0, true)).toBe(96)
+  })
+
+  it('self max at energy 100', () => {
+    expect(getAvatarSize(100, true)).toBe(136)
+  })
+
+  it('clamps negative energy to 0', () => {
+    expect(getAvatarSize(-10, false)).toBe(64)
+  })
+
+  it('clamps energy > 100 to 100', () => {
+    expect(getAvatarSize(150, false)).toBe(120)
+  })
+})
+
+describe('getLayoutMode', () => {
+  it('0 members returns row', () => {
+    expect(getLayoutMode(0)).toBe('row')
+  })
+
+  it('2 members returns row', () => {
+    expect(getLayoutMode(2)).toBe('row')
+  })
+
+  it('3 members returns row', () => {
+    expect(getLayoutMode(3)).toBe('row')
+  })
+
+  it('4 members returns multi-row', () => {
+    expect(getLayoutMode(4)).toBe('multi-row')
+  })
+
+  it('10 members returns multi-row', () => {
+    expect(getLayoutMode(10)).toBe('multi-row')
+  })
+})
+
+describe('getDisplayOrder', () => {
+  it('2-person: self first, other second', () => {
+    const members = [{ id: 1, nickname: '爸' }, { id: 2, nickname: '我' }]
+    const order = getDisplayOrder(members, 2)
+    expect(order[0].id).toBe(2)
+    expect(order[1].id).toBe(1)
+  })
+
+  it('3-person: others on sides, self center', () => {
+    const members = [{ id: 1, nickname: '爸' }, { id: 2, nickname: '我' }, { id: 3, nickname: '妈' }]
+    const order = getDisplayOrder(members, 2)
+    expect(order[0].id).toBe(1)
+    expect(order[1].id).toBe(2)
+    expect(order[2].id).toBe(3)
+  })
+})
+
+describe('getLineEndPoints', () => {
+  it('calculates correct horizontal line endpoints', () => {
+    const pts = getLineEndPoints(100, 100, 30, 300, 100, 20)
+    expect(pts.x1).toBe(130)
+    expect(pts.x2).toBe(280)
+    expect(pts.y1).toBe(100)
+    expect(pts.y2).toBe(100)
+  })
+
+  it('returns null when distance < 1', () => {
+    const pts = getLineEndPoints(100, 100, 30, 100, 100, 20)
+    expect(pts).toBeNull()
+  })
+})
+
+describe('getEnergy', () => {
+  it('retrieves body energy by dimension code', () => {
+    const energyMap = { 1: { bodyScore: 80, mindScore: 40 } }
+    expect(getEnergy(energyMap, 1, 'body')).toBe(80)
+  })
+
+  it('retrieves mind energy by dimension code', () => {
+    const energyMap = { 1: { bodyScore: 80, mindScore: 40 } }
+    expect(getEnergy(energyMap, 1, 'mind')).toBe(40)
+  })
+
+  it('returns 0 for missing member', () => {
+    const energyMap = { 1: { bodyScore: 80 } }
+    expect(getEnergy(energyMap, 2, 'body')).toBe(0)
+  })
+})

+ 2 - 0
cfc-frontend/package.json

@@ -4,6 +4,8 @@
   "description": "浠艾福小程序前端",
   "main": "main.js",
   "scripts": {
+    "test": "jest",
+    "test:coverage": "jest --coverage",
     "postbuild": "node -e \"try{require('fs').unlinkSync('dist/dev/mp-weixin/custom-tab-bar/index.vue')}catch(e){};try{const c=require('./dist/dev/mp-weixin/project.config.json');c.setting=c.setting||{};c.setting.lazyCodeLoading='requiredComponents';require('fs').writeFileSync('./dist/dev/mp-weixin/project.config.json',JSON.stringify(c,null,2))}catch(e){}\""
   },
   "devDependencies": {

+ 75 - 2
cfc-frontend/pages/action/index.vue

@@ -63,6 +63,33 @@
       @productClick="goProductDetail"
       @moreProducts="goMoreProducts" />
 
+    <!-- 推荐阅读 -->
+    <view class="section" v-if="sectionVisible('recommended_reading') && actionArticles.length > 0">
+      <view class="section-header">
+        <text class="section-title">推荐阅读</text>
+      </view>
+      <view class="article-list">
+        <view class="article-card" v-for="article in actionArticles" :key="article.id" @click="goArticleDetail(article.id)">
+          <view class="article-category" :style="{ background: article.categoryColor }">
+            <text class="article-category-text">{{ article.category }}</text>
+          </view>
+          <text class="article-title">{{ article.title }}</text>
+          <text class="article-summary">{{ article.summary }}</text>
+          <view class="article-meta">
+            <view class="meta-item">
+              <text class="meta-icon">&#x1F4D6;</text>
+              <text class="meta-text">{{ article.views }}</text>
+            </view>
+            <view class="meta-item">
+              <text class="meta-icon">&#x1F44D;</text>
+              <text class="meta-text">{{ article.likes }}</text>
+            </view>
+            <text class="article-date">{{ article.date }}</text>
+          </view>
+        </view>
+      </view>
+    </view>
+
     <!-- ===== 重要关系维护 ===== -->
     <view class="contact-section" v-if="isLoggedIn">
       <view class="contact-header">
@@ -124,7 +151,7 @@ import DimensionProducts from '../../components/DimensionProducts.vue'
 import ContactCard from '../../components/ContactCard.vue'
 import ContactImport from '../../components/ContactImport.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
-import { getVisibleSections, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getContactList, getVisibleFamilyMembers } from '../../utils/api.js'
+import { getVisibleSections, getEnergyOverview, getFamilyEnergySandbox, getTodayTasksByCategory, getActivityList, getProductsByDomain, getChildren, getContactList, getVisibleFamilyMembers, getFeaturedArticles } from '../../utils/api.js'
 
 export default {
   components: { PageBanner, LoginGuideCard, FamilyEnergyBar, UserQuickEntry, DimensionTasks, DimensionActivities, DimensionProducts, ContactCard, ContactImport, FamilyRelationGraph },
@@ -143,6 +170,7 @@ export default {
       dimensionTasks: [],
       dimensionActivities: [],
       dimensionProducts: [],
+      articles: [],
       contactList: [],
       showImportModal: false,
       funcList: [
@@ -170,6 +198,18 @@ export default {
     },
     intimacyMapForGraph: function() {
       return {}
+    },
+    actionArticles: function() {
+      if (!this.articles || this.articles.length === 0) return []
+      var result = []
+      for (var i = 0; i < this.articles.length; i++) {
+        var a = this.articles[i]
+        if (a.relatedDimensions && a.relatedDimensions.indexOf('action') !== -1) {
+          result.push(a)
+        }
+      }
+      if (result.length === 0) return this.articles.slice(0, 5)
+      return result.slice(0, 5)
     }
   },
   onShow() {
@@ -188,6 +228,8 @@ export default {
     // 游客也能浏览活动和商品
     this.loadDimensionActivities()
     this.loadDimensionProducts()
+    // 游客也能浏览文章
+    this.loadFeaturedArticles()
   },
   methods: {
     loadSectionConfig: function() {
@@ -383,7 +425,38 @@ export default {
         url: '/pages/action/member-action-detail?childId=' + member.memberId
       })
     },
-    goLogin: function() { uni.navigateTo({ url: '/pages/login/login' }) }
+    goLogin: function() { uni.navigateTo({ url: '/pages/login/login' }) },
+    loadFeaturedArticles: function() {
+      var self = this
+      getFeaturedArticles({ size: 5 }).then(function(res) {
+        if (res.code === 200 && res.data) {
+          var gradientColors = [
+            'linear-gradient(135deg, #10B981, #34D399)',
+            'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
+            'linear-gradient(135deg, #8B5CF6, #C084FC)',
+            'linear-gradient(135deg, #F97316, #FB923C)',
+            'linear-gradient(135deg, #6366F1, #818CF8)'
+          ]
+          var list = Array.isArray(res.data) ? res.data : (res.data.records || [])
+          self.articles = list.map(function(item, index) {
+            return {
+              id: item.id,
+              category: item.categoryName || '行动文章',
+              categoryColor: gradientColors[index % gradientColors.length],
+              title: item.title || '',
+              summary: item.summary || '',
+              views: item.viewCount ? String(item.viewCount) : '0',
+              likes: '0',
+              date: item.publishedAt ? item.publishedAt.slice(0, 10) : '',
+              relatedDimensions: item.relatedDimensions || []
+            }
+          })
+        }
+      }).catch(function() {})
+    },
+    goArticleDetail: function(id) {
+      uni.navigateTo({ url: '/pages/action/article-detail?id=' + id })
+    }
   }
 }
 </script>

+ 2 - 2
cfc-frontend/pages/growth/supplement.vue

@@ -10,7 +10,7 @@
         <text class="label">报告日期</text>
         <picker mode="date" :value="reportDate" @change="onDateChange">
           <view class="picker-input">{{ reportDate || '请选择日期' }}</view>
-        </view>
+        </picker>
       </view>
 
       <view class="form-item">
@@ -30,7 +30,7 @@
       <button class="btn-submit" @click="uploadAndParse" :disabled="!canUpload">上传并解析</button>
     </view>
 
-    <view class="form" v-if="step === 2">
+    <view class="form" v-else-if="step === 2">
       <view class="form-item">
         <text class="label">解析结果预览</text>
       </view>

+ 2 - 0
cfc-frontend/pages/membership/upgrade.vue

@@ -1,4 +1,5 @@
 <template>
+  <view class="upgrade-page-wrapper">
   <scroll-view class="page" scroll-y>
     <!-- 当前会员状态卡片 -->
     <view class="status-card">
@@ -137,6 +138,7 @@
       </view>
     </view>
   </view>
+  </view>
 </template>
 
 <script>

+ 765 - 548
cfc-frontend/pages/wisdom/self-quiz.vue

@@ -1,194 +1,132 @@
 <template>
   <view class="container">
-    <!-- 顶部导航 -->
-    <view class="nav-bar">
-      <view class="nav-back" @click="confirmQuit">
-        <text class="nav-back-icon">&#x2190;</text>
+    <view class="resume-prompt" v-if="showResume">
+      <text class="resume-text">检测到上次未完成的测评</text>
+      <view class="resume-btns">
+        <view class="btn btn--outline" @tap="resumeSession">继续答题</view>
+        <view class="btn btn--ghost" @tap="clearSession">重新开始</view>
       </view>
-      <text class="nav-title">数学闯关</text>
-      <view class="nav-placeholder"></view>
     </view>
 
-    <!-- ================================================================== -->
-    <!-- Step 1: 设定挑战 -->
-    <!-- ================================================================== -->
-    <view class="step-content" v-if="currentStep === 1">
-      <view class="level-card" v-if="sessionInfo">
-        <text class="level-title">当前等级</text>
-        <text class="level-name">{{ sessionInfo.levelName }}</text>
-        <text class="level-desc" v-if="sessionInfo.totalPassed > 0">
-          已通关 {{ sessionInfo.totalPassed }} 次
-          <text v-if="sessionInfo.bestAccuracy">
-            · 最佳正确率 {{ sessionInfo.bestAccuracy }}%
-          </text>
-        </text>
-      </view>
-
-      <view class="age-hint" v-if="sessionInfo">
-        <text class="hint-text">匹配年龄 {{ sessionInfo.age }} 岁 · 建议目标 {{ sessionInfo.suggestedTargetN }} 题</text>
-      </view>
-
-      <!-- 目标设定 -->
-      <view class="content-card">
-        <text class="content-title">设定挑战目标</text>
-        <text class="content-desc">选择连续答对多少题算通关</text>
-
-        <view class="target-row">
-          <view class="target-btn" @click="adjustTarget(-1)">−</view>
-          <view class="target-display">
-            <text class="target-num">{{ targetN }}</text>
-            <text class="target-unit">题</text>
+    <view class="step-wrap" v-if="!showResume">
+      <view class="step-row">
+        <view class="step-item" v-for="i in 4" :key="i" @tap="goStep(i)">
+          <view class="step-dot" :class="{
+            'step-dot--active': step === i,
+            'step-dot--done': step > i,
+            'step-dot--pending': step < i
+          }">
+            <text v-if="step > i" class="step-check">&#10003;</text>
+            <text v-else class="step-num">{{ i }}</text>
           </view>
-          <view class="target-btn" @click="adjustTarget(1)">+</view>
+          <text class="step-label" :class="{
+            'step-label--active': step >= i,
+            'step-label--muted': step < i
+          }">{{ stepLabels[i - 1] }}</text>
         </view>
-        <text class="range-hint">范围 2 ~ 15 题</text>
+      </view>
+    </view>
 
-        <!-- 等级选择 -->
-        <view class="level-selector" v-if="sessionInfo">
-          <text class="level-select-label">难度</text>
-          <view class="level-options">
-            <view class="level-option"
-              :class="{ active: selectedLevel === 1 }"
-              @click="selectedLevel = 1">一位数<br>加减</view>
-            <view class="level-option"
-              :class="{ active: selectedLevel === 2 }"
-              @click="selectedLevel = 2">两位数<br>加减</view>
-            <view class="level-option"
-              :class="{ active: selectedLevel === 3 }"
-              @click="selectedLevel = 3">一位数<br>四则</view>
-            <view class="level-option"
-              :class="{ active: selectedLevel === 4 }"
-              @click="selectedLevel = 4">两位数<br>四则</view>
+    <view class="step-content" v-if="step === 1 && !showResume">
+      <view class="content-header">
+        <text class="content-title">选择测评目标</text>
+        <text class="content-sub">选择你想提升的能力维度</text>
+      </view>
+      <view class="goal-grid">
+        <view class="goal-card" v-for="g in goals" :key="g.id" :class="{ 'goal-card--selected': selectedGoal && selectedGoal.id === g.id }" @tap="selectGoal(g)">
+          <view class="goal-icon-wrap">
+            <text class="goal-icon">{{ g.icon }}</text>
           </view>
+          <text class="goal-title">{{ g.title }}</text>
+          <text class="goal-desc">{{ g.desc }}</text>
         </view>
-
-        <view class="start-btn" @click="startChallenge">
-          <text class="start-btn-text">开始挑战</text>
-        </view>
+      </view>
+      <view class="action-bar">
+        <view class="btn btn--primary" :class="{ 'btn--disabled': !selectedGoal }" @tap="goStep2">下一步</view>
       </view>
     </view>
 
-    <!-- ================================================================== -->
-    <!-- Step 2: 出题答题 -->
-    <!-- ================================================================== -->
-    <view class="step-content" v-if="currentStep === 2">
-      <!-- 进度条 -->
-      <view class="progress-bar">
-        <view class="progress-fill" :style="{ width: progressPercent + '%' }"></view>
+    <view class="step-content" v-if="step === 2 && !showResume">
+      <view class="content-header">
+        <text class="content-title">选择题目类型</text>
+        <text class="content-sub">共5道题,选择适合的答题方式</text>
       </view>
-      <view class="progress-info">
-        <text class="progress-text">连续答对 {{ correctCount }} / {{ targetN }}</text>
-        <text class="timer-text">{{ formattedTime }}</text>
-      </view>
-
-      <!-- 已出题目列表 -->
-      <view class="expr-list" v-if="askedExpressions.length > 0">
-        <view class="expr-item" v-for="(item, i) in askedExpressions" :key="i">
-          <text class="expr-index">{{ i + 1 }}.</text>
-          <text class="expr-text">{{ item.expr }}</text>
-          <text class="expr-result" :class="item.correct ? 'correct' : 'wrong'">
-            {{ item.correct ? '✓' : '✗' }}
-          </text>
+      <view class="type-list">
+        <view class="type-card" v-for="t in questionTypes" :key="t.id" :class="{ 'type-card--selected': questionType === t.id }" @tap="selectType(t.id)">
+          <view class="type-icon-wrap">
+            <text class="type-icon">{{ t.icon }}</text>
+          </view>
+          <view class="type-info">
+            <text class="type-name">{{ t.name }}</text>
+            <text class="type-desc">{{ t.desc }}</text>
+          </view>
+          <view class="type-radio" :class="{ 'type-radio--checked': questionType === t.id }">
+            <view v-if="questionType === t.id" class="type-radio-dot"></view>
+          </view>
         </view>
       </view>
+      <view class="action-bar action-bar--double">
+        <view class="btn btn--outline" @tap="goStep1">上一步</view>
+        <view class="btn btn--primary" :class="{ 'btn--disabled': !questionType }" @tap="startQuiz">开始答题</view>
+      </view>
+    </view>
 
-      <!-- 出题输入区 -->
-      <view class="quiz-card">
-        <view class="input-group">
-          <text class="input-label">写出算式</text>
-          <input class="expr-input" v-model="currentExpression"
-            placeholder="例如:12+38" maxlength="100"
-            :disabled="waitingVerify" />
-        </view>
-        <view class="input-group">
-          <text class="input-label">你的答案</text>
-          <input class="answer-input" v-model="currentAnswer" type="text"
-            placeholder="输入答案" maxlength="50"
-            :disabled="waitingVerify" />
+    <view class="step-content" v-if="step === 3 && !showResume">
+      <view class="progress-header">
+        <text class="progress-label">第 {{ currentQ + 1 }}/{{ questions.length }} 题</text>
+        <view class="progress-bar">
+          <view class="progress-fill" :style="'width:' + progressPct + '%'"></view>
         </view>
-
-        <view class="submit-row">
-          <view class="submit-btn" @click="submitAnswer"
-            v-if="!waitingVerify && currentExpression.trim() && currentAnswer.trim()">
-            <text class="submit-btn-text">提交验证</text>
-          </view>
-          <view class="submit-btn disabled" v-else-if="waitingVerify">
-            <text class="submit-btn-text">AI 验证中...</text>
+      </view>
+      <view class="question-card" v-if="currentQuestion">
+        <view class="q-tag">{{ tagLabel }}</view>
+        <text class="q-text">{{ currentQuestion.question }}</text>
+        <view class="q-options" v-if="currentQuestion.isTF">
+          <view class="q-option q-option--tf" :class="{ 'q-option--selected': currentAnswer === '正确' }" @tap="currentAnswer = '正确'">
+            <text class="q-option-tf-text">正确</text>
           </view>
-          <view class="submit-btn disabled" v-else>
-            <text class="submit-btn-text">请填写算式和答案</text>
+          <view class="q-option q-option--tf" :class="{ 'q-option--selected': currentAnswer === '错误' }" @tap="currentAnswer = '错误'">
+            <text class="q-option-tf-text">错误</text>
           </view>
         </view>
-
-        <!-- AI 反馈 -->
-        <view class="feedback" v-if="feedbackText">
-          <view class="feedback-card" :class="lastCorrect ? 'fc-correct' : 'fc-wrong'">
-            <text class="fb-icon">{{ lastCorrect ? '🎉' : '💪' }}</text>
-            <view class="fb-content">
-              <text class="fb-title">{{ lastCorrect ? '正确!' : '再想想' }}</text>
-              <text class="fb-desc">{{ feedbackText }}</text>
+        <view class="q-options" v-else>
+          <view class="q-option" v-for="(opt, oi) in currentQuestion.options" :key="oi" :class="{ 'q-option--selected': currentAnswer === opt }" @tap="currentAnswer = opt">
+            <view class="q-radio" :class="{ 'q-radio--checked': currentAnswer === opt }">
+              <view v-if="currentAnswer === opt" class="q-radio-dot"></view>
             </view>
-          </view>
-          <view class="continue-btn" @click="continueQuiz" v-if="!waitingVerify">
-            <text class="continue-btn-text">{{ correctCount >= targetN ? '查看结果' : '下一题' }}</text>
+            <text class="q-option-text">{{ opt }}</text>
           </view>
         </view>
       </view>
-
-      <!-- 放弃按钮 -->
-      <view class="quit-row" v-if="!waitingVerify">
-        <view class="quit-btn" @click="confirmQuit">
-          <text class="quit-btn-text">放弃挑战</text>
-        </view>
+      <view class="action-bar action-bar--double">
+        <view class="btn btn--outline" :class="{ 'btn--disabled': currentQ === 0 }" @tap="prevQuestion">上一题</view>
+        <view class="btn btn--primary" :class="{ 'btn--disabled': !currentAnswer }" @tap="nextQuestion">{{ currentQ < questions.length - 1 ? '下一题' : '查看结果' }}</view>
       </view>
     </view>
 
-    <!-- ================================================================== -->
-    <!-- Step 3: 过关统计 -->
-    <!-- ================================================================== -->
-    <view class="step-content" v-if="currentStep === 3">
-      <view class="result-card">
-        <text class="result-icon">🏆</text>
-        <text class="result-title" v-if="passResult.passed">恭喜通关!</text>
-        <text class="result-title" v-else>挑战结束</text>
-
-        <view class="stats-row">
-          <view class="stat-item">
-            <text class="stat-value">{{ passResult.accuracy }}%</text>
-            <text class="stat-label">正确率</text>
-          </view>
-          <view class="stat-item">
-            <text class="stat-value">{{ passResult.totalAttempts }}</text>
-            <text class="stat-label">总答题</text>
-          </view>
-          <view class="stat-item">
-            <text class="stat-value">{{ formatDuration(passResult.totalTimeMs) }}</text>
-            <text class="stat-label">用时</text>
-          </view>
-        </view>
-
-        <!-- 升级/降级提示 -->
-        <view class="level-change" v-if="passResult.levelUp">
-          <text class="change-icon">⬆</text>
-          <text class="change-text">{{ passResult.reason }}</text>
-        </view>
-        <view class="level-change down" v-if="passResult.levelDown">
-          <text class="change-icon">⬇</text>
-          <text class="change-text">{{ passResult.reason }}</text>
-        </view>
-        <view class="level-change stay" v-if="!passResult.levelUp && !passResult.levelDown">
-          <text class="change-text">{{ passResult.reason }}</text>
+    <view class="step-content" v-if="step === 4 && !showResume">
+      <view class="result-header">
+        <view class="score-circle">
+          <text class="score-value">{{ score }}/5</text>
         </view>
-
-        <view class="result-actions">
-          <view class="action-btn primary" @click="playAgain">
-            <text class="action-btn-text">再来一轮</text>
+        <text class="score-label">{{ resultMessage }}</text>
+      </view>
+      <view class="section-title">维度分析</view>
+      <view class="result-dims">
+        <view class="dim-bar-item" v-for="(dim, di) in dimBreakdown" :key="di">
+          <view class="dim-bar-header">
+            <text class="dim-bar-name">{{ dim.name }}</text>
+            <text class="dim-bar-count">{{ dim.correct }}/{{ dim.total }}</text>
           </view>
-          <view class="action-btn secondary" @click="goBack">
-            <text class="action-btn-text">返回</text>
+          <view class="dim-bar-track">
+            <view class="dim-bar-fill" :style="'width:' + dim.pct + '%'"></view>
           </view>
         </view>
       </view>
+      <view class="action-bar">
+        <view class="btn btn--primary" @tap="resetQuiz">再测一次</view>
+        <view class="btn btn--outline" @tap="goHome">返回首页</view>
+      </view>
     </view>
 
     <view class="bottom-spacer"></view>
@@ -196,219 +134,330 @@
 </template>
 
 <script>
-import { getMathQuizStart, verifyMathAnswer, completeMathQuiz } from '../../utils/api.js'
+var QUESTION_BANK = [
+  { dimension: 'focus', question: '以下哪个因素最容易影响注意力集中?', options: ['噪音', '温度', '光照', '以上都是'], answer: '以上都是' },
+  { dimension: 'focus', question: '持续专注工作建议多久休息一次?', options: ['15分钟', '30分钟', '45分钟', '60分钟'], answer: '45分钟' },
+  { dimension: 'focus', question: '舒尔特方格的训练目标是提升什么?', options: ['记忆力', '专注力', '逻辑思维', '创造力'], answer: '专注力' },
+  { dimension: 'memory', question: '短期记忆通常能记住几个信息组块?', options: ['3-4个', '5-9个', '10-15个', '20个以上'], answer: '5-9个' },
+  { dimension: 'memory', question: '以下哪个方法有助于增强记忆?', options: ['死记硬背', '联想记忆', '反复抄写', '一次性学习'], answer: '联想记忆' },
+  { dimension: 'memory', question: '睡眠对记忆的影响是什么?', options: ['削弱记忆', '巩固记忆', '无影响', '干扰记忆'], answer: '巩固记忆' },
+  { dimension: 'logic', question: '如果A>B且B>C,那么?', options: ['A=C', 'A>C', 'A<C', '不确定'], answer: 'A>C' },
+  { dimension: 'logic', question: '1, 1, 2, 3, 5, 8, ? 下一个数是多少?', options: ['10', '11', '12', '13'], answer: '13' },
+  { dimension: 'logic', question: '以下哪个不是有效推理方式?', options: ['归纳推理', '演绎推理', '循环论证', '类比推理'], answer: '循环论证' },
+  { dimension: 'perception', question: '人眼能分辨约多少种颜色?', options: ['几百种', '几千种', '几万种', '数百万种'], answer: '数百万种' },
+  { dimension: 'perception', question: '以下哪种错觉与视觉感知有关?', options: ['缪勒-莱尔错觉', '德布罗意错觉', '薛定谔错觉', '迈克尔逊错觉'], answer: '缪勒-莱尔错觉' },
+  { dimension: 'perception', question: '感觉适应是指什么?', options: ['敏感度降低', '敏感度升高', '感知消失', '感知增强'], answer: '敏感度降低' },
+  { dimension: 'spatial', question: '正方体有多少个面?', options: ['4', '6', '8', '12'], answer: '6' },
+  { dimension: 'spatial', question: '以下哪个是立体几何的基本要素?', options: ['点线面', '正反合', '红黄蓝', '上下左右'], answer: '点线面' },
+  { dimension: 'spatial', question: '一个立方体展开图有几个正方形?', options: ['4', '6', '8', '12'], answer: '6' },
+  { dimension: 'speed', question: '反应时实验测量的是什么?', options: ['思考速度', '反应速度', '运动速度', '阅读速度'], answer: '反应速度' },
+  { dimension: 'speed', question: '以下哪个因素会影响加工速度?', options: ['年龄', '情绪', '疲劳', '以上都是'], answer: '以上都是' },
+  { dimension: 'speed', question: '信息加工速度最快的年龄段是?', options: ['儿童期', '青春期', '成年早期', '中老年期'], answer: '成年早期' }
+]
 
 export default {
-  data: function() {
+  data() {
     return {
-      currentStep: 1,
-      sessionInfo: null,
-      childId: null,
-      age: null,
-      targetN: 5,
-      selectedLevel: 1,
-
-      correctCount: 0,
-      totalAttempts: 0,
-      startTime: null,
-      timerHandle: null,
-      elapsedSeconds: 0,
-      askedExpressions: [],
-      askedSet: {},
-
-      currentExpression: '',
-      currentAnswer: '',
-      waitingVerify: false,
-      lastCorrect: false,
-      feedbackText: '',
-
-      passResult: {}
+      step: 1,
+      selectedGoal: null,
+      questionType: null,
+      questions: [],
+      currentQ: 0,
+      answers: [],
+      showResume: false,
+      savedSession: null,
+      goals: [
+        { id: 'focus', title: '专注力', desc: '提升注意力集中能力', icon: '集' },
+        { id: 'memory', title: '记忆力', desc: '增强信息记忆与回忆能力', icon: '忆' },
+        { id: 'logic', title: '逻辑思维', desc: '锻炼逻辑推理与判断能力', icon: '逻' },
+        { id: 'perception', title: '感知觉', desc: '提高感官信息处理能力', icon: '感' },
+        { id: 'spatial', title: '空间思维', desc: '培养空间想象与构建能力', icon: '间' },
+        { id: 'speed', title: '加工速度', desc: '加快信息处理与反应速度', icon: '速' }
+      ],
+      questionTypes: [
+        { id: 'choice', name: '选择题', desc: '四选一标准选择题', icon: '选' },
+        { id: 'truefalse', name: '判断题', desc: '判断陈述正误', icon: '判' },
+        { id: 'mix', name: '混合题型', desc: '选择题与判断题混合', icon: '混' }
+      ],
+      stepLabels: ['目标', '题型', '答题', '结果']
     }
   },
   computed: {
-    progressPercent: function() {
-      if (this.targetN <= 0) return 0
-      return Math.min(100, (this.correctCount / this.targetN) * 100)
+    currentQuestion: function() {
+      if (this.questions.length === 0) {
+        return null
+      }
+      return this.questions[this.currentQ]
     },
-    formattedTime: function() {
-      var m = Math.floor(this.elapsedSeconds / 60)
-      var s = this.elapsedSeconds % 60
-      return (m > 0 ? m + '分' : '') + s + '秒'
+    currentAnswer: {
+      get: function() {
+        return this.answers[this.currentQ] || null
+      },
+      set: function(val) {
+        this.$set(this.answers, this.currentQ, val)
+      }
+    },
+    progressPct: function() {
+      if (this.questions.length === 0) {
+        return 0
+      }
+      return ((this.currentQ + 1) / this.questions.length) * 100
+    },
+    score: function() {
+      var correct = 0
+      for (var i = 0; i < this.questions.length; i++) {
+        var q = this.questions[i]
+        var a = this.answers[i]
+        if (q && a && q.answer === a) {
+          correct++
+        }
+      }
+      return correct
+    },
+    resultMessage: function() {
+      var s = this.score
+      if (s >= 5) return '太棒了!满分通过!'
+      if (s >= 4) return '非常优秀!继续保持!'
+      if (s >= 3) return '表现不错,还有提升空间!'
+      return '继续加油,多练习会更好!'
+    },
+    tagLabel: function() {
+      if (!this.currentQuestion) return ''
+      var dimMap = {
+        focus: '专注力',
+        memory: '记忆力',
+        logic: '逻辑思维',
+        perception: '感知觉',
+        spatial: '空间思维',
+        speed: '加工速度'
+      }
+      return dimMap[this.currentQuestion.dimension] || ''
+    },
+    dimBreakdown: function() {
+      var dimNames = {
+        focus: '专注力',
+        memory: '记忆力',
+        logic: '逻辑思维',
+        perception: '感知觉',
+        spatial: '空间思维',
+        speed: '加工速度'
+      }
+      var dims = {}
+      for (var i = 0; i < this.questions.length; i++) {
+        var q = this.questions[i]
+        if (!q) continue
+        if (!dims[q.dimension]) {
+          dims[q.dimension] = { total: 0, correct: 0 }
+        }
+        dims[q.dimension].total++
+        if (this.answers[i] && q.answer === this.answers[i]) {
+          dims[q.dimension].correct++
+        }
+      }
+      var result = []
+      var keys = Object.keys(dims)
+      for (var k = 0; k < keys.length; k++) {
+        var key = keys[k]
+        var item = dims[key]
+        result.push({
+          name: dimNames[key] || key,
+          total: item.total,
+          correct: item.correct,
+          pct: item.total > 0 ? Math.round((item.correct / item.total) * 100) : 0
+        })
+      }
+      return result
     }
   },
-  onLoad: function(options) {
-    if (options && options.childId) {
-      this.childId = options.childId
-    }
-    this.loadSession()
+  onLoad: function() {
+    this.checkSession()
   },
   onUnload: function() {
-    this.stopTimer()
+    this.saveSession()
   },
   methods: {
-    loadSession: function() {
-      var self = this
-      var childId = this.childId || uni.getStorageSync('currentChildId')
-      this.childId = childId
-      getMathQuizStart(childId, this.age).then(function(res) {
-        if (res.code === 200 && res.data) {
-          self.sessionInfo = res.data
-          self.selectedLevel = res.data.level || 1
-          self.targetN = res.data.suggestedTargetN || 5
-          self.age = res.data.age
+    checkSession: function() {
+      var saved = uni.getStorageSync('selfQuizSession')
+      if (saved) {
+        try {
+          var session = JSON.parse(saved)
+          if (session && session.step && session.step >= 2 && session.step <= 3) {
+            this.showResume = true
+            this.savedSession = session
+          } else {
+            uni.removeStorageSync('selfQuizSession')
+          }
+        } catch (e) {
+          uni.removeStorageSync('selfQuizSession')
         }
-      }).catch(function() {})
+      }
     },
-    adjustTarget: function(delta) {
-      var n = this.targetN + delta
-      if (n >= 2 && n <= 15) {
-        this.targetN = n
+    saveSession: function() {
+      var session = {
+        step: this.step,
+        selectedGoal: this.selectedGoal,
+        questionType: this.questionType,
+        questions: this.questions,
+        currentQ: this.currentQ,
+        answers: this.answers
       }
+      uni.setStorageSync('selfQuizSession', JSON.stringify(session))
     },
-    startChallenge: function() {
-      this.correctCount = 0
-      this.totalAttempts = 0
-      this.askedExpressions = []
-      this.askedSet = {}
-      this.currentExpression = ''
-      this.currentAnswer = ''
-      this.feedbackText = ''
-      this.lastCorrect = false
-      this.waitingVerify = false
-      this.startTime = Date.now()
-      this.elapsedSeconds = 0
-      this.currentStep = 2
-      this.startTimer()
+    resumeSession: function() {
+      if (!this.savedSession) return
+      var s = this.savedSession
+      this.step = s.step
+      this.selectedGoal = s.selectedGoal
+      this.questionType = s.questionType
+      this.questions = s.questions || []
+      this.currentQ = s.currentQ || 0
+      this.answers = s.answers || []
+      this.showResume = false
+      this.savedSession = null
     },
-    startTimer: function() {
-      var self = this
-      this.stopTimer()
-      this.timerHandle = setInterval(function() {
-        self.elapsedSeconds = Math.floor((Date.now() - self.startTime) / 1000)
-      }, 1000)
+    clearSession: function() {
+      uni.removeStorageSync('selfQuizSession')
+      this.showResume = false
+      this.savedSession = null
     },
-    stopTimer: function() {
-      if (this.timerHandle) {
-        clearInterval(this.timerHandle)
-        this.timerHandle = null
+    selectGoal: function(g) {
+      this.selectedGoal = g
+    },
+    goStep2: function() {
+      if (this.selectedGoal) {
+        this.step = 2
+        this.saveSession()
       }
     },
-    submitAnswer: function() {
-      var expr = this.currentExpression.trim()
-      var ans = this.currentAnswer.trim()
-      if (!expr || !ans) return
-
-      var exprKey = expr.replace(/\s+/g, '').toLowerCase()
-      if (this.askedSet[exprKey]) {
-        uni.showToast({ title: '这题出过了,换一题吧', icon: 'none' })
-        return
+    goStep1: function() {
+      this.step = 1
+      this.saveSession()
+    },
+    selectType: function(id) {
+      this.questionType = id
+    },
+    startQuiz: function() {
+      if (!this.questionType) return
+      this.generateQuestions()
+      this.currentQ = 0
+      this.answers = []
+      this.step = 3
+      this.saveSession()
+    },
+    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
       }
-
-      this.waitingVerify = true
-      this.feedbackText = ''
-      var self = this
-      var childId = this.childId
-
-      verifyMathAnswer(childId, expr, ans).then(function(res) {
-        self.waitingVerify = false
-        if (res.code === 200 && res.data) {
-          self.totalAttempts++
-          var correct = res.data.correct === true
-          self.lastCorrect = correct
-          self.feedbackText = res.data.feedback || (correct ? '答案正确!' : '再想想哦')
-
-          self.askedSet[exprKey] = true
-          self.askedExpressions.push({
-            expr: expr + ' = ' + ans,
-            correct: correct
-          })
-
-          if (correct) {
-            self.correctCount++
-          } else {
-            self.correctCount = 0
-          }
-        }
-      }).catch(function() {
-        self.waitingVerify = false
-        self.feedbackText = '验证失败,请重试'
-      })
+      return a
     },
-    continueQuiz: function() {
-      if (this.correctCount >= this.targetN) {
-        this.finishChallenge()
-        return
+    convertToTF: function(q) {
+      var opts = q.options
+      var idx = opts.indexOf(q.answer)
+      var wrongOpts = []
+      for (var w = 0; w < opts.length; w++) {
+        if (opts[w] !== q.answer) {
+          wrongOpts.push(opts[w])
+        }
+      }
+      var isTrue = Math.random() > 0.4
+      var claimIdx
+      if (isTrue) {
+        claimIdx = idx
+      } else {
+        var randomWrong = wrongOpts[Math.floor(Math.random() * wrongOpts.length)]
+        claimIdx = opts.indexOf(randomWrong)
+      }
+      return {
+        dimension: q.dimension,
+        question: q.question + ' 正确答案是"' + opts[claimIdx] + '"',
+        options: ['正确', '错误'],
+        answer: isTrue ? '正确' : '错误',
+        isTF: true
       }
-      this.currentExpression = ''
-      this.currentAnswer = ''
-      this.feedbackText = ''
-      this.lastCorrect = false
     },
-    finishChallenge: function() {
-      this.stopTimer()
-      var totalTimeMs = Date.now() - this.startTime
-      var self = this
-      var exprs = this.askedExpressions.map(function(e) { return e.expr })
-
-      completeMathQuiz({
-        childId: this.childId,
-        level: this.selectedLevel,
-        targetN: this.targetN,
-        correctCount: this.correctCount,
-        totalAttempts: this.totalAttempts,
-        totalTimeMs: totalTimeMs,
-        expressions: exprs
-      }).then(function(res) {
-        if (res.code === 200 && res.data) {
-          self.passResult = res.data
-          self.currentStep = 3
+    generateQuestions: function() {
+      var goalId = this.selectedGoal && this.selectedGoal.id
+      if (!goalId) return
+      var pool = []
+      for (var p = 0; p < QUESTION_BANK.length; p++) {
+        if (QUESTION_BANK[p].dimension === goalId) {
+          pool.push(QUESTION_BANK[p])
         }
-      }).catch(function() {
-        self.passResult = {
-          passed: self.correctCount >= self.targetN,
-          accuracy: self.totalAttempts > 0 ? Math.round(self.correctCount / self.totalAttempts * 100) : 0,
-          totalAttempts: self.totalAttempts,
-          totalTimeMs: totalTimeMs,
-          levelUp: false,
-          levelDown: false,
-          newLevel: self.selectedLevel,
-          reason: '结果保存失败'
+      }
+      var shuffled = this.shuffle(pool)
+      var type = this.questionType
+      if (type === 'truefalse') {
+        var tfQuestions = []
+        var count = Math.min(5, shuffled.length)
+        for (var ti = 0; ti < count; ti++) {
+          tfQuestions.push(this.convertToTF(shuffled[ti]))
         }
-        self.currentStep = 3
-      })
-    },
-    playAgain: function() {
-      this.loadSession()
-      this.startChallenge()
-    },
-    confirmQuit: function() {
-      if (this.currentStep === 2) {
-        var self = this
-        uni.showModal({
-          title: '提示',
-          content: '确定要放弃当前挑战吗?',
-          success: function(r) {
-            if (r.confirm) {
-              self.stopTimer()
-              self.currentStep = 1
-              self.loadSession()
+        this.questions = tfQuestions
+      } else if (type === 'mix') {
+        var allDims = ['focus', 'memory', 'logic', 'perception', 'spatial', 'speed']
+        var otherDims = []
+        for (var od = 0; od < allDims.length; od++) {
+          if (allDims[od] !== goalId) {
+            otherDims.push(allDims[od])
+          }
+        }
+        otherDims = this.shuffle(otherDims)
+        var selectedDims = [goalId].concat(otherDims.slice(0, 4))
+        var mixedQuestions = []
+        for (var di = 0; di < selectedDims.length; di++) {
+          var dimPool = []
+          for (var dq = 0; dq < QUESTION_BANK.length; dq++) {
+            if (QUESTION_BANK[dq].dimension === selectedDims[di]) {
+              dimPool.push(QUESTION_BANK[dq])
             }
           }
-        })
+          if (dimPool.length > 0) {
+            var pick = this.shuffle(dimPool)
+            mixedQuestions.push(pick[0])
+          }
+        }
+        this.questions = this.shuffle(mixedQuestions).slice(0, 5)
       } else {
-        this.goBack()
+        this.questions = shuffled.slice(0, 5)
       }
     },
-    goBack: function() {
-      uni.navigateBack()
+    nextQuestion: function() {
+      if (!this.currentAnswer) return
+      if (this.currentQ < this.questions.length - 1) {
+        this.currentQ++
+        this.saveSession()
+      } else {
+        this.step = 4
+        this.saveSession()
+      }
+    },
+    prevQuestion: function() {
+      if (this.currentQ > 0) {
+        this.currentQ--
+      }
+    },
+    resetQuiz: function() {
+      uni.removeStorageSync('selfQuizSession')
+      this.step = 1
+      this.selectedGoal = null
+      this.questionType = null
+      this.questions = []
+      this.currentQ = 0
+      this.answers = []
+    },
+    goHome: function() {
+      uni.switchTab({ url: '/pages/wisdom/index' })
     },
-    formatDuration: function(ms) {
-      if (!ms) return '0秒'
-      var s = Math.floor(ms / 1000)
-      if (s < 60) return s + '秒'
-      var m = Math.floor(s / 60)
-      var r = s % 60
-      return m + '分' + r + '秒'
+    goStep: function(n) {
+      if (n === this.step) return
+      if (n < this.step) {
+        this.step = n
+        this.saveSession()
+      }
     }
   }
 }
@@ -417,288 +466,456 @@ export default {
 <style scoped>
 .container {
   min-height: 100vh;
-  background: linear-gradient(180deg, #FFF8E1 0%, #f5f7fa 100%);
-  padding-bottom: 100rpx;
+  background: #f5f7fa;
+  padding: 30rpx;
+  padding-bottom: 120rpx;
+  box-sizing: border-box;
+}
+
+.resume-prompt {
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 50rpx 30rpx;
+  text-align: center;
+  margin-top: 160rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
 }
-.nav-bar {
+.resume-text {
+  font-size: 28rpx;
+  color: #333;
+  display: block;
+  margin-bottom: 30rpx;
+}
+.resume-btns {
   display: flex;
-  flex-direction: row;
+  justify-content: center;
+  gap: 20rpx;
+}
+
+.step-wrap {
+  margin-bottom: 10rpx;
+}
+.step-row {
+  display: flex;
+  justify-content: center;
+  align-items: flex-start;
+  padding: 20rpx 0;
+}
+.step-item {
+  display: flex;
+  flex-direction: column;
   align-items: center;
-  padding: 80rpx 30rpx 20rpx;
-  background: linear-gradient(135deg, #FFD700, #FFA500);
+  margin: 0 24rpx;
 }
-.nav-back {
-  width: 60rpx; height: 60rpx;
+.step-dot {
+  width: 48rpx;
+  height: 48rpx;
+  border-radius: 50%;
   display: flex;
   align-items: center;
   justify-content: center;
+  border: 3rpx solid #ddd;
+  background: #fff;
+  transition: all 0.3s;
 }
-.nav-back-icon { font-size: 36rpx; color: #fff; }
-.nav-title {
-  flex: 1; text-align: center;
-  font-size: 34rpx; font-weight: bold; color: #fff;
-  margin-right: 60rpx;
+.step-dot--active {
+  background: #FFD700;
+  border-color: #FFD700;
+}
+.step-dot--done {
+  border-color: #FFD700;
+  background: #FFD700;
+}
+.step-dot--pending {
+  border-color: #ddd;
+  background: #fff;
+}
+.step-check {
+  font-size: 22rpx;
+  color: #fff;
+  font-weight: bold;
+}
+.step-num {
+  font-size: 22rpx;
+  color: #999;
+}
+.step-dot--active .step-num {
+  color: #fff;
+}
+.step-label {
+  font-size: 20rpx;
+  margin-top: 8rpx;
+  color: #999;
+}
+.step-label--active {
+  color: #FFD700;
+  font-weight: bold;
+}
+.step-label--muted {
+  color: #ccc;
 }
-.nav-placeholder { width: 60rpx; }
 
-/* ===== Step 1 ===== */
-.step-content { padding: 30rpx; }
-.level-card {
-  background: linear-gradient(135deg, #FFD700, #FFA500);
-  border-radius: 20rpx;
-  padding: 40rpx;
-  text-align: center;
-  margin-bottom: 30rpx;
+.step-content {
+  animation: fadeIn 0.3s ease;
 }
-.level-title { font-size: 26rpx; color: rgba(255,255,255,0.85); display: block; }
-.level-name {
-  font-size: 48rpx; font-weight: bold; color: #fff;
-  display: block; margin: 10rpx 0;
+@keyframes fadeIn {
+  from { opacity: 0; transform: translateY(20rpx); }
+  to { opacity: 1; transform: translateY(0); }
 }
-.level-desc { font-size: 24rpx; color: rgba(255,255,255,0.8); display: block; }
-.age-hint { text-align: center; margin-bottom: 20rpx; }
-.hint-text { font-size: 24rpx; color: #999; }
 
-.content-card {
-  background: #fff;
-  border-radius: 24rpx;
-  padding: 40rpx;
-  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+.content-header {
+  text-align: center;
+  margin-bottom: 30rpx;
 }
 .content-title {
-  font-size: 34rpx; font-weight: bold; color: #333;
-  display: block; text-align: center; margin-bottom: 10rpx;
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+.content-sub {
+  font-size: 24rpx;
+  color: #999;
+  margin-top: 10rpx;
+  display: block;
 }
-.content-desc {
-  font-size: 26rpx; color: #999;
-  display: block; text-align: center; margin-bottom: 40rpx;
+.section-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 20rpx;
 }
 
-.target-row {
+.goal-grid {
   display: flex;
-  flex-direction: row;
+  flex-wrap: wrap;
+}
+.goal-card {
+  width: calc(50% - 12rpx);
+  margin: 6rpx;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 30rpx 16rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+  display: flex;
+  flex-direction: column;
   align-items: center;
-  justify-content: center;
-  margin-bottom: 10rpx;
+  border: 3rpx solid transparent;
+  transition: all 0.3s;
+  box-sizing: border-box;
 }
-.target-btn {
-  width: 60rpx; height: 60rpx;
-  border-radius: 50%;
+.goal-card--selected {
+  border-color: #FFD700;
   background: #FFF8E1;
-  border: 2rpx solid #FFD700;
+}
+.goal-icon-wrap {
+  width: 80rpx;
+  height: 80rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
   display: flex;
   align-items: center;
   justify-content: center;
-  font-size: 36rpx; color: #B8860B; font-weight: bold;
+  margin-bottom: 16rpx;
+}
+.goal-icon {
+  font-size: 32rpx;
+  color: #fff;
+  font-weight: bold;
+}
+.goal-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  margin-bottom: 6rpx;
+}
+.goal-desc {
+  font-size: 22rpx;
+  color: #999;
+  text-align: center;
+  line-height: 1.4;
 }
-.target-btn:active { opacity: 0.7; }
-.target-display { margin: 0 40rpx; text-align: center; }
-.target-num { font-size: 72rpx; font-weight: bold; color: #B8860B; display: block; }
-.target-unit { font-size: 28rpx; color: #999; display: block; text-align: center; }
-.range-hint { font-size: 24rpx; color: #ccc; display: block; text-align: center; margin-bottom: 40rpx; }
 
-.level-selector {
-  margin-bottom: 40rpx;
-  border-top: 2rpx solid #f5f5f5;
-  padding-top: 30rpx;
+.type-list {
+  margin-top: 10rpx;
 }
-.level-select-label {
-  font-size: 26rpx; color: #666;
-  display: block; margin-bottom: 16rpx;
+.type-card {
+  display: flex;
+  align-items: center;
+  background: #fff;
+  border-radius: 20rpx;
+  padding: 28rpx 24rpx;
+  margin-bottom: 20rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+  border: 3rpx solid transparent;
+  transition: all 0.3s;
 }
-.level-options {
+.type-card--selected {
+  border-color: #FFD700;
+  background: #FFF8E1;
+}
+.type-icon-wrap {
+  width: 72rpx;
+  height: 72rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
   display: flex;
-  flex-direction: row;
-  justify-content: space-between;
+  align-items: center;
+  justify-content: center;
+  margin-right: 20rpx;
+  flex-shrink: 0;
+}
+.type-icon {
+  font-size: 28rpx;
+  color: #fff;
+  font-weight: bold;
 }
-.level-option {
+.type-info {
   flex: 1;
-  text-align: center;
-  padding: 16rpx 8rpx;
-  margin: 0 6rpx;
-  border-radius: 12rpx;
-  background: #f5f5f5;
+}
+.type-name {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #333;
+  display: block;
+}
+.type-desc {
   font-size: 22rpx;
   color: #999;
-  line-height: 1.4;
+  margin-top: 4rpx;
+  display: block;
 }
-.level-option.active {
-  background: #FFF8E1;
-  color: #B8860B;
-  font-weight: bold;
-  border: 2rpx solid #FFD700;
+.type-radio {
+  width: 36rpx;
+  height: 36rpx;
+  border-radius: 50%;
+  border: 3rpx solid #ddd;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  margin-left: 16rpx;
 }
-
-.start-btn {
-  background: linear-gradient(135deg, #FFD700, #FFA500);
-  padding: 24rpx 0;
-  border-radius: 44rpx;
-  text-align: center;
-  box-shadow: 0 4rpx 20rpx rgba(255,215,0,0.4);
+.type-radio--checked {
+  border-color: #FFD700;
+}
+.type-radio-dot {
+  width: 20rpx;
+  height: 20rpx;
+  border-radius: 50%;
+  background: #FFD700;
 }
-.start-btn:active { opacity: 0.8; }
-.start-btn-text { font-size: 30rpx; color: #fff; font-weight: bold; }
 
-/* ===== Step 2 ===== */
+.progress-header {
+  margin-bottom: 30rpx;
+}
+.progress-label {
+  font-size: 26rpx;
+  color: #666;
+  display: block;
+  margin-bottom: 12rpx;
+}
 .progress-bar {
-  height: 12rpx;
-  background: #e0e0e0;
-  border-radius: 6rpx;
+  height: 8rpx;
+  background: #eee;
+  border-radius: 4rpx;
   overflow: hidden;
-  margin-bottom: 10rpx;
 }
 .progress-fill {
   height: 100%;
-  background: linear-gradient(90deg, #FFD700, #FFA500);
-  border-radius: 6rpx;
-  transition: width 0.3s;
-}
-.progress-info {
-  display: flex;
-  flex-direction: row;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 20rpx;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  border-radius: 4rpx;
+  transition: width 0.3s ease;
 }
-.progress-text { font-size: 26rpx; color: #666; }
-.timer-text { font-size: 26rpx; color: #B8860B; font-weight: bold; }
 
-.expr-list {
+.question-card {
   background: #fff;
-  border-radius: 16rpx;
-  padding: 16rpx 20rpx;
+  border-radius: 20rpx;
+  padding: 36rpx 28rpx;
+  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+  margin-bottom: 30rpx;
+}
+.q-tag {
+  display: inline-block;
+  padding: 6rpx 20rpx;
+  background: #FFF8E1;
+  color: #B8860B;
+  font-size: 20rpx;
+  border-radius: 20rpx;
   margin-bottom: 20rpx;
-  box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
-  max-height: 300rpx;
-  overflow-y: auto;
 }
-.expr-item {
+.q-text {
+  font-size: 30rpx;
+  color: #333;
+  line-height: 1.6;
+  display: block;
+  margin-bottom: 30rpx;
+}
+.q-options {
+  display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.q-option {
   display: flex;
-  flex-direction: row;
   align-items: center;
-  padding: 8rpx 0;
-  border-bottom: 1rpx solid #f5f5f5;
-  font-size: 24rpx;
+  padding: 24rpx 20rpx;
+  border-radius: 16rpx;
+  border: 2rpx solid #eee;
+  transition: all 0.3s;
 }
-.expr-item:last-child { border-bottom: none; }
-.expr-index { color: #999; width: 40rpx; }
-.expr-text { flex: 1; color: #333; }
-.expr-result { width: 36rpx; text-align: center; font-weight: bold; }
-.expr-result.correct { color: #52c41a; }
-.expr-result.wrong { color: #ff4d4f; }
-
-.quiz-card {
-  background: #fff;
-  border-radius: 24rpx;
-  padding: 30rpx;
-  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
+.q-option--selected {
+  border-color: #FFD700;
+  background: #FFF8E1;
 }
-.input-group { margin-bottom: 24rpx; }
-.input-label {
-  font-size: 28rpx; color: #333; font-weight: 500;
-  display: block; margin-bottom: 10rpx;
+.q-option--tf {
+  justify-content: center;
+  padding: 28rpx;
 }
-.expr-input, .answer-input {
-  width: 100%;
-  height: 72rpx;
-  background: #f5f7fa;
-  border-radius: 12rpx;
-  padding: 0 20rpx;
+.q-option-tf-text {
   font-size: 28rpx;
-  color: #333;
-  box-sizing: border-box;
+  font-weight: bold;
+  color: #666;
 }
-
-.submit-row { margin-top: 20rpx; }
-.submit-btn {
-  background: linear-gradient(135deg, #FFD700, #FFA500);
-  padding: 20rpx 0;
-  border-radius: 44rpx;
-  text-align: center;
-  box-shadow: 0 4rpx 20rpx rgba(255,215,0,0.4);
+.q-option--selected .q-option-tf-text {
+  color: #B8860B;
+}
+.q-radio {
+  width: 32rpx;
+  height: 32rpx;
+  border-radius: 50%;
+  border: 3rpx solid #ddd;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+  margin-right: 16rpx;
+}
+.q-radio--checked {
+  border-color: #FFD700;
+}
+.q-radio-dot {
+  width: 18rpx;
+  height: 18rpx;
+  border-radius: 50%;
+  background: #FFD700;
+}
+.q-option-text {
+  font-size: 26rpx;
+  color: #333;
+  flex: 1;
 }
-.submit-btn.disabled { opacity: 0.5; }
-.submit-btn:active { opacity: 0.8; }
-.submit-btn-text { font-size: 28rpx; color: #fff; font-weight: bold; }
 
-.feedback { margin-top: 24rpx; }
-.feedback-card {
+.action-bar {
+  margin-top: 40rpx;
   display: flex;
+  flex-direction: column;
+  gap: 16rpx;
+}
+.action-bar--double {
   flex-direction: row;
-  align-items: center;
-  padding: 20rpx;
+  gap: 20rpx;
+}
+.action-bar--double .btn {
+  flex: 1;
+}
+
+.btn {
+  padding: 24rpx 0;
   border-radius: 16rpx;
-  margin-bottom: 16rpx;
+  text-align: center;
+  font-size: 28rpx;
+  font-weight: bold;
+  transition: all 0.3s;
+  box-sizing: border-box;
 }
-.fc-correct { background: #f0fff4; border: 2rpx solid #52c41a; }
-.fc-wrong { background: #fff5f5; border: 2rpx solid #ff4d4f; }
-.fb-icon { font-size: 40rpx; margin-right: 16rpx; }
-.fb-content { flex: 1; }
-.fb-title { font-size: 28rpx; font-weight: bold; color: #333; display: block; }
-.fb-desc { font-size: 24rpx; color: #999; margin-top: 4rpx; display: block; }
-.continue-btn {
+.btn--primary {
   background: linear-gradient(135deg, #FFD700, #FFA500);
-  padding: 16rpx 0;
-  border-radius: 40rpx;
-  text-align: center;
+  color: #fff;
 }
-.continue-btn:active { opacity: 0.8; }
-.continue-btn-text { font-size: 28rpx; color: #fff; font-weight: bold; }
-
-.quit-row { margin-top: 30rpx; text-align: center; }
-.quit-btn { display: inline-block; padding: 16rpx 60rpx; }
-.quit-btn-text { font-size: 26rpx; color: #ccc; }
-
-/* ===== Step 3 ===== */
-.result-card {
+.btn--primary:active {
+  opacity: 0.9;
+  transform: scale(0.98);
+}
+.btn--disabled {
+  opacity: 0.4;
+  pointer-events: none;
+}
+.btn--outline {
+  border: 3rpx solid #FFD700;
+  color: #B8860B;
   background: #fff;
-  border-radius: 24rpx;
-  padding: 60rpx 40rpx;
-  text-align: center;
-  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
 }
-.result-icon { font-size: 120rpx; display: block; margin-bottom: 20rpx; }
-.result-title {
-  font-size: 40rpx; font-weight: bold; color: #B8860B;
-  display: block; margin-bottom: 40rpx;
+.btn--outline:active {
+  background: #FFF8E1;
+}
+.btn--ghost {
+  color: #999;
+  background: transparent;
 }
-.stats-row {
+
+.result-header {
   display: flex;
-  flex-direction: row;
+  flex-direction: column;
+  align-items: center;
+  margin: 40rpx 0;
+}
+.score-circle {
+  width: 160rpx;
+  height: 160rpx;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #FFD700, #FFA500);
+  display: flex;
+  align-items: center;
   justify-content: center;
-  margin-bottom: 40rpx;
+  box-shadow: 0 8rpx 30rpx rgba(255,215,0,0.3);
+  margin-bottom: 20rpx;
+}
+.score-value {
+  font-size: 40rpx;
+  font-weight: bold;
+  color: #fff;
+}
+.score-label {
+  font-size: 28rpx;
+  color: #666;
+  display: block;
 }
-.stat-item { text-align: center; margin: 0 24rpx; }
-.stat-value { font-size: 44rpx; font-weight: bold; color: #B8860B; display: block; }
-.stat-label { font-size: 22rpx; color: #999; margin-top: 6rpx; display: block; }
-
-.level-change {
-  padding: 20rpx;
-  border-radius: 12rpx;
-  margin-bottom: 40rpx;
-  background: #f0fff4;
-}
-.level-change.down { background: #fff5f5; }
-.level-change.stay { background: #f5f5f5; }
-.change-icon { font-size: 32rpx; font-weight: bold; display: block; margin-bottom: 6rpx; }
-.change-text { font-size: 26rpx; color: #333; line-height: 1.5; }
 
-.result-actions { display: flex; flex-direction: column; align-items: center; }
-.action-btn {
-  width: 60%;
-  padding: 20rpx 0;
-  border-radius: 44rpx;
-  margin-bottom: 20rpx;
-  text-align: center;
+.result-dims {
+  margin: 20rpx 0 30rpx;
+}
+.dim-bar-item {
+  margin-bottom: 24rpx;
+}
+.dim-bar-header {
+  display: flex;
+  justify-content: space-between;
+  margin-bottom: 8rpx;
+}
+.dim-bar-name {
+  font-size: 24rpx;
+  color: #333;
+  font-weight: bold;
+}
+.dim-bar-count {
+  font-size: 22rpx;
+  color: #999;
+}
+.dim-bar-track {
+  height: 8rpx;
+  background: #eee;
+  border-radius: 4rpx;
+  overflow: hidden;
 }
-.action-btn.primary {
+.dim-bar-fill {
+  height: 100%;
   background: linear-gradient(135deg, #FFD700, #FFA500);
-  box-shadow: 0 4rpx 20rpx rgba(255,215,0,0.4);
+  border-radius: 4rpx;
+  transition: width 0.6s ease;
 }
-.action-btn.secondary { background: #fff; border: 2rpx solid #ddd; }
-.action-btn-text { font-size: 28rpx; font-weight: bold; }
-.action-btn.primary .action-btn-text { color: #fff; }
-.action-btn.secondary .action-btn-text { color: #666; }
 
-.bottom-spacer { height: 40rpx; }
+.bottom-spacer {
+  height: 120rpx;
+}
 </style>

+ 11 - 11
tests/e2e/mini-games-playwright.spec.js

@@ -8,9 +8,9 @@ const { test, expect } = require('@playwright/test');
 test.describe('Mini-Games E2E - Playwright', () => {
   
   test.beforeEach(async ({ page }) => {
-    // Navigate to the mini-games list page
-    // Note: Adjust URL based on your actual deployment
-    await page.goto('/pages/games/list');
+    // Navigate to the mini-games list page (uni-app H5 hash router)
+    // Requires `npm run dev:h5` running in cfc-frontend
+    await page.goto('/#/pages/games/list');
     
     // Wait for page to load
     await page.waitForLoadState('networkidle');
@@ -31,7 +31,7 @@ test.describe('Mini-Games E2E - Playwright', () => {
   });
 
   test('1a2b - should calculate and display score on win', async ({ page }) => {
-    await page.goto('/pages/games/1a2b');
+    await page.goto('/#/pages/games/1a2b');
     
     // Mock the API response
     await page.route('**/api/mini-game/complete', async route => {
@@ -60,7 +60,7 @@ test.describe('Mini-Games E2E - Playwright', () => {
   });
 
   test('1a2b - should handle missing childId gracefully', async ({ page }) => {
-    await page.goto('/pages/games/1a2b');
+    await page.goto('/#/pages/games/1a2b');
     
     // Clear any stored user data
     await page.evaluate(() => {
@@ -78,7 +78,7 @@ test.describe('Mini-Games E2E - Playwright', () => {
   });
 
   test('Sudoku - should display difficulty selection', async ({ page }) => {
-    await page.goto('/pages/games/sudoku');
+    await page.goto('/#/pages/games/sudoku');
     
     // Verify difficulty buttons
     await expect(page.locator('.level-btn:has-text("简单")')).toBeVisible();
@@ -87,7 +87,7 @@ test.describe('Mini-Games E2E - Playwright', () => {
   });
 
   test('Sudoku - should start game and display board', async ({ page }) => {
-    await page.goto('/pages/games/sudoku');
+    await page.goto('/#/pages/games/sudoku');
     
     // Select difficulty
     await page.click('.level-btn:has-text("简单")');
@@ -103,7 +103,7 @@ test.describe('Mini-Games E2E - Playwright', () => {
   });
 
   test('Sudoku - should calculate score based on difficulty and time', async ({ page }) => {
-    await page.goto('/pages/games/sudoku');
+    await page.goto('/#/pages/games/sudoku');
     
     // Mock API
     await page.route('**/api/mini-game/complete', async route => {
@@ -130,7 +130,7 @@ test.describe('Mini-Games E2E - Playwright', () => {
   });
 
   test('API - should call completeMiniGame with correct params', async ({ page }) => {
-    await page.goto('/pages/games/schulte');
+    await page.goto('/#/pages/games/schulte');
     
     // Intercept API call
     let apiCallParams = null;
@@ -162,7 +162,7 @@ test.describe('Mini-Games E2E - Playwright', () => {
   });
 
   test('UI - should display result modal with score', async ({ page }) => {
-    await page.goto('/pages/games/schulte');
+    await page.goto('/#/pages/games/schulte');
     
     // Mock a completed game state
     await page.evaluate(() => {
@@ -181,7 +181,7 @@ test.describe('Mini-Games E2E - Playwright', () => {
   });
 
   test('Games List - should navigate to all games', async ({ page }) => {
-    await page.goto('/pages/games/list');
+    await page.goto('/#/pages/games/list');
     
     // Verify all game cards are visible
     await expect(page.locator('text=舒尔特方格')).toBeVisible();

+ 19 - 11
tests/integration/mini-games-api.spec.js

@@ -5,7 +5,7 @@
 
 describe('Mini-Games API Integration Tests', () => {
   
-  const BASE_URL = process.env.API_BASE_URL || 'http://localhost:8080';
+  const BASE_URL = process.env.API_BASE_URL || 'http://localhost:9082';
   
   describe('POST /api/mini-game/complete', () => {
     
@@ -120,14 +120,16 @@ describe('Mini-Games API Integration Tests', () => {
     });
   });
 
-  describe('GET /api/mini-game/list', () => {
+  describe('POST /api/mini-game/list', () => {
     
     test('should return list of available games', async () => {
       const response = await fetch(`${BASE_URL}/api/mini-game/list`, {
-        method: 'GET',
+        method: 'POST',
         headers: {
+          'Content-Type': 'application/json',
           'Authorization': 'Bearer test-token'
-        }
+        },
+        body: JSON.stringify({})
       });
       
       const data = await response.json();
@@ -140,10 +142,12 @@ describe('Mini-Games API Integration Tests', () => {
 
     test('should include game details', async () => {
       const response = await fetch(`${BASE_URL}/api/mini-game/list`, {
-        method: 'GET',
+        method: 'POST',
         headers: {
+          'Content-Type': 'application/json',
           'Authorization': 'Bearer test-token'
-        }
+        },
+        body: JSON.stringify({})
       });
       
       const data = await response.json();
@@ -157,14 +161,16 @@ describe('Mini-Games API Integration Tests', () => {
     });
   });
 
-  describe('GET /api/mini-game/:gameCode', () => {
+  describe('POST /api/mini-game/:gameCode', () => {
     
     test('should return game details by code', async () => {
       const response = await fetch(`${BASE_URL}/api/mini-game/schulte`, {
-        method: 'GET',
+        method: 'POST',
         headers: {
+          'Content-Type': 'application/json',
           'Authorization': 'Bearer test-token'
-        }
+        },
+        body: JSON.stringify({})
       });
       
       const data = await response.json();
@@ -176,10 +182,12 @@ describe('Mini-Games API Integration Tests', () => {
 
     test('should return 404 for invalid game code', async () => {
       const response = await fetch(`${BASE_URL}/api/mini-game/nonexistent`, {
-        method: 'GET',
+        method: 'POST',
         headers: {
+          'Content-Type': 'application/json',
           'Authorization': 'Bearer test-token'
-        }
+        },
+        body: JSON.stringify({})
       });
       
       expect(response.status).toBe(404);

+ 3 - 3
tests/unit/games/1a2b.spec.js

@@ -3,10 +3,10 @@
 // The actual test-runner and Vue test-utils setup may vary in your repo.
 import Vue from 'vue'
 import { mount, createLocalVue } from '@vue/test-utils'
-import Game1a2b from '../../../zxyj-frontend/pages/games/1a2b.vue'
+import Game1a2b from '../../../cfc-frontend/pages/games/1a2b.vue'
 
 // Mock API module
-jest.mock('../../../zxyj-frontend/utils/api.js', () => ({
+jest.mock('../../../cfc-frontend/utils/api.js', () => ({
   completeMiniGame: jest.fn(() => Promise.resolve({ code: 200, data: { pointsEarned: 10, newBalance: 110 } })),
   getChildren: jest.fn(() => Promise.resolve({ data: [{ id: 'child1' }] }))
 }))
@@ -35,7 +35,7 @@ describe('1a2b.vue - first-guess immediate win', () => {
     expect(wrapper.vm.isWin).toBe(true)
     expect(wrapper.vm.score).toBe(100)
     // Ensure submitScore was invoked (mocked API should be called)
-    const api = require('../../../zxyj-frontend/utils/api.js')
+    const api = require('../../../cfc-frontend/utils/api.js')
     expect(api.completeMiniGame).toHaveBeenCalled()
   })
 })