QuickAccessCard 常用菜单逻辑说明
大约 6 分钟
QuickAccessCard 常用菜单逻辑说明
记录:基于 VuePress 2 + vuepress-theme-hope + Vuex 的“常用菜单”实现,包含点击统计、时间衰减和排序逻辑,便于后续项目复用。
一、整体目标
实现一个“常用菜单”模块:
- 自动记录每个页面的访问情况
- 计算一个“热度分数”(score)
- 在页面上展示“最常用的若干菜单”(比如前 15 个)
- 引入时间衰减:老点击的影响逐渐变小,能反映最近的使用习惯
对应组件:QuickAccessCard。
二、数据流总体设计
整体数据流分为三层:
- 路由守卫:捕获每一次路由跳转,调用
store.commit('SET_PAGE_COUNT', to)记录点击。 - Vuex Store:
state.pageCountMap保存所有页面的统计数据(含时间衰减字段)。mutations.SET_PAGE_COUNT负责更新单个页面的数据(点击次数 + 时间衰减)。getters.popularPages负责对所有页面按“热度 score”排序,返回前 N 个常用菜单。
- 展示组件 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 = 1,decayFactor = 0.9,衰减 10%。 - 如果
deltaDays = 7,decayFactor = 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%,更偏向“最近几天”的使用情况。
- 例如:
九、在新项目中的复用建议
复制这三部分结构:
- 路由守卫(beforeEach 里 commit
SET_PAGE_COUNT)。 - Vuex store:
pageCountMap+SET_PAGE_COUNT+popularPages。 - 展示组件:
QuickAccessCard(或其他 UI 形式)。
- 路由守卫(beforeEach 里 commit
按新项目的需求调整:
- 是否需要排除某些路径(例如后台管理页)。
slice(0, 15)中的数量。decayBase的值,决定“记忆时长”。
如果页面标题不在
meta.t,记得改为你自己的标题来源字段。
以上就是当前项目中 QuickAccessCard 的“时间衰减版常用菜单逻辑”的完整说明,可在新项目中直接复制/调整使用。