玄学放置 · 中间插槽化与装备气泡框交互改造
起因
项目发展到这个阶段,左侧导航只承载"学科"一种菜单。但放置游戏的菜单注定会生长——市场、商店、技能树、成就、设置……每加一个菜单,中间主体区域就要展示完全不同的内容。之前 HomeView 里硬编码 <SubjectPanel />,点哪个学科都只看得到任务卡片,完全不具备扩展性。
同时,右侧装备面板的交互一直停留在"浏览器原生 tooltip"阶段——悬浮出一段纯文本提示,装备点击直接穿戴,槽位点击直接卸下。这种操作方式有两个问题:玩家很难发现"点一下就能穿",因为视觉上和普通物品格没有任何区别;一旦误点就无法撤销,比如本来只想看看属性结果手滑直接把装备卸了。
这次改造解决这两个问题:中间区域插槽化,让不同菜单类型各自承载自己的内容;装备/物品交互从悬浮提示改为点击弹出气泡框,操作放在气泡框内,给玩家一个"看清再决定"的缓冲区。
中间插槽化:MenuType 驱动动态组件
核心设计是在 subjects store 里引入 MenuType 和 currentMenuType:
export type MenuType = 'subject' | 'market' | 'settings'
export interface MenuItem {
id: string
name: string
iconKey: string
color: string
type: MenuType
level?: number
}MenuItem 是一个统一的视图模型。学科菜单项的 type 固定为 'subject',附带 level;扩展菜单(市场、设置)的 type 对应各自的枚举值。
store 的 state 加了 currentMenuType 字段,selectMenu() 方法同时设置 currentId 和 currentMenuType,让两者始终一致:
selectMenu(menuItem: MenuItem): void {
this.currentId = menuItem.id
this.currentMenuType = menuItem.type
}HomeView 里的中间区域换成动态组件:
<component :is="currentMainComponent" :key="store.currentId" />currentMainComponent 根据 currentMenuType 返回不同的组件——学科返回 SubjectPanel,市场返回 MarketPanel,设置返回 SettingsPanel。:key 绑定 currentId 确保切换学科时组件重建,避免数据残留。
这种模式的好处是新增菜单类型时只需要三步:给 MenuType 加一个枚举值、写一个对应的面板组件、在 computed 里加一个 case。HomeView 和 SidebarNav 不需要改动。
SidebarNav 的分组与分割
改造后的侧栏导航不再是一列扁平的学科按钮,而是分组展示:上方"学科"组展示所有学科(带等级),下方通过分割线隔开展示扩展菜单。
分组不是装饰——视觉上的分区暗示了功能上的分区:学科是核心玩法入口,市场和设置是辅助功能。玩家扫一眼就能建立"主功能在上、辅助在下"的心智模型。
isActive() 的判断逻辑也跟着变了。原来只比对 currentId,现在要同时比对 currentId 和 currentMenuType,因为不同类型菜单可能共享 id 命名空间:
function isActive(item: MenuItem): boolean {
return store.currentId === item.id && store.currentMenuType === item.type
}装备气泡框:从 title 属性到 el-popover
原来的交互是这样的:
<button
class="bag__cell"
:title="buildTooltip(entry.item)"
@click="onEquip(entry)"
>浏览器原生 title 属性有几个硬伤:延迟约 1 秒才出现,样式不可控,不支持富文本排版,移动端几乎不可用。更关键的是点击直接触发穿戴/卸下操作——没有"看清再决定"的中间态。
改造后全部换成 el-popover,trigger="click":
<el-popover
placement="left"
:width="200"
trigger="click"
popper-class="item-popover"
>
<template #reference>
<button class="bag__cell" :class="['rarity-' + entry.item?.rarity]">
<PixelIcon :name="entry.item.iconKey" :size="34" />
</button>
</template>
<!-- 气泡内容 -->
<div class="item-pop">
<div class="item-pop__head">...</div>
<div class="item-pop__stats">...</div>
<div class="item-pop__actions">
<button @click="equipItem(entry.itemId)">穿戴</button>
</div>
</div>
</el-popover>气泡框内的结构统一为:头部(图标+名称)→ 品质标签 → 描述 → 属性词条 → 操作按钮。不同场景的操作按钮不同:
- 装备槽:显示"卸下装备"按钮(红色,危险操作视觉提示)
- 背包装备:显示"穿戴"按钮,已装备时变灰并文案改为"已装备"
- 物品 tab:纯展示,显示持有数量,无操作按钮
placement 的方向根据位置选择——左侧装备槽弹出方向选 left,右侧装备槽选 right,背包统一 left。这样气泡框始终往面板外侧展开,不会遮挡主体内容。
品质文字颜色:从边框到标签
原来品质只用边框颜色+发光表达,在气泡框内部这些视觉信号失效了——框内空间紧凑,发光效果挤在一起反而显得杂。所以给气泡框内的品质标签加了一组文字颜色类:
.rarity-text--common { color: #9ca3af; }
.rarity-text--uncommon { color: #4ade80; }
.rarity-text--rare { color: #60a5fa; }
.rarity-text--epic { color: #a78bfa; }
.rarity-text--legendary { color: #fbbf24; }和边框色一一对应,但在小空间里文字颜色比边框发光更容易辨识。玩家不需要盯住一个小格子看边框,直接读品质标签的颜色就行。
目录结构调整
同一轮迭代里,另一个改动是对项目目录结构做了重组。之前的组件全部平铺在 components/ 下,分不出哪些是全局通用、哪些属于特定视图。
调整后的结构:
src/
components/ ← 全局通用组件
PixelIcon/index.vue
ProgressBar/index.vue
views/
Home/
index.vue ← 原 HomeView.vue
components/ ← Home 视图专属组件
ActiveTaskBar/index.vue
ChatPanel/index.vue
EquipmentPanel/index.vue
MarketPanel/index.vue
QueuePopover/index.vue
SettingsPanel/index.vue
SidebarNav/index.vue
SubjectPanel/index.vue每个组件从单文件变成目录套 index.vue,好处有三:
defineOptions({ name })可以写了——单文件组件默认用文件名作 name,改成目录后需要手动声明,换来的是 Vue DevTools 里的组件名更清晰- 未来每个组件可以有自己的
composables.ts/types.ts——不需要提前创建,但目录结构已经为此留了空间 - import 路径更自解释——
@/views/Home/components/EquipmentPanel/index.vue比../components/EquipmentPanel.vue多了"属于 Home 视图"的语义
同时引入了 Vite 的 @ 路径别名,所有相对路径的跨目录 import 统一改成 @/stores/subjects 这种绝对路径写法。相对路径在扁平目录里还好,层级深了以后 ../../stores/xxx 的心智负担明显偏高——数几个点比直接写 @/stores 慢且容易出错。
MarketPanel 和 SettingsPanel 也从 HomeView 里的 render 函数占位组件,拆成了独立的 .vue 文件。render 函数写简单占位没问题,但一旦需要加模板、加交互、加样式,render 函数的可维护性远不如 SFC。早拆早省心。
踩到的坑
CSS 嵌套的 &--modifier 语法。气泡框按钮的样式用了 SCSS 风格的 &--primary / &--danger,但 Vue scoped style 不经过 SCSS 编译(项目没有给 scoped style 配 SCSS preprocessor),原生 CSS 嵌套不允许 & 直接接 -- 开头的类型选择器。Vite 构建时报了两条 css-syntax-error 警告。
修法是把嵌套展开成平铺的 .item-pop__btn--primary / .item-pop__btn--danger,传统但不会出问题。
el-popover 在 v-for 里的 key 稳定性。装备槽的 popover 用 v-for="key in leftSlots" 渲染,:key="key" 直接用槽位 key(head / chest 等),非常稳定。如果用数组 index 作 key,在动态增减元素时可能出现气泡框错位——popover 的显隐状态和 key 绑定,key 变了 popover 可能不关闭或指向错误元素。
收获
这次改造的核心体会:
- 插槽化不是提前设计,是时机对了——当需求已经明确提出"市场菜单"时,从硬编码改成动态组件就是最小改动,不需要引入路由或状态机
- 气泡框比 tooltip 的交互质量高一个台阶——点击触发、富文本排版、操作按钮,三个特性叠加让"看属性"和"做决策"变成了同一个流程而不是两个
- 目录重组和功能改造可以同轮做——只要重组不改变运行时行为,风险就是零,而且新目录结构会让后续开发更顺手
下一篇会围绕市场面板展开——有了插槽化的基础,市场功能的实现只需要写组件、加 mock 数据、接 store,三步走完。