Bläddra i källkod

feat(pearl): PearlDiagram 两层同心圆重构 + 父组件 overdueMap 集成

Sisyphus Agent 1 vecka sedan
förälder
incheckning
717fd0f1aa
2 ändrade filer med 260 tillägg och 125 borttagningar
  1. 244 81
      cfc-frontend/components/PearlDiagram.vue
  2. 16 44
      cfc-frontend/pages/action-detail/index.vue

+ 244 - 81
cfc-frontend/components/PearlDiagram.vue

@@ -36,6 +36,8 @@
         </view>
         <!-- 操作按钮 -->
         <view class="popup-actions">
+          <text class="action-btn action-btn-contact" v-if="selectedNode.groupType !== 'PERSON' && selectedNode.groupType !== 'SKILL'" @tap="onContact">联系</text>
+          <text class="action-btn action-btn-priority" v-if="selectedNode.groupType !== 'PERSON' && selectedNode.groupType !== 'SKILL'" @tap="onAdjustPriority">优先级</text>
           <text class="action-btn action-btn-delete" v-if="canDelete" @tap="onDelete">删除</text>
         </view>
         <view class="popup-arrow" :class="{ 'popup-arrow-down': showPopupBelow }"></view>
@@ -46,7 +48,7 @@
   <view class="pearl-graph-empty" v-else-if="isEmpty">
     <text class="empty-icon">📿</text>
     <text class="empty-text">暂无珍珠图数据</text>
-    <text class="empty-hint">登记你的资源,画出珍珠项链图</text>
+    <text class="empty-hint">登记你的社会连接,画出珍珠项链图</text>
     <text class="action-btn action-btn-primary" @tap="onAdd">+ 登记资源</text>
   </view>
   <!-- 加载状态 -->
@@ -56,18 +58,20 @@
 </template>
 
 <script>
