先说结论:做AI对话类应用,如果你要同时撑住"当前会话、历史会话、没发出去的草稿"这三种状态,别再用一堆零散的 ref 硬扛了,拿 Pinia 开一个 store,把会话当成一个 Map 来管,切起来是真的顺。
事情是这样的。前段时间我接了个内部活儿,给运营那帮人做一个能跟AI聊天的小工具。一开始我想得特简单,不就一个输入框一个消息列表嘛。结果第二天产品就来加需求:能不能像那种聊天软件一样,左边一栏历史会话,点哪个进哪个,而且我打了一半没发的字,切走再切回来不能丢。
行吧。一丢草稿我才意识到,这是个状态管理问题,不是 UI 问题。
数据怎么摆
我先把脑子里乱成一团的东西捋成一个结构。核心就一句话:所有会话存一个对象,再单独记一个"当前是谁" 。
javascript
// stores/chat.js
import { defineStore } from 'pinia'
export const useChatStore = defineStore('chat', {
state: () => ({
sessions: {}, // { [id]: { id, title, messages, draft } }
activeId: null, // 当前激活的会话 id
}),
getters: {
current: (s) => s.sessions[s.activeId] || null,
history: (s) => Object.values(s.sessions)
.sort((a, b) => b.updatedAt - a.updatedAt),
},
})
current 这个 getter 是关键。组件里我永远只认 current,从来不直接去摸 sessions[xxx]。这样切会话的本质,就退化成改一个 activeId 而已。
切换:就是换个指针
javascript
actions: {
switchTo(id) {
if (this.sessions[id]) this.activeId = id
},
newSession() {
const id = crypto.randomUUID()
this.sessions[id] = {
id, title: '新对话', messages: [],
draft: '', updatedAt: Date.now(),
}
this.activeId = id
},
}
切换不搬数据,只动 activeId。消息列表组件里绑 current.messages,输入框绑 current.draft。activeId 一变,整个视图跟着换,Vue 的响应式自己就接上了。我第一次跑通的时候还愣了一下------草稿居然真的不丢了,因为它压根没存在组件里,存在会话对象身上呢。
草稿:别防抖,直接写
输入框我是这么接的:
dart
// 组件里
const chat = useChatStore()
const draft = computed({
get: () => chat.current?.draft ?? '',
set: (v) => { if (chat.current) chat.current.draft = v },
})
一个 computed 的 get/set 怼上去,敲字直接落到当前会话的 draft 字段。我本来还想加个 300ms 防抖,后来一想犯不上,这又不是请求,就是改内存里一个字符串,V 一下就完事。
发送的时候把 draft 推进 messages,再清空:
javascript
send() {
const c = this.current
if (!c?.draft.trim()) return
c.messages.push({ role: 'user', content: c.draft })
c.draft = ''
c.updatedAt = Date.now()
}
持久化顺手做了
刷新别丢,我用 pinia-plugin-persistedstate 把整个 chat store 落到 localStorage。一行配置的事。唯一的坑是:正在请求中的 loading 态别一起持久化,不然刷新后会卡个假的"正在输入",我后来把临时态单独拎出来不入库才干净。
说点不好的。这套结构会话一多------我塞到两百多条历史的时候------Object.values 那个 sort getter 每次都全量算一遍,列表滚动有点黏。后来给 history 做了个分页截断才缓过来。所以别迷信"全存内存最爽",量上来还是得收着点。
整个 store 写完也就五十来行,但配套的对话本身才是大头:接哪个大模型、要不要挂知识库、prompt 怎么调。这部分我没自己造,用了一个零代码就能配智能体的平台,拖一拖把模型和我那份产品文档的知识库挂上,发布成一个 API,前端这边只管 store 和 UI。等于我把"AI 怎么想"外包了,自己专心管"会话怎么切"。说实话挺解放------一个前端,硬是没写后端就把这玩意儿跑起来了。
(对了,模型这块我走的是讯飞的 MaaS,现成 API 调,没自己部署算力。)
你们做多会话是怎么存草稿的?也是挂在会话对象上,还是单独开了个 draft store?评论区聊聊,我这套撑到两百条就有点喘了,想看看有没有更稳的姿势。