|
`persistGroupTables` 位于 `frontend_v2/src/views/pages/AiQuery.vue` 第 2292 行,作用是**把用户当前选择的表(`selectedTables`)持久化绑定到当前对话会话(`currentGroup`)**。
```javascript
// 选表与会话绑定:变更后持久化到服务端(切页/应用历史会话后恢复)
function persistGroupTables() {
const groupId = currentGroup.value?.id
if (!groupId) return
const tables = [...selectedTables.value]
selectedTablesStore.set(groupId, tables)
// 同步内存中的组对象(当前组/组列表/历史列表):switchChat 恢复时优先取
// group.selected_tables,若只更新服务端而内存组对象留旧值,
// 会出现"应用历史会话→新增表→切走→切回"后新增的绑定丢失
if (currentGroup.value) currentGroup.value.selected_tables = tables
const syncList = (list) => {
if (Array.isArray(list)) {
const g = list.find(item => item && item.id === groupId)
if (g) g.selected_tables = tables
}
}
syncList(chatGroups.value)
syncList(chatHistoryList.value)
aiChatApi.updateGroupTables(groupId, tables).catch(err => {
console.error('保存会话选表失败:', err)
})
}
```
|
|
### 第 1 部分:取组 ID 并防御
```javascript
const groupId = currentGroup.value?.id
if (!groupId) return
```
| 代码 | 语法点 | 解释 |
|------|--------|------|
| `currentGroup.value` | Vue 3 `ref` 的取值 | `currentGroup` 是用 `ref()` 创建的响应式变量,JS 代码里读写必须加 `.value`(模板里不用加) |
| `?.` | 可选链操作符 | "如果前者不是 null/undefined 就继续取后面的属性,否则整个表达式返回 undefined"。等价于 `currentGroup.value && currentGroup.value.id`,但写法更短、更安全 |
| `const` | 常量声明 | 声明后不能重新赋值(对象内容仍可修改) |
| `if (!groupId) return` | 提前返回(卫语句) | `!groupId` 在 groupId 为 `undefined`/`null`/`0` 时为 true;没有当前会话就直接退出,后面代码不执行 |
### 第 2 部分:复制数组 + 写内存缓存
```javascript
const tables = [...selectedTables.value]
selectedTablesStore.set(groupId, tables)
```
| 代码 | 语法点 | 解释 |
|------|--------|------|
| `[...数组]` | 展开运算符(浅拷贝) | `...` 把数组元素逐个"倒"进新数组,得到一份**副本**。这样后续别人改 `selectedTables` 不会影响已存下的 `tables`,反之亦然 |
| `selectedTablesStore.set(k, v)` | Map 的 set | `selectedTablesStore` 是一个 `new Map()`(键值对集合),`set(组id, 选表数组)` 缓存每个会话的选表,供同一次页面停留内快速切换恢复 |
### 第 3 部分:同步内存中的组对象
```javascript
if (currentGroup.value) currentGroup.value.selected_tables = tables
```
- 普通对象属性赋值:把当前组对象上的 `selected_tables` 字段改成最新值。
- `if (currentGroup.value)` 先判空,防止对 `undefined` 取属性报错(`TypeError: Cannot set property of undefined`)。
```javascript
const syncList = (list) => {
if (Array.isArray(list)) {
const g = list.find(item => item && item.id === groupId)
if (g) g.selected_tables = tables
}
}
syncList(chatGroups.value)
syncList(chatHistoryList.value)
```
| 代码 | 语法点 | 解释 |
|------|--------|------|
| `(list) => { ... }` | 箭头函数 | 匿名函数的简写,赋给常量 `syncList` 便成了可复用的函数,等价于 `function syncList(list) { ... }` |
| `Array.isArray(list)` | 类型判断 | 判断是不是数组,防止传入 null/undefined 时调 `.find` 报错 |
| `list.find(fn)` | 数组查找 | 从头遍历,返回**第一个**让 fn 返回 true 的元素;找不到返回 `undefined`(不会报错) |
| `item => item && item.id === groupId` | 回调 + 短路求值 | `&&` 左边为假则右边不执行,先保证 item 存在再比较 id,避免 `item` 为 null 时读属性报错 |
| `===` | 严格相等 | 不做类型转换(`1 == '1'` 是 true,`1 === '1'` 是 false),JS 里推荐永远用 `===` |
这一段把**组列表**(`chatGroups`)和**对话历史列表**(`chatHistoryList`)里同一个组的 `selected_tables` 也更新掉。
### 第 4 部分:异步保存到服务端
```javascript
aiChatApi.updateGroupTables(groupId, tables).catch(err => {
console.error('保存会话选表失败:', err)
})
```
| 代码 | 语法点 | 解释 |
|------|--------|------|
| `aiChatApi.updateGroupTables(...)` | 返回 Promise 的异步请求 | 内部用 fetch 发 `PUT /api/v1/old/conversation/groups/{id}/tables`,请求体 `{"tables": [...]}` |
| `.catch(fn)` | Promise 错误处理 | 网络失败/服务端报错时执行 fn,**不写 catch 的话错误会被静默吞掉**,控制台出现 Uncaught (in promise) |
| 不 `await` | "发射后不管" | 保存动作在后台进行,用户界面不用等它完成;失败只打日志,不打断操作 |
|
|
### 双写策略:内存 + 服务端
方法做了一次"**双写**":同一个 `tables` 值写到 **4 个地方**,各有用途:
| 写入位置 | 类型 | 谁会读它 |
|----------|------|----------|
| `selectedTablesStore`(Map 缓存) | 仅内存(页面刷新即失) | 同次停留内快速切换会话时的兜底恢复 |
| `currentGroup.selected_tables` | 内存中的组对象 | `switchChat` 切回该组时恢复选表 |
| `chatGroups` / `chatHistoryList` 中的组对象 | 内存中的列表 | 同上(列表里的对象与 currentGroup 可能不是同一个引用) |
| 服务端数据库(PUT 接口) | 持久化 | 刷新页面/重新登录/换电脑后,`GET /groups` 返回最新绑定 |
### 绑定表功能的完整闭环
1. **绑定(写)**:用户勾选/去选表 → `toggleTable`/`deselectTable` → `persistGroupTables()` 双写内存+服务端
2. **恢复(读)**:切换页面再回来、应用历史会话、切换对话组 → `switchChat` 从 `group.selected_tables`(优先)或内存缓存恢复 `selectedTables`
3. **再绑定(更新)**:恢复后又新增表 → 再次走第 1 步全量覆盖——所以"应用历史会话后新增的表"也会更新绑定到当前会话
### 为什么要同步内存组对象(v1.1 修复的 bug)
`switchChat` 恢复时**优先取组对象上的 `selected_tables`**。如果只更新服务端而内存组对象留着旧值:
```
应用历史会话 → 恢复 [A, B]
新增表 C → 服务端已变成 [A, B, C],但内存组对象还是 [A, B]
切到别的组再切回 → switchChat 用内存旧值恢复 → C 丢了
```
同步内存对象后,任何读取路径拿到的都是最新值。
|
|
### 谁调用了 persistGroupTables(4 个入口)
| 入口函数 | 用户操作 | 场景 |
|----------|----------|------|
| `toggleTable(tableName)` | 选表列表里勾选/取消一张表 | 唯一的"选/去选"入口,变更后立即调用 |
| `deselectTable(tableName)` | 已选标签上的 × 去选 | 同上 |
| `toggleFieldPopup(table)` | 打开字段浮层时自动选中该表 | 隐式新增绑定 |
| `handleReset()` | 清空对话 | 选表一并清空(`tables = []` 全量覆盖) |
### 恢复侧:switchChat 的关键代码
```javascript
// 切换对话(应用历史会话/切组/页面回显都走这里)
const boundTables = Array.isArray(group?.selected_tables) ? group.selected_tables : null
if (boundTables) {
selectedTables.value = [...boundTables] // 优先:会话绑定的选表(服务端字段)
} else {
selectedTables.value = selectedTablesStore.get(group.id) || [] // 兜底:内存缓存
}
```
- `Array.isArray(x) ? A : B`:三元表达式,x 是数组取 A 否则取 B——防御历史数据没有该字段(null)的情况。
- 提问时后端 `completions` 也会把请求里的 `table_info.tables` 自动写入组,是绑定的第 5 条路径。
|
|
### 本方法涉及的 JS/Vue 核心语法速记
| 语法 | 名称 | 一句话记忆 |
|------|------|-----------|
| `x?.y` | 可选链 | x 存在才取 y,否则 undefined,不报错 |
| `[...arr]` | 展开拷贝 | 复制一份数组,隔离修改 |
| `(a) => expr` | 箭头函数 | 匿名函数简写,常作回调 |
| `arr.find(fn)` | 数组查找 | 返回第一个满足条件的元素,找不到是 undefined |
| `===` | 严格相等 | 不转换类型,永远优先用它 |
| `&&` 短路 | 逻辑与 | 左边假则右边不执行,常用于防御 |
| `!x` | 取反 | undefined/null/0/''/false 都是真"空" |
| `.catch(fn)` | Promise 兜底 | 异步失败的处理,不写就静默吞错 |
| `ref().value` | Vue 响应式 | script 里读写要 `.value`,模板里不用 |
| 卫语句 | 提前 return | 条件不满足立刻退出,主逻辑不嵌套 |
### 常见初学者坑
- **忘了 `.value`**:`currentGroup.id` 是 undefined(读的是 ref 包装对象本身)
- **直接赋数组引用**:`const tables = selectedTables.value` 后两边会"联动"修改,必须拷贝
- **不写 `.catch`**:请求失败无任何提示,排查困难
- **同步列表时漏了某个列表**:某个读取路径拿到旧值,出现"偶现丢失"这类难查的 bug
|
|
### 一句话理解
`currentGroup` 是 AiQuery.vue 页面里的一个**响应式变量**(`ref`),记录"**当前正在和哪个会话对话**"。它像一个指针:无论用户是切换历史会话、点选左侧对话列表,还是页面刚加载默认选中第一个会话——只要"当前会话"变了,代码就会**主动更新** `currentGroup`,让整个页面围绕同一个会话状态工作。
### 定义(AiQuery.vue 第 1304 行)
```javascript
const currentGroup = ref(null) // 初始为 null:页面刚打开还没有任何会话
```
- `ref(null)` 创建一个响应式包装:`currentGroup.value` 在 script 里读写(模板里直接写 `currentGroup`)
- 初始 `null` 是"空状态"信号,所有使用处都用 `currentGroup.value?.id`、`if (!currentGroup.value)` 做防御
### 它装的是什么数据
值就是**后端 `GET /groups` 返回的一个组对象**(数组中的一项),典型字段:
| 字段 | 含义 |
|------|------|
| `id` | 会话(对话组)唯一标识,绑定/提问/清空都靠它 |
| `name` / `group_name` | 会话名称(默认取用户第一次提问的前 100 字) |
| `session_name` | 业务会话名(固定"风险筛查") |
| `selected_tables` | 会话绑定的选表数组(绑定表功能的核心字段) |
| `created_by` / `created_at` | 创建人 / 创建时间 |
|
|
### 两处主动更新点(谁给 currentGroup 赋值)
**① 页面加载/刷新组列表:默认选中第一个会话**(`fetchChatGroups`,第 1842 行)
```javascript
if (chatGroups.value.length > 0) {
activeChatIndex.value = 0
currentGroup.value = chatGroups.value[0] // 默认指向第一个组
await switchChat(0, chatGroups.value[0])
}
```
**② 切换会话:所有切换路径的汇聚点**(`switchChat`,第 1926 行)
```javascript
async function switchChat(index, group) {
// 先保存旧会话的选表
if (currentGroup.value?.id) {
selectedTablesStore.set(currentGroup.value.id, [...selectedTables.value])
}
activeChatIndex.value = index
currentGroup.value = group // ★ 指针切换:新会话成为当前会话
// 恢复新会话的选表、加载历史消息……
}
```
### "应用历史会话"如何走到这里
对话历史弹窗里的"使用该对话"按钮,最终也是调用 `switchChat`:
```
useChatFromHistory(group) // 弹窗里点"使用该对话"
→ switchToChatFromDialog() // 找到该组在列表中的位置
→ switchChat(idx, group) // ★ currentGroup.value = group
```
所以结论是:**当前会话或历史会话,一旦被切换为当前会话,`switchChat` 都会主动把 `currentGroup` 更新为该会话对象**——历史会话"变成"当前会话后,后续所有操作(提问、绑表、清空)都自然落在这个会话上。
|
|
### 页面里谁在读 currentGroup
| 使用处 | 用途 |
|--------|------|
| 模板"当前"徽标 | `v-if="currentGroup && currentGroup.id === group.id"` 给列表中当前会话打"当前"标记 |
| 提问 `sendMessage` | 请求体带 `group_id: currentGroup.value.id`,消息落到当前会话 |
| `persistGroupTables` | 取 `currentGroup.value?.id` 作为绑定目标;并回写 `selected_tables` |
| 清空对话 `handleReset` | 用 `currentGroup.value.id` 只清当前会话的消息 |
| 重命名 `renameChat` | 更新 `currentGroup.value.name`,界面即时生效 |
| 删除消息 `deleteMessage` | 校验 `currentGroup.value?.id` 存在才执行 |
### 设计意义:单一数据源(Single Source of Truth)
页面有多个地方需要知道"现在是哪个会话"(提问按钮、选表弹窗、历史面板、清空按钮……)。如果各自记一份,切换时容易漏更新、状态不一致。统一收口到 `currentGroup` 一个变量:
- **写**:只有 2 处赋值(默认选中 + switchChat),好排查
- **读**:所有消费方读同一份,切换后自动全部"跟着变"——这就是 Vue 响应式的价值:`currentGroup` 变了,依赖它的模板("当前"徽标)和逻辑自动更新
|
|
### 绑定表功能中两者如何配合
```
用户应用历史会话
→ switchChat: currentGroup = 该会话组,并按 group.selected_tables 恢复选表
用户新增一张表
→ toggleTable: selectedTables 变更
→ persistGroupTables():
绑定目标取 currentGroup.value.id ← currentGroup 告诉它"绑给谁"
currentGroup.value.selected_tables = tables ← 同时把最新绑定写回 currentGroup
PUT /groups/{currentGroup.value.id}/tables ← 服务端全量覆盖
切走再切回
→ switchChat 从 group.selected_tables 恢复 → 新增的表还在 ✅
```
- `currentGroup` 回答"**绑给哪个会话**"(提供 id)
- `persistGroupTables` 回答"**怎么绑**"(内存双写 + 服务端覆盖)
- 若 `persistGroupTables` 不回写 `currentGroup.selected_tables`,切回时恢复的就是旧值——这正是曾修复的"新增绑定丢失"bug
|
|
### 理解要点小结
1. **currentGroup = 当前会话的指针**:值为一个组对象(或 null),页面所有会话相关操作的"锚点"
2. **更新时机集中**:默认选中第一个组(fetchChatGroups)+ 任何切换(switchChat,含应用历史会话)
3. **历史会话切换后即当前会话**:`useChatFromHistory → switchToChatFromDialog → switchChat`,`currentGroup` 随之指向它
4. **响应式联动**:`currentGroup` 一变,"当前"徽标等界面元素自动刷新,无需手工同步
5. **防御性访问**:统一用 `currentGroup.value?.id` / `if (!currentGroup.value)`,初始 null 不会引发报错
6. **单一数据源思想**:多处读、少数写,是前端状态管理的基本模式(小页面用 ref,大项目用 Pinia 等状态库,思想相同)
|