浏览代码

feat: 家庭关系图视觉重编码 (信任/沟通/亲密度) + 身份形状 + 自导航维护页

- 连线粗细=信任,虚线密度=沟通,颜色=亲密度
- 节点形状按身份区分 (圆角矩形/圆形/菱形)
- 点自己跳转维护页 (原为添加成员)
- 触摸自身显示⚙提示
- child-index: 新增'自己出题'快速入口
Xiaogang Liao 1 月之前
父节点
当前提交
6b334d34fe

+ 159 - 27
cfc-frontend/components/FamilyRelationGraph.vue

@@ -14,9 +14,9 @@
       @touchstart="onTouchStart"
       @touchmove="onTouchMove"
       @touchend="onTouchEnd" />
-    <!-- + 添加成员提示(触摸自身节点时显示) -->
+    <!-- ⚙ 维护页提示(触摸自身节点时显示) -->
     <view class="add-hint" :class="{ 'add-hint-visible': touchingSelf }" :style="addHintPosition">
-      <text class="add-hint-text">+</text>
+      <text class="add-hint-text"></text>
     </view>
   </view>
   <!-- 空状态 -->
@@ -94,7 +94,7 @@ export default {
       dragging: null,         // { nodeIndex, offsetX, offsetY }
       dragStartPos: null,    // 用于判断是否真的拖拽了
 
-      // 触摸/悬停状态(显示 + 号
+      // 触摸/悬停状态(显示 ⚙ 提示
       touchingSelf: false,
       hoveringSelf: false,
 
@@ -120,7 +120,7 @@ export default {
       return this.themeColors[this.dimensionCode] || '#8D6E63'
     },
 
-    // + 提示位置(跟随中心节点偏移)
+    // ⚙ 提示位置(跟随自身节点偏移)
     addHintPosition: function() {
       var selfNode = this.nodeData.find(function(n) { return n.isSelf })
       if (!selfNode) return {}
@@ -164,13 +164,16 @@ export default {
           displayName: displayName,
           isSelf: isSelf,
           memberType: m.memberType || 'child',
+          effectiveRole: m.effectiveRole || m.relativeLabel || '',
+          gender: m.gender || '',
+          relativeLabel: m.relativeLabel || '',
           radius: radius,
           x: initX,
           y: initY,
           vx: 0,
           vy: 0,
-          fx: 0, // 固定 x (拖拽时)
-          fy: 0  // 固定 y (拖拽时)
+          fx: 0,
+          fy: 0
         }
       })
     },
@@ -461,14 +464,29 @@ simulationTick: function() {
         var s = edge.source
         var t = edge.target
 
-        // 边颜色
+        // 连线粗细 = 信任度 (1-6px, 信任越高越粗)
         var trust = edge.trust || 75
+        ctx.lineWidth = 1 + (trust / 100) * 5
+
+        // 虚线密度 = 沟通频率 (0=稀疏虚线, 100=实线)
         var comm = edge.communication || 50
-        var hue = 120 * trust / 100
-        ctx.strokeStyle = 'hsla(' + hue + ', 75%, 45%, 0.6)'
-        ctx.lineWidth = 1 + (comm / 100) * 4
+        if (comm >= 90) {
+          ctx.setLineDash([])
+        } else if (comm >= 70) {
+          ctx.setLineDash([8, 3])
+        } else if (comm >= 50) {
+          ctx.setLineDash([5, 5])
+        } else if (comm >= 30) {
+          ctx.setLineDash([3, 8])
+        } else {
+          ctx.setLineDash([2, 12])
+        }
+
+        // 连线颜色 = 亲密度 (低=冷色蓝紫, 高=暖色橙红)
+        var closeness = edge.closeness || 50
+        var hue = 220 - (closeness / 100) * 200
+        ctx.strokeStyle = 'hsla(' + hue + ', 75%, 50%, 0.65)'
 
-        // 画线
         ctx.beginPath()
         ctx.moveTo(s.x, s.y)
         ctx.lineTo(t.x, t.y)
@@ -476,16 +494,85 @@ simulationTick: function() {
       }
     },
 
