QuickAccessCard 常用菜单逻辑说明

lishihuan大约 6 分钟

QuickAccessCard 常用菜单逻辑说明

记录:基于 VuePress 2 + vuepress-theme-hope + Vuex 的“常用菜单”实现,包含点击统计、时间衰减和排序逻辑,便于后续项目复用。


一、整体目标

实现一个“常用菜单”模块:

  • 自动记录每个页面的访问情况
  • 计算一个“热度分数”(score)
  • 在页面上展示“最常用的若干菜单”(比如前 15 个)
  • 引入时间衰减:老点击的影响逐渐变小,能反映最近的使用习惯

对应组件:QuickAccessCard


二、数据流总体设计

整体数据流分为三层:

  1. 路由守卫:捕获每一次路由跳转,调用 store.commit('SET_PAGE_COUNT', to) 记录点击。
  2. Vuex Store
    • state.pageCountMap 保存所有页面的统计数据(含时间衰减字段)。
    • mutations.SET_PAGE_COUNT 负责更新单个页面的数据(点击次数 + 时间衰减)。
    • getters.popularPages 负责对所有页面按“热度 score”排序,返回前 N 个常用菜单。
  3. 展示组件 QuickAccessCard
    • 直接从 this.$store.getters.popularPages 读取列表,渲染到页面。

三、路由守卫:记录每一次访问

文件:src/.vuepress/router/router.js(简化示意)

router.beforeEach((to, from, next) => {
  if (to.name && to.name !== '404') {
    // 关键:每次路由跳转时记录一次访问
    store.commit('SET_PAGE_COUNT', to)
  }
  next()
})

说明:

  • to 是即将进入的路由对象,包含:
    • to.path:页面路径
    • to.meta.t:页面标题(在主题/路由配置时写入)
  • 每一次路由跳转(排除 404)都会触发一次 SET_PAGE_COUNT,相当于“该菜单被点击了一次”。

四、Vuex Store 状态结构

文件:src/.vuepress/store/store.js

1. state:持久化的点击数据

state: {
  pageCountMap: (typeof window !== 'undefined')
    ? JSON.parse(window.localStorage.getItem('pageCountMap')) || {}
    : {},
},
  • pageCountMap 是一个对象,key 是 path,value 是该页面的统计信息。
  • 使用 localStorage 持久化,刷新页面 / 重新打开浏览器后数据仍然存在。

单个页面的结构,例如:

pageCountMap['/notes/java/java基础.html'] = {
  path: '/notes/java/java基础.html',
  name: 'Java 基础',     // 来自路由 meta.t
  count: 12,             // 总点击次数(历史累积)
  lastClickTime: 1710681600000, // 最近一次点击时间戳(ms)
  score: 8.35,           // 当前“热度分数”,用于排序
}

