Day 5 搭三栏工作台:El-Splitter 分栏、Pinia 管账号与当前页,中间先留 webview 位。
开场:登录进来了,不能还是一整块白屏
昨天,登录成功,router.replace('/main')------老王第一次进 AnchorChat 主界面,预期是day 规划中白板上那三个格子:左选账号、中间聊天、右边工具。
今天把 布局骨架 搭出来。中间会挂上 ChatHost 组件占位,但 <webview> 怎么嵌 Telegram Web 留到明天------别贪,先把 Split 面板和 Pinia 状态理顺,后面 25 天都是往格子里填东西。

整体结构:WorkspaceView 一个页面扛大旗
登录后进 /main,渲染 WorkspaceView.vue(主工作台)。它比 Day 2 的 Hello World 复杂,但顶层就四层:
text
AppTitleBar(Day 3)
└── el-splitter(横向或纵向,可拖拽)
├── 左 panel → AccountSidebar(账号 / 平台)
└── 右 panel → el-main(中间工作区)+ el-aside(ToolSidebar)
用 Element Plus 的 el-splitter 做可拖拽分栏------比手写 mousemove 监听省头发🐶:
vue
<el-splitter
ref="layoutSplitter"
:layout="layoutHorizontal ? 'horizontal' : 'vertical'"
@resize="onSidebarResize"
@resize-end="notifyChatHostLayout"
>
<!-- 左:账号栏 -->
<el-splitter-panel :size="sidebarSize" :min="252">
<AccountSidebar
@platform-change="onPlatformChange"
@account-select="onAccountSelect"
@open-translate-drawer="openTranslateDrawer"
/>
</el-splitter-panel>
<!-- 右:中间 + 右侧工具 -->
<el-splitter-panel min="50%">
<el-main>
<ChatHost
v-for="host in visibleChatHosts"
:key="host.id"
:account="host"
v-show="host.isActive"
/>
<DashboardView v-show="workspaceView === 'dashboard'" />
</el-main>
<el-aside v-show="workspaceView !== 'dashboard'">
<ToolSidebar
:active-account="selectedAccount"
:collapsed="toolPanelCollapsed"
@close="closeToolPanel"
/>
</el-aside>
</el-splitter-panel>
</el-splitter>
为什么中间和右工具在同一个 splitter-panel 里?
左栏独立拖拽宽度;中间 webview 和右侧翻译/CRM 工具 共享剩余空间 ,右侧 el-aside 自己再 collapse。
三栏分别干什么
| 区域 | 组件(连载名) | Day 5 职责 |
|---|---|---|
| 左 | AccountSidebar |
平台入口、账号列表、添加 / 休眠 / 激活 |
| 中 | ChatHost + 若干 v-show 子页 |
当前激活 IM 的 webview 容器 |
| 右 | ToolSidebar |
翻译、快捷回复、客户备注等 Tab |
Pinia:谁 active、当前在哪个「子页面」
三栏联动靠 Pinia,少层层 props:
1. 账号列表 --- useAccountStore
typescript
export const useAccountStore = defineStore('accounts', () => {
const accounts = ref<ChatAccount[]>([])
const loading = ref(true)
function setAccounts(list: ChatAccount[]) {
loading.value = false
accounts.value = list
}
return { accounts, loading, setAccounts }
})
登录后拉「我的 IM 实例」写进 accounts。每项大致含:id、platformKey(telegram / whatsapp...)、isActive、isSleeping。
2. 当前挂载哪些 ChatHost --- visibleChatHosts
typescript
const accountStore = useAccountStore()
const mountBatchLimit = ref(3)
const visibleChatHosts = computed(() => {
return accountStore.accounts
.filter((a) => !a.pendingReload)
.filter((a) => !a.isSleeping)
.filter((a) => a.isActive || a.wasOpened)
.slice(0, mountBatchLimit.value)
})
点左侧账号 → 对应项 isActive = true → 中间 ChatHost 显示。休眠账号不进列表,省内存。
3. 中间显示聊天还是设置 --- useWorkspaceStore
typescript
export const useWorkspaceStore = defineStore('workspace', () => {
// 'dashboard' | 'chat' | 'settings' | ...
const view = ref<'dashboard' | 'chat' | 'settings'>('dashboard')
return { view }
})
view === 'dashboard' 出首页仪表盘;'chat' 出 IM 工作区;'settings' 切系统设置------同一 WorkspaceView 里 v-show 切换,不必每个设置页单独加子路由,桌面应用里很常见。
登录成功后会清掉残留 isActive,并 view = 'dashboard',避免一进来闪上次打开的 Telegram。
Splitter 拖拽:左栏宽度与 webview 尺寸
拖左栏时两件事:
- 紧凑模式 :宽度小于阈值 →
sidebarCompact = true,只露图标 - 通知 webview 重排 :Split 结束调
notifyChatHostLayout(),否则 IM Web 版布局会歪
typescript
const sidebarCompact = ref(true)
const sidebarSize = ref(252)
function onSidebarResize(_index: number, sizes: number[]) {
sidebarCompact.value = sizes[0] <= 265
sidebarSize.value = Math.max(sizes[0], 252)
// translateDrawer 的 left/top 跟随 sidebarSize ...
}
竖版布局时左栏变顶栏,sidebarSize 最小高度约 90px------有的客服用竖屏,我们留了 layoutHorizontal 开关。
启动遮罩:别让用户看布局抖三秒
bootLoading 为 true 时盖一层「加载中」:AccountSidebar 拉完平台 + 账号列表后触发 onAccountsReady,再隐藏。否则用户看见 Splitter 从 0 宽度弹出来------像未完成的 PPT
踩坑与思考
- 一次 mount 太多 ChatHost :即使用
v-show也吃内存。mountBatchLimit渐进加载 +isSleeping休眠 v-showvsv-if:常切换用v-show保登录态;彻底卸载用pendingReload过滤------别混用把 Cookie 弄丢- Splitter 最小宽度 252 :改主题时同步改
onSidebarResize里的阈值,否则左栏「折叠了还能拖出 1px 缝」 - 翻译抽屉 vs 右栏动画:两套 UI 状态可能互斥,打开抽屉时记得收右栏
明日预告
在 ChatHost.vue 里写第一个 <webview>,解析 preload 路径,让 Telegram Web 第一次在 AnchorChat 中间栏亮起来------至少能看见登录二维码。