+    // ===== 身份 → 形状映射 =====
+
+    getNodeShapeType: function(node) {
+      var role = node.effectiveRole || node.relativeLabel || ''
+      var type = node.memberType || ''
+      // 长辈 (爷爷/奶奶/外公/外婆)
+      if (role.indexOf('爷爷') !== -1 || role.indexOf('奶奶') !== -1 ||
+          role.indexOf('外公') !== -1 || role.indexOf('外婆') !== -1 ||
+          role.indexOf('祖父') !== -1 || role.indexOf('祖母') !== -1) {
+        return 'diamond'
+      }
+      // 父亲/男性家长
+      if (role.indexOf('爸爸') !== -1 || role.indexOf('父亲') !== -1 ||
+          role.indexOf('爸') !== -1 || role.indexOf('爹') !== -1) {
+        return 'roundedRect'
+      }
+      // 母亲/女性家长
+      if (role.indexOf('妈妈') !== -1 || role.indexOf('母亲') !== -1 ||
+          role.indexOf('妈') !== -1 || role.indexOf('娘') !== -1) {
+        return 'circle'
+      }
+      // 通用 parent → 根据性别区分
+      if (type === 'parent') {
+        return node.gender === 'male' ? 'roundedRect' : 'circle'
+      }
+      // child → 圆形
+      if (type === 'child') {
+        return 'childCircle'
+      }
+      return 'circle'
+    },
+
+    // 绘制圆角矩形
+    drawRoundedRectNode: function(ctx, x, y, r) {
+      var w = r * 1.8
+      var h = r * 1.8
+      var cr = r * 0.3
+      ctx.beginPath()
+      ctx.moveTo(x - w/2 + cr, y - h/2)
+      ctx.lineTo(x + w/2 - cr, y - h/2)
+      ctx.arcTo(x + w/2, y - h/2, x + w/2, y - h/2 + cr, cr)
+      ctx.lineTo(x + w/2, y + h/2 - cr)
+      ctx.arcTo(x + w/2, y + h/2, x + w/2 - cr, y + h/2, cr)
+      ctx.lineTo(x - w/2 + cr, y + h/2)
+      ctx.arcTo(x - w/2, y + h/2, x - w/2, y + h/2 - cr, cr)
+      ctx.lineTo(x - w/2, y - h/2 + cr)
+      ctx.arcTo(x - w/2, y - h/2, x - w/2 + cr, y - h/2, cr)
+      ctx.closePath()
+    },
+
+    // 绘制菱形
+    drawDiamondNode: function(ctx, x, y, r) {
+      ctx.beginPath()
+      ctx.moveTo(x, y - r * 1.2)
+      ctx.lineTo(x + r * 1.2, y)
+      ctx.lineTo(x, y + r * 1.2)
+      ctx.lineTo(x - r * 1.2, y)
+      ctx.closePath()
+    },
+
     drawNodes: function(ctx) {
       var nodes = this.nodeData
       for (var i = 0; i < nodes.length; i++) {
         var node = nodes[i]
         var r = node.radius
+        var shapeType = this.getNodeShapeType(node)
 
-        // 节点背景
-        ctx.beginPath()
-        ctx.arc(node.x, node.y, r, 0, 2 * Math.PI)
+        // 按身份绘制不同形状
+        if (shapeType === 'roundedRect') {
+          this.drawRoundedRectNode(ctx, node.x, node.y, r)
+        } else if (shapeType === 'diamond') {
+          this.drawDiamondNode(ctx, node.x, node.y, r)
+        } else {
+          // 默认圆形 (母亲/女性家长/孩子)
+          ctx.beginPath()
+          ctx.arc(node.x, node.y, r, 0, 2 * Math.PI)
+        }
 
+        // 填充 + 描边
         if (node.isSelf) {
           ctx.fillStyle = '#FFFFFF'
           ctx.fill()
@@ -497,7 +584,7 @@ simulationTick: function() {
           ctx.fill()
         }
 
-        // 字母
+        // 第一字符 (昵称首字)
         ctx.fillStyle = node.isSelf ? this.themeColor : '#4A5568'
         ctx.font = 'bold ' + (r * 0.9) + 'px sans-serif'
         ctx.textAlign = 'center'
@@ -526,9 +613,28 @@ simulationTick: function() {
         var edge = edges[i]
         var trust = edge.trust || 75
         var comm = edge.communication || 50
-        var hue = 120 * trust / 100
-        ctx.setStrokeStyle('hsla(' + hue + ', 75%, 45%, 0.6)')
-        ctx.setLineWidth(1 + (comm / 100) * 4)
+        var closeness = edge.closeness || 50
+
+        // 连线粗细 = 信任度
+        ctx.setLineWidth(1 + (trust / 100) * 5)
+
+        // 虚线密度 = 沟通频率
+        if (comm >= 90) {
+          ctx.setLineDash([])
+        } else if (comm >= 70) {
+          ctx.setLineDash([8, 3])
+        } else if (comm >= 50) {
+          ctx.setLineDash([5, 5])
+        } else if (comm >= 30) {
+          ctx.setLineDash([3, 8])
+        } else {
+          ctx.setLineDash([2, 12])
+        }
+
+        // 连线颜色 = 亲密度 (低=冷色蓝紫, 高=暖色橙红)
+        var hue = 220 - (closeness / 100) * 200
+        ctx.setStrokeStyle('hsla(' + hue + ', 75%, 50%, 0.65)')
+
         ctx.beginPath()
         ctx.moveTo(edge.source.x, edge.source.y)
         ctx.lineTo(edge.target.x, edge.target.y)
@@ -539,9 +645,36 @@ simulationTick: function() {
       for (var j = 0; j < nodes.length; j++) {
         var node = nodes[j]
         var r = node.radius
+        var shapeType = this.getNodeShapeType(node)
+
+        // legacy API 的形状绘制
+        if (shapeType === 'roundedRect') {
+          var w = r * 1.8
+          var h = r * 1.8
+          var cr = r * 0.3
+          ctx.beginPath()
+          ctx.moveTo(node.x - w/2 + cr, node.y - h/2)
+          ctx.lineTo(node.x + w/2 - cr, node.y - h/2)
+          ctx.arcTo(node.x + w/2, node.y - h/2, node.x + w/2, node.y - h/2 + cr, cr)
+          ctx.lineTo(node.x + w/2, node.y + h/2 - cr)
+          ctx.arcTo(node.x + w/2, node.y + h/2, node.x + w/2 - cr, node.y + h/2, cr)
+          ctx.lineTo(node.x - w/2 + cr, node.y + h/2)
+          ctx.arcTo(node.x - w/2, node.y + h/2, node.x - w/2, node.y + h/2 - cr, cr)
+          ctx.lineTo(node.x - w/2, node.y - h/2 + cr)
+          ctx.arcTo(node.x - w/2, node.y - h/2, node.x - w/2 + cr, node.y - h/2, cr)
+          ctx.closePath()
+        } else if (shapeType === 'diamond') {
+          ctx.beginPath()
+          ctx.moveTo(node.x, node.y - r * 1.2)
+          ctx.lineTo(node.x + r * 1.2, node.y)
+          ctx.lineTo(node.x, node.y + r * 1.2)
+          ctx.lineTo(node.x - r * 1.2, node.y)
+          ctx.closePath()
+        } else {
+          ctx.beginPath()
+          ctx.arc(node.x, node.y, r, 0, 2 * Math.PI)
+        }
 
-        ctx.beginPath()
-        ctx.arc(node.x, node.y, r, 0, 2 * Math.PI)
         if (node.isSelf) {
           ctx.setFillStyle('#FFFFFF')
           ctx.fill()
@@ -648,12 +781,11 @@ simulationTick: function() {
     },
 
     onTouchEnd: function(e) {
-      // 如果正在触摸自身节点 → 跳转添加成员
+      // 如果正在触摸自身节点 → 跳转成员维护页
       if (this.touchingSelf) {
         this.touchingSelf = false
         if (this.interactive) {
-          this.$emit('addMemberTap')
-          this.goAddMember()
+          this.goSelfMaintenance()
         }
         this.dragging = null
         this.dragStartPos = null
@@ -685,10 +817,10 @@ simulationTick: function() {
       this.dragStartPos = null
     },
 
-    // 跳转到添加成员页面
-    goAddMember: function() {
+    // 跳转到成员维护页面 (点自己进入维护)
+    goSelfMaintenance: function() {
       uni.navigateTo({
-        url: '/pages/family/add-member'
+        url: '/pages/profile-extra/family-members'
       })
     },
 
@@ -770,7 +902,7 @@ simulationTick: function() {
   font-size: 32rpx;
   color: #94A3B8;
 }
-/* + 添加成员提示 */
+/* ⚙ 维护页提示 */
 .add-hint {
   position: absolute;
   z-index: 5;

+ 16 - 0
cfc-frontend/pages/home-pages/child-index.vue

@@ -202,6 +202,17 @@ B) 积分卡片 — Claymorphism 暖橙黏土风格
             </view>
             <text class="quick-btn-label">小游戏</text>
           </view>
+
+          <view
+            class="quick-clay-btn"
+            @click="goToSelfQuiz"
+            hover-class="quick-btn-pressed"
+          >
+            <view class="quick-btn-icon quick-icon--quiz">
+              <text class="quick-emoji">✏️</text>
+            </view>
+            <text class="quick-btn-label">自己出题</text>
+          </view>
         </view>
       </view>
 
@@ -630,6 +641,10 @@ export default {
       this.$emit('navigate', { page: 'games' })
       uni.navigateTo({ url: '/pages/games/games' })
     },
+    goToSelfQuiz() {
+      this.$emit('navigate', { page: 'self-quiz' })
+      uni.navigateTo({ url: '/pages/wisdom-detail/self-quiz' })
+    },
     goToAddChild() {
       this.$emit('navigate', { page: 'add-child' })
       uni.navigateTo({ url: '/pages/profile-extra/family-members' })
@@ -1172,6 +1187,7 @@ padding-top: 4rpx;
 .quick-icon--rewards { background: rgba(245,158,11,0.15); border-color: #FDE68A; }
 .quick-icon--growth  { background: rgba(91,155,213,0.15); border-color: #CBD5E1; }
 .quick-icon--games   { background: rgba(236,72,153,0.12); border-color: #FBCFE8; }
+.quick-icon--quiz    { background: rgba(99,102,241,0.15); border-color: #C7D2FE; }
 
 .quick-btn-label {
   font-size: 28rpx;

+ 0 - 4
cfc-frontend/pages/wisdom/index.vue

@@ -200,7 +200,6 @@ export default {
       danReports: [],
       funcList: [
         { icon: '\u{1F4CA}', label: '认知报告', needLogin: true, page: '/pages/wisdom-detail/cognitive-report' },
-        { icon: '\u{270F}\uFE0F', label: '自己出题', needLogin: true, page: '/pages/wisdom-detail/self-quiz' },
         { icon: '\u{1F9E0}', label: '训练中心', needLogin: true, page: '/pages/wisdom-detail/training-hub' },
         { icon: '\u{1F3AF}', label: '测评预约', needLogin: false, page: '/pages/assessment/apply' },
         { icon: '\u{1F4D6}', label: '阅读天地', needLogin: false, page: '/pages/mind-extra/articles' }
@@ -423,9 +422,6 @@ export default {
     goCognitiveReport: function() {
       uni.navigateTo({ url: '/pages/wisdom-detail/cognitive-report' })
     },
-    goSelfQuiz: function() {
-      uni.navigateTo({ url: '/pages/wisdom-detail/self-quiz' })
-    },
     goTrainingHub: function() {
       uni.navigateTo({ url: '/pages/wisdom-detail/training-hub' })
     },