字段含义

  • path:页面路由路径
  • name:页面标题,用于展示
  • count:总点击次数
  • lastClickTime:最近一次点击时间(Date.now()
  • score带时间衰减的热度分数,排序依据

五、时间衰减逻辑(方案 A 的具体实现)

1. 设计目标

  • 最近频繁点击的页面,score 越高,越容易排在前面。
  • 很久之前频繁点击、但最近几乎不用的页面,其 score 会逐渐变小,最终被新常用菜单替代。
  • 保留 count 作为总点击次数参考,但排序主要看 score

2. 关键公式

mutations.SET_PAGE_COUNT 中(简化后的核心逻辑):

SET_PAGE_COUNT (state, pathItem) {
  const key = pathItem.path
  const now = Date.now()
  const decayBase = 0.9 // 每天保留 90% 的权重
  const exist = state.pageCountMap[key]

  if (exist) {
    const lastClickTime = exist.lastClickTime || now
    const lastScore = typeof exist.score === 'number'
      ? exist.score
      : (exist.count || 0)

    const deltaDays = Math.max(0, (now - lastClickTime) / (1000 * 60 * 60 * 24))
    const decayFactor = Math.pow(decayBase, deltaDays)

    const newScore = lastScore * decayFactor + 1

    exist.count = (exist.count || 0) + 1
    exist.lastClickTime = now
    exist.score = newScore
  } else {
    state.pageCountMap[key] = {
      path: pathItem.path,
      name: pathItem.meta.t,
      count: 1,
      lastClickTime: now,
      score: 1,
    }
  }

  if (typeof window !== 'undefined') {
    window.localStorage.setItem('pageCountMap', JSON.stringify(state.pageCountMap))
  }
}

3. 公式逐行解释

  • decayBase = 0.9

    • 表示“每过去 1 天,保留原先权重的 90%”。
    • 第 1 天后:权重乘以 0.9
    • 第 2 天后:权重乘以 0.9^2 ≈ 0.81
    • 第 n 天后:权重乘以 0.9^n
  • deltaDays

    const deltaDays = (now - lastClickTime) / (1000 * 60 * 60 * 24)
    

    表示距离上一次点击过去了多少天(可以是小数,比如 0.5 天)。

  • decayFactor

    const decayFactor = Math.pow(decayBase, deltaDays)
    

    根据时间间隔计算衰减系数:

    • 如果 deltaDays = 0(刚刚点过),decayFactor = 1,不衰减。
    • 如果 deltaDays = 1decayFactor = 0.9,衰减 10%。
    • 如果 deltaDays = 7decayFactor = 0.9^7 ≈ 0.478,一周不用,热度减半左右。
  • lastScore

    const lastScore = typeof exist.score === 'number'
      ? exist.score
      : (exist.count || 0)
    

    兼容旧数据:

    • 如果之前已经有 score 字段,就用 score
    • 如果是升级前的老数据(只有 count),就用 count 当作初始 score。
  • newScore

    const newScore = lastScore * decayFactor + 1
    
    • 先对旧的 score 按时间衰减。
    • 然后因为“本次点击”,再 +1。
    • 这样“时间越久 + 点击越少”的页面,score 会逐渐变小; 而“最近经常被点”的页面,score 会不断累加,保持在前面。

4. 新老数据兼容

  • 老版本只存了 path、name、count
    • 第一次升级后访问时,exist.score 不存在,会使用 count 作为初始 score。
    • 然后继续按新公式进行衰减和 +1,平滑过渡。

六、热门菜单列表 getter:popularPages

getters: {
  popularPages (state) {
    const items = Object.values(state.pageCountMap || {})
      .filter(item => item && item.path !== '/' && item.name) // 过滤首页和无标题项
      .map(item => {
        const baseScore = typeof item.score === 'number'
          ? item.score
          : (item.count || 0)
        return {
          ...item,
          score: baseScore,
        }
      })
      .sort((a, b) => b.score - a.score) // 按 score 从大到小排序
      .slice(0, 15)                      // 只保留前 15 条

    return items
  },
},

说明:

  • 再次兼容旧数据:确保每一项都有 score 字段。
  • 过滤规则:
    • 去掉首页 path === '/'
    • 去掉没有 name 的记录(避免显示空菜单名)
  • 排序规则:
    • 按照衰减后的 score 排序,而不是按总点击次数。
  • 截取前 15 个作为常用菜单。

七、展示组件:QuickAccessCard

文件:

  • src/notes/other/个人知识库/components/QuickAccessCard.vue
  • .vuepress/components/QuickAccessCard.vue(在 VuePress 布局中复用)

核心代码:

<template>
  <div class="module" v-if="cards.length > 0">
    <h2 class="module-title">常用菜单</h2>
    <el-row class="card-container">
      <el-col
        class="card"
        v-for="(card, index) in cards"
        :key="index"
        :xs="24"
        :sm="12"
        :md="12"
        :lg="8"
        :xl="6"
      >
        <div class="nav-card df_ac_jcc" @click.native="gotoSite(card.path)">
          {{ card.name }}
        </div>
      </el-col>
    </el-row>
  </div>
</template>

<script>
export default {
  name: 'QuickAccessCard',
  data () {
    return {
      // 直接使用 Vuex 计算好的热门菜单列表
      cards: this.$store.getters.popularPages,
    }
  },
  methods: {
    gotoSite (path) {
      this.$router.push(path)
    },
  },
}
</script>

说明:

  • 组件本身非常“薄”,核心逻辑都放在 Vuex:
    • 统计点击
    • 时间衰减
    • 排序
  • 组件的职责只是:
    • 读数据:this.$store.getters.popularPages
    • 展示卡片
    • 跳转到对应的 path

八、效果总结

1. 解决的问题

  • 短期疯狂点击但后来不用 的页面:

    • 随着时间推移,score 被衰减,热度越来越低。
    • 如果用户不再点击,该页面最终会被最近常用的菜单挤出榜单。
  • 最近高频使用 的页面:

    • 每一次点击都会给 score 增加 1。
    • 即使总点击不如老页面多,只要最近使用频繁,就能排到前面。

2. 调整参数的方式

  • 如果觉得衰减太快 / 太慢,可以调整:

    const decayBase = 0.9 // 每天保留 90% 的权重
    
    • 例如:
      • 0.95:每天只衰减 5%,老数据影响更持久。
      • 0.8:每天衰减 20%,更偏向“最近几天”的使用情况。

九、在新项目中的复用建议

  1. 复制这三部分结构:

    • 路由守卫(beforeEach 里 commit SET_PAGE_COUNT)。
    • Vuex store:pageCountMap + SET_PAGE_COUNT + popularPages
    • 展示组件:QuickAccessCard(或其他 UI 形式)。
  2. 按新项目的需求调整:

    • 是否需要排除某些路径(例如后台管理页)。
    • slice(0, 15) 中的数量。
    • decayBase 的值,决定“记忆时长”。
  3. 如果页面标题不在 meta.t,记得改为你自己的标题来源字段。


以上就是当前项目中 QuickAccessCard 的“时间衰减版常用菜单逻辑”的完整说明,可在新项目中直接复制/调整使用。