+import { addPearlInteraction, updatePearlPriority } from '../utils/api.js'
+
 /**
  * PearlDiagram - 珍珠图 Canvas 组件
  *
  * 珍珠项链式资源清单可视化:
  * - 中心节点 = 当前用户"我"(固定不动)
- * - 珍珠节点 = 资源,按 4 组分布在中心周围
- *   人脉(PERSON) / 技能(SKILL) / 信息(INFO) / 场所(PLACE)
+ * - 珍珠节点 = 资源,按 25 组分布在中心周围(两层同心圆)
+ *   内圈必备 12 组 + 外圈理想 13 组
  * - 每组以弧形分布在中心周围,颜色编码
  * - 点击珍珠 → 显示详情弹窗
  *
  * 数据源:父组件通过 resources prop 传入
- * 布局方式:确定性弧形布局(非力导向)
+ * 布局方式:环形均分布局(非力导向)
  */
 export default {
   name: 'PearlDiagram',
@@ -96,6 +100,11 @@ export default {
     interactive: {
       type: Boolean,
       default: true
+    },
+    /** 逾期提醒映射 { resourceItemId: daysSinceLast },父组件从 /reminder/list 构建 */
+    overdueMap: {
+      type: Object,
+      default: null
     }
   },
   data: function() {
@@ -114,16 +123,37 @@ export default {
       _touchStartPos: null,
       // 默认头像
       defaultAvatar: '/static/default-avatar.png',
-      // 分组配置:类型、颜色、半径比例、弧度范围(弧度)
-      // 0=右(3点钟), PI/2=下(6点), PI=左(9点), -PI/2=上(12点)
+      // 分组配置:25 类社会连接,内圈必备 12 组 / 外圈理想 13 组
+      // 颜色盘:内圈偏暖(红橙),外圈偏冷(蓝紫绿)
       groupConfigs: [
-        { type: 'PERSON', typeName: '人脉', color: '#F97316', radiusPct: 0.28, startAngle: -Math.PI / 2, endAngle: 0 },
-        { type: 'SKILL', typeName: '技能', color: '#F59E0B', radiusPct: 0.38, startAngle: Math.PI, endAngle: Math.PI * 1.5 },
-        { type: 'INFO', typeName: '信息', color: '#10B981', radiusPct: 0.38, startAngle: 0, endAngle: Math.PI / 2 },
-        { type: 'PLACE', typeName: '场所', color: '#6366F1', radiusPct: 0.28, startAngle: Math.PI / 2, endAngle: Math.PI }
+        // ===== 内圈:必备 12 组(radiusPct 0.28)=====
+        { type: 'MEDICAL', typeName: '医疗', color: '#EF4444', radiusPct: 0.28, ring: 'inner' },
+        { type: 'EDUCATION', typeName: '教育', color: '#F97316', radiusPct: 0.28, ring: 'inner' },
+        { type: 'PUBLIC_SECURITY', typeName: '公安', color: '#DC2626', radiusPct: 0.28, ring: 'inner' },
+        { type: 'AUTO', typeName: '汽车相关', color: '#EA580C', radiusPct: 0.28, ring: 'inner' },
+        { type: 'REAL_ESTATE', typeName: '房产服务', color: '#D97706', radiusPct: 0.28, ring: 'inner' },
+        { type: 'TICKETING', typeName: '票务', color: '#CA8A04', radiusPct: 0.28, ring: 'inner' },
+        { type: 'HANDYMAN', typeName: '多能工人', color: '#B45309', radiusPct: 0.28, ring: 'inner' },
+        { type: 'APPLIANCE', typeName: '家电', color: '#92400E', radiusPct: 0.28, ring: 'inner' },
+        { type: 'CATERING', typeName: '餐饮', color: '#E11D48', radiusPct: 0.28, ring: 'inner' },
+        { type: 'FOOD', typeName: '食品', color: '#BE123C', radiusPct: 0.28, ring: 'inner' },
+        { type: 'LEGAL', typeName: '法律', color: '#B91C1C', radiusPct: 0.28, ring: 'inner' },
+        { type: 'WEEKEND_EXPERT', typeName: '周末达人', color: '#FB7185', radiusPct: 0.28, ring: 'inner' },
+        // ===== 外圈:理想 13 组(radiusPct 0.55)=====
+        { type: 'GOVERNMENT', typeName: '政府综合', color: '#8B5CF6', radiusPct: 0.55, ring: 'outer' },
+        { type: 'TAX', typeName: '税务', color: '#7C3AED', radiusPct: 0.55, ring: 'outer' },
+        { type: 'BUSINESS_ADMIN', typeName: '工商', color: '#6D28D9', radiusPct: 0.55, ring: 'outer' },
+        { type: 'BANK', typeName: '银行', color: '#2563EB', radiusPct: 0.55, ring: 'outer' },
+        { type: 'MEDIA', typeName: '媒体', color: '#1D4ED8', radiusPct: 0.55, ring: 'outer' },
+        { type: 'TRAVEL', typeName: '旅游', color: '#0EA5E9', radiusPct: 0.55, ring: 'outer' },
+        { type: 'LEADING_ENTERPRISE', typeName: '领军企业', color: '#0891B2', radiusPct: 0.55, ring: 'outer' },
+        { type: 'INDUSTRY_BENCHMARK', typeName: '行业标杆', color: '#059669', radiusPct: 0.55, ring: 'outer' },
+        { type: 'OVERSEAS', typeName: '国外', color: '#10B981', radiusPct: 0.55, ring: 'outer' },
+        { type: 'UNIVERSITY', typeName: '高校', color: '#16A34A', radiusPct: 0.55, ring: 'outer' },
+        { type: 'FINANCE', typeName: '金融', color: '#84CC16', radiusPct: 0.55, ring: 'outer' },
+        { type: 'SENIOR_LOCAL', typeName: '资深土著', color: '#65A30D', radiusPct: 0.55, ring: 'outer' },
+        { type: 'KEY_CITY', typeName: '北上广等', color: '#4D7C0F', radiusPct: 0.55, ring: 'outer' }
       ],
-      // 弧度边距(每侧留白角度)
-      arcMargin: Math.PI / 20
     }
   },
   computed: {
@@ -245,12 +275,13 @@ export default {
      * 根据 resources 构建珍珠节点的 x, y 坐标
      * 布局逻辑:
      * - 中心 = (canvasWidth/2, canvasHeight/2) → "我"
-     * - 每组分配一个象限 + 特定半径
+     * - 内圈 12 组均分整个圆周(radiusPct 0.28)
+     * - 外圈 13 组均分整个圆周(radiusPct 0.55)
      * - 组内珍珠沿弧线等间距分布
+     * - 珍珠半径按 priority 映射
      */
     buildLayout: function() {
       this.pearlNodes = []
-
       if (!this.resources || !this.resources.groups || !Array.isArray(this.resources.groups)) {
         return
       }
@@ -258,51 +289,82 @@ export default {
       var cx = this.canvasWidth / 2
       var cy = this.canvasHeight / 2
       var minDim = Math.min(this.canvasWidth, this.canvasHeight)
-      var configs = this.groupConfigs
-
-      for (var c = 0; c < configs.length; c++) {
-        var config = configs[c]
-        // 在 resources.groups 中查找该类型
-        var group = null
-        for (var g = 0; g < this.resources.groups.length; g++) {
-          if (this.resources.groups[g].type === config.type) {
-            group = this.resources.groups[g]
-            break
-          }
-        }
+      var self = this
 
-        if (!group || !group.items || group.items.length === 0) continue
+      // 按 ring 分组:内圈 12 / 外圈 13
+      var innerConfigs = []
+      var outerConfigs = []
+      for (var c = 0; c < this.groupConfigs.length; c++) {
+        var cfg = this.groupConfigs[c]
+        if (cfg.ring === 'inner') innerConfigs.push(cfg)
+        else outerConfigs.push(cfg)
+      }
 
-        var R = minDim * config.radiusPct
-        var effStart = config.startAngle + this.arcMargin
-        var effEnd = config.endAngle - this.arcMargin
-        var arcSpan = effEnd - effStart
-        var items = group.items
-
-        for (var i = 0; i < items.length; i++) {
-          var item = items[i]
-          var t = items.length === 1 ? 0.5 : i / (items.length - 1)
-          var angle = effStart + t * arcSpan
-
-          this.pearlNodes.push({
-            id: item.id || item.contactId || null,
-            contactId: item.contactId || null,
-            groupType: config.type,
-            groupTypeName: config.typeName,
-            name: item.name || '未知',
-            displayName: item.name ? item.name.charAt(0) : '?',
-            avatar: item.avatar || null,
-            description: item.description || '',
-            skillCount: item.skillCount || 0,
-            skills: item.skills || [],
-            relationshipType: item.relationshipType || '',
-            radius: (config.type === 'PERSON' || config.type === 'SKILL') ? 24 : 20,
-            color: config.color,
-            x: cx + R * Math.cos(angle),
-            y: cy + R * Math.sin(angle)
-          })
+      function placeRing(ringConfigs, radiusPct) {
+        var R = minDim * radiusPct
+        var n = ringConfigs.length
+        var totalSpan = Math.PI * 2
+        var gap = totalSpan * 0.015
+        var used = 0
+        for (var i = 0; i < n; i++) {
+          var config = ringConfigs[i]
+          // 在 resources.groups 中查找该类型
+          var group = null
+          for (var g = 0; g < self.resources.groups.length; g++) {
+            if (self.resources.groups[g].type === config.type) {
+              group = self.resources.groups[g]
+              break
+            }
+          }
+          if (!group || !group.items || group.items.length === 0) {
+            used += (totalSpan / n)
+            continue
+          }
+
+          // 本组角度区间
+          var groupSpan = totalSpan / n - gap
+          var startAngle = -Math.PI / 2 + used + gap / 2
+          var items = group.items
+
+          for (var k = 0; k < items.length; k++) {
+            var item = items[k]
+            var t = items.length === 1 ? 0.5 : k / (items.length - 1)
+            var angle = startAngle + t * groupSpan
+
+            // 珍珠半径:按 priority 映射(高=大)
+            var priority = item.priority || 2
+            var multiplier = priority === 1 ? 1.5 : priority === 3 ? 0.7 : 1.0
+            var baseR = minDim * radiusPct * 0.09
+            var itemRadius = Math.max(8, Math.min(26, baseR * multiplier))
+
+            self.pearlNodes.push({
+              id: item.id || item.contactId || null,
+              contactId: item.contactId || null,
+              groupType: config.type,
+              groupTypeName: config.typeName,
+              name: item.name || '未知',
+              displayName: item.name ? item.name.charAt(0) : '?',
+              avatar: item.avatar || null,
+              description: item.description || '',
+              skillCount: item.skillCount || 0,
+              skills: item.skills || [],
+              relationshipType: item.relationshipType || '',
+              priority: priority,
+              valueScore: item.valueScore || 0,
+              daysSinceLast: item.daysSinceLast || null,
+              radius: itemRadius,
+              color: config.color,
+              ring: config.ring,
+              x: cx + R * Math.cos(angle),
+              y: cy + R * Math.sin(angle)
+            })
+          }
+          used += (totalSpan / n)
         }
       }
+
+      placeRing(innerConfigs, 0.28)
+      placeRing(outerConfigs, 0.55)
     },
 
     /* ===========================
@@ -317,19 +379,22 @@ export default {
 
       ctx.clearRect(0, 0, w, h)
 
-      // 1. 绘制分组弧线背景(半透明色带)
+      // 1. 绘制分组弧线背景(半透明色带,双层
       this._drawGroupArcs(ctx)
 
       // 2. 绘制中心节点"我"
       this._drawCenterNode(ctx)
 
-      // 3. 绘制珍珠节点
+      // 3. 绘制珍珠节点(内圈先画,外圈后画)
       for (var i = 0; i < this.pearlNodes.length; i++) {
         this._drawPearlNode(ctx, this.pearlNodes[i])
       }
 
       // 4. 绘制分组标签
       this._drawGroupLabels(ctx)
+
+      // 5. 逾期角标(红色圆点+天数,仅 daysSinceLast 超过建议周期的资源)
+      this._drawOverdueBadges(ctx)
     },
 
     /** 绘制每组的弧形色带(背景)和连接线 */
@@ -337,36 +402,40 @@ export default {
       var cx = this.canvasWidth / 2
       var cy = this.canvasHeight / 2
       var minDim = Math.min(this.canvasWidth, this.canvasHeight)
-      var configs = this.groupConfigs
       var self = this
 
-      for (var c = 0; c < configs.length; c++) {
-        var config = configs[c]
-        // 检查该组是否有节点
-        var hasItems = false
+      // 绘制每组的连接线和色带
+      for (var c = 0; c < this.groupConfigs.length; c++) {
+        var config = this.groupConfigs[c]
+        var R = minDim * config.radiusPct
+
+        // 收集该组所有珍珠的角度
+        var groupAngles = []
         for (var p = 0; p < this.pearlNodes.length; p++) {
           if (this.pearlNodes[p].groupType === config.type) {
-            hasItems = true
-            break
+            var dx = this.pearlNodes[p].x - cx
+            var dy = this.pearlNodes[p].y - cy
+            groupAngles.push(Math.atan2(dy, dx))
           }
         }
-        if (!hasItems) continue
+        if (groupAngles.length === 0) continue
 
-        var R = minDim * config.radiusPct
-        var effStart = config.startAngle + this.arcMargin
-        var effEnd = config.endAngle - this.arcMargin
+        // 排序角度
+        groupAngles.sort(function(a, b) { return a - b })
+        var minA = groupAngles[0]
+        var maxA = groupAngles[groupAngles.length - 1]
 
-        // 色带背景(粗线,低透明度
+        // 色带背景(半透明弧
         ctx.beginPath()
-        ctx.arc(cx, cy, R, effStart, effEnd)
+        ctx.arc(cx, cy, R, minA, maxA)
         ctx.lineWidth = 14
         ctx.strokeStyle = self._hexToRgba(config.color, 0.12)
         ctx.lineCap = 'round'
         ctx.stroke()
 
-        // 连接线(细线,沿串联珍珠
+        // 连接线(细弧)
         ctx.beginPath()
-        ctx.arc(cx, cy, R, effStart, effEnd)
+        ctx.arc(cx, cy, R, minA, maxA)
         ctx.lineWidth = 1.5
         ctx.strokeStyle = self._hexToRgba(config.color, 0.3)
         ctx.lineCap = 'butt'
@@ -443,6 +512,15 @@ export default {
       ctx.lineWidth = 2
       ctx.stroke()
 
+      // 高优先级(priority=1)白色粗描边
+      if (node.priority === 1) {
+        ctx.beginPath()
+        ctx.arc(node.x, node.y, node.radius, 0, Math.PI * 2)
+        ctx.lineWidth = 2
+        ctx.strokeStyle = '#FFFFFF'
+        ctx.stroke()
+      }
+
       // 首字
       ctx.font = 'bold ' + Math.max(12, r * 0.65) + 'px PingFang SC, sans-serif'
       ctx.fillStyle = '#FFFFFF'
@@ -456,23 +534,27 @@ export default {
       var cx = this.canvasWidth / 2
       var cy = this.canvasHeight / 2
       var minDim = Math.min(this.canvasWidth, this.canvasHeight)
-      var configs = this.groupConfigs
       var self = this
 
-      for (var c = 0; c < configs.length; c++) {
-        var config = configs[c]
-        // 检查该组是否有节点
-        var hasItems = false
+      for (var c = 0; c < this.groupConfigs.length; c++) {
+        var config = this.groupConfigs[c]
+        // 收集该组所有珍珠的角度和位置
+        var groupAngles = []
         for (var p = 0; p < this.pearlNodes.length; p++) {
           if (this.pearlNodes[p].groupType === config.type) {
-            hasItems = true
-            break
+            var dx = this.pearlNodes[p].x - cx
+            var dy = this.pearlNodes[p].y - cy
+            groupAngles.push(Math.atan2(dy, dx))
           }
         }
-        if (!hasItems) continue
+        if (groupAngles.length === 0) continue
 
+        // 中间角度
+        var minA = groupAngles[0]
+        var maxA = groupAngles[groupAngles.length - 1]
+        var midAngle = (minA + maxA) / 2
         var R = minDim * config.radiusPct
-        var midAngle = (config.startAngle + config.endAngle) / 2
+
         // 标签在弧线外侧
         var labelR = R + 18
         var lx = cx + labelR * Math.cos(midAngle)
@@ -486,6 +568,31 @@ export default {
       }
     },
 
+    /** 逾期角标:红色小圆点 + 天数(仅当 PENDING 提醒语义成立时,由父组件传入 overdueMap) */
+    _drawOverdueBadges: function(ctx) {
+      if (!this.overdueMap) return
+      var self = this
+      var overdueIds = Object.keys(this.overdueMap)
+      for (var i = 0; i < this.pearlNodes.length; i++) {
+        var node = this.pearlNodes[i]
+        if (!node.id) continue
+        var days = this.overdueMap['' + node.id]
+        if (days === undefined || days === null) continue
+        var bx = node.x + node.radius - 4
+        var by = node.y - node.radius + 4
+        // 红底白字小徽标
+        ctx.beginPath()
+        ctx.arc(bx, by, 9, 0, Math.PI * 2)
+        ctx.fillStyle = '#EF4444'
+        ctx.fill()
+        ctx.fillStyle = '#FFFFFF'
+        ctx.font = 'bold 10px sans-serif'
+        ctx.textAlign = 'center'
+        ctx.textBaseline = 'middle'
+        ctx.fillText(String(days), bx, by + 1)
+      }
+    },
+
     /* ===========================
      * 交互:触摸处理
      * =========================== */
@@ -584,6 +691,54 @@ export default {
       this.selectedNode = null
     },
 
+    /** 联系:弹出方式选择 */
+    onContact: function() {
+      var self = this
+      var node = this.selectedNode
+      if (!node || !node.id) return
+      uni.showActionSheet({
+        itemList: ['电话', '微信', '见面', '其他'],
+        success: function(res) {
+          var types = ['PHONE', 'WECHAT', 'MEETING', 'OTHER']
+          addPearlInteraction({
+            itemId: node.id,
+            interactionType: types[res.tapIndex],
+            content: ''
+          }).then(function(resp) {
+            if (resp.code === 200) {
+              uni.showToast({ title: '已记录', icon: 'success' })
+              self.$emit('interaction-updated')
+            } else {
+              uni.showToast({ title: resp.message || '记录失败', icon: 'none' })
+            }
+          })
+        }
+      })
+    },
+
+    /** 调整优先级 */
+    onAdjustPriority: function() {
+      var self = this
+      var node = this.selectedNode
+      if (!node || !node.id) return
+      uni.showActionSheet({
+        itemList: ['高(大珍珠)', '中(默认)', '低(小珍珠)'],
+        success: function(res) {
+          updatePearlPriority({
+            itemId: node.id,
+            priority: res.tapIndex + 1
+          }).then(function(resp) {
+            if (resp.code === 200) {
+              uni.showToast({ title: '已调整', icon: 'success' })
+              self.$emit('interaction-updated')
+            } else {
+              uni.showToast({ title: resp.message || '调整失败', icon: 'none' })
+            }
+          })
+        }
+      })
+    },
+
     /* ===========================
      * Canvas 初始化
      * =========================== */
@@ -821,6 +976,14 @@ export default {
   color: #EF4444;
   margin-left: 16rpx;
 }
+.action-btn-contact {
+  background: #3B82F6;
+  color: #fff;
+}
+.action-btn-priority {
+  background: #8B5CF6;
+  color: #fff;
+}
 .popup-arrow {
   position: absolute;
   bottom: -8rpx;

+ 16 - 44
cfc-frontend/pages/action-detail/index.vue

@@ -66,23 +66,6 @@
         :interactive="true" />
     </view>
 
-    <!-- 登录后:章鱼图(谁帮过我) -->
-    <view class="section" v-if="isLoggedIn">
-      <view class="section-header">
-        <view>
-          <text class="section-title">🐙 章鱼图</text>
-          <text class="section-sub">盘点帮助过你的人——你不是一个人在硬撑</text>
-        </view>
-        <text class="section-link" @tap="goOctopusHelp">+ 记录</text>
-      </view>
-      <OctopusDiagram
-        ref="octopusDiagram"
-        :selfId="selfId"
-        :interactive="true"
-        @view-records="onViewOctopusRecords"
-        @add-help="goOctopusHelp" />
-    </view>
-
     <!-- 登录后:珍珠图(我有什么) -->
     <view class="section" v-if="isLoggedIn">
       <view class="section-header">
@@ -96,6 +79,7 @@
         ref="pearlDiagram"
         :selfId="selfId"
         :resources="pearlData"
+        :overdue-map="overdueMap"
         :interactive="true"
         @add="goPearlAddResource"
         @delete="onDeletePearlResource" />
@@ -178,7 +162,6 @@ import TabTransition from '../../components/tab-transition.vue'
 import PageBanner from '../../components/PageBanner.vue'
 import FamilyEnergyBar from '../../components/FamilyEnergyBar.vue'
 import FamilyRelationGraph from '../../components/FamilyRelationGraph.vue'
-import OctopusDiagram from '../../components/OctopusDiagram.vue'
 import PearlDiagram from '../../components/PearlDiagram.vue'
 import AbilityDiagram from '../../components/AbilityDiagram.vue'
 import FamilyMemberStrip from '../../components/FamilyMemberStrip.vue'
@@ -189,10 +172,10 @@ import DimensionIntroCard from '../../components/DimensionIntroCard.vue'
 import FloatingAvatar from '../../components/AIFloatingAvatar.vue'
 import RadarChart from '../../components/RadarChart.vue'
 import DimensionSubDims from '../../components/DimensionSubDims.vue'
-import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getOctopusTentacles, getPearlResources, deletePearlResource, getAbilityMap } from '../../utils/api.js'
+import { getVisibleSections, getEnergyOverview, getChildren, getFamilyEnergySandbox, getEnergySandbox, getFamilyMemberList, getPearlResources, deletePearlResource, getAbilityMap, getPearlReminders } from '../../utils/api.js'
 
 export default {
-  components: { TabTransition, PageBanner, FamilyEnergyBar, FamilyRelationGraph, OctopusDiagram, PearlDiagram, AbilityDiagram, FamilyMemberStrip, DimensionTasks, DimensionActivities, DimensionProducts, DimensionIntroCard, FloatingAvatar, RadarChart, DimensionSubDims },
+  components: { TabTransition, PageBanner, FamilyEnergyBar, FamilyRelationGraph, PearlDiagram, AbilityDiagram, FamilyMemberStrip, DimensionTasks, DimensionActivities, DimensionProducts, DimensionIntroCard, FloatingAvatar, RadarChart, DimensionSubDims },
   data() {
     return {
       isLoggedIn: false,
@@ -214,6 +197,7 @@ export default {
       visibleSections: [],
       pearlData: null,
       abilityData: null,
+      overdueMap: null,
       funcList: [
         { icon: '\u{1F4AC}', label: '记录互动', needLogin: true, page: '/pages/action-detail/interaction-log' },
         { icon: '\u{1F4DD}', label: '关系问卷', needLogin: true, page: '/pages/action-detail/relationship-questionnaire' },
@@ -341,7 +325,6 @@ export default {
     if (this.isLoggedIn) {
       this.loadChildren()
       this.loadFamilyMembersVisible()
-      this.loadOctopusTentacles()
       this.loadPearlResources()
       this.loadAbilityMap()
     }
@@ -474,29 +457,6 @@ export default {
       }
       if (item.page) uni.navigateTo({ url: item.page })
     },
-    /** 加载章鱼图数据 */
-    loadOctopusTentacles: function() {
-      var self = this
-      getOctopusTentacles().then(function(res) {
-        var tentacles = (res.code === 200 && Array.isArray(res.data)) ? res.data : []
-        if (self.$refs.octopusDiagram) {
-          self.$refs.octopusDiagram.tentacles = tentacles
-        }
-      }).catch(function(e) {
-        console.log('获取章鱼图数据失败', e)
-      })
-    },
-    /** 查看某人的帮助明细 */
-    onViewOctopusRecords: function(contactId) {
-      if (!contactId) return
-      uni.navigateTo({
-        url: '/pages/action-detail/octopus-records?contactId=' + contactId
-      })
-    },
-    /** 记录帮助入口 */
-    goOctopusHelp: function() {
-      uni.navigateTo({ url: '/pages/action-detail/octopus-add-help' })
-    },
     /** 加载珍珠图数据 */
     loadPearlResources: function() {
       var self = this
@@ -506,6 +466,18 @@ export default {
         console.log('获取珍珠图数据失败', e)
         self.pearlData = null
       })
+      // 构建逾期映射
+      getPearlReminders().then(function(res) {
+        var map = {}
+        if (res.code === 200 && res.data) {
+          for (var i = 0; i < res.data.length; i++) {
+            map['' + res.data[i].resourceItemId] = res.data[i].daysSinceLast
+          }
+        }
+        self.overdueMap = map
+      }).catch(function(e) {
+        self.overdueMap = null
+      })
     },
     /** 登记珍珠图资源入口 */
     goPearlAddResource: function() {