224 lines
7.8 KiB
Markdown
224 lines
7.8 KiB
Markdown
# ChatHistory 会话列表重构迁移计划
|
||
|
||
## 参考来源
|
||
- **源文件**: `ClawX/src/components/layout/Sidebar.tsx:280-335`
|
||
- **目标文件**: `zn-ai/src/pages/home/ChatHistory.vue`
|
||
|
||
## 源实现思路分析
|
||
|
||
`Sidebar.tsx:280-335` 的历史会话列表核心设计:
|
||
|
||
1. **时间分桶(Time Buckets)**
|
||
- 将会话按最后活动时间划分为:`today`、`yesterday`、`withinWeek`、`withinTwoWeeks`、`withinMonth`、`older`。
|
||
- 通过 `getSessionBucket(activityMs, nowMs)` 函数计算每个会话所属的分组。
|
||
- 会话先按时间降序排序,再归入对应 bucket。
|
||
|
||
2. **分组标签渲染**
|
||
- 每个 bucket 顶部显示一个 11px 的灰色小标签(如 "Today" / "Yesterday" / "Older")。
|
||
- 空 bucket 直接跳过不渲染。
|
||
|
||
3. **会话项结构**
|
||
- 外层 `group relative flex items-center` 包裹。
|
||
- 左侧显示 **agent 名称标签**(圆角小 pill),右侧显示 **会话标题**(`truncate` 截断)。
|
||
- 当前选中的会话高亮:背景色 + 加粗文字。
|
||
- hover 时背景变深。
|
||
|
||
4. **删除交互(悬停显隐)**
|
||
- 删除按钮默认 `opacity-0`,hover 整个会话项时通过 `group-hover:opacity-100` 显现。
|
||
- 按钮绝对定位在右侧,避免挤压标题空间。
|
||
|
||
5. **点击行为**
|
||
- 点击整个会话行:切换会话 + 导航到聊天页。
|
||
- 删除按钮单独拦截 `stopPropagation`。
|
||
|
||
---
|
||
|
||
## 迁移目标
|
||
|
||
将上述时间分桶、分组标签、悬停删除、紧凑列表布局迁移到 `ChatHistory.vue`,替换当前简单平铺的 `groups` 列表,同时保留 zn-ai 项目现有风格与功能。
|
||
|
||
---
|
||
|
||
## 实现步骤
|
||
|
||
### 1. 引入时间分桶工具函数
|
||
|
||
在 `<script setup>` 中新增 `SessionBucketKey` 类型与 `getSessionBucket` 函数:
|
||
|
||
```ts
|
||
type SessionBucketKey =
|
||
| 'today'
|
||
| 'yesterday'
|
||
| 'withinWeek'
|
||
| 'withinTwoWeeks'
|
||
| 'withinMonth'
|
||
| 'older';
|
||
|
||
function getSessionBucket(activityMs: number, nowMs: number): SessionBucketKey {
|
||
if (!activityMs || activityMs <= 0) return 'older';
|
||
const now = new Date(nowMs);
|
||
const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
|
||
const startOfYesterday = startOfToday - 24 * 60 * 60 * 1000;
|
||
|
||
if (activityMs >= startOfToday) return 'today';
|
||
if (activityMs >= startOfYesterday) return 'yesterday';
|
||
|
||
const daysAgo = (startOfToday - activityMs) / (24 * 60 * 60 * 1000);
|
||
if (daysAgo <= 7) return 'withinWeek';
|
||
if (daysAgo <= 14) return 'withinTwoWeeks';
|
||
if (daysAgo <= 30) return 'withinMonth';
|
||
return 'older';
|
||
}
|
||
```
|
||
|
||
> **说明**:ChatHistory.vue 当前接口可能不返回 `lastActivity` 时间。若接口无该字段,可先以会话创建时间/更新时间或本地 `Date.now()` 作为兜底,后续再补充真实数据。
|
||
|
||
### 2. 扩展数据模型
|
||
|
||
`HistoryMessage` 接口增加可选时间字段,用于分桶:
|
||
|
||
```ts
|
||
interface HistoryMessage {
|
||
conversationId: string;
|
||
conversationTitle: string;
|
||
updatedAt?: number; // 毫秒时间戳,用于分桶
|
||
}
|
||
```
|
||
|
||
映射接口数据时注入时间戳(若后端返回字符串时间,先 `new Date().getTime()` 转换):
|
||
|
||
```ts
|
||
groups.value = list.sessions.map((item: any) => ({
|
||
conversationId: item.session_id,
|
||
conversationTitle: item.title,
|
||
updatedAt: item.updated_at ? new Date(item.updated_at).getTime() : Date.now(),
|
||
}));
|
||
```
|
||
|
||
### 3. 计算时间分桶(响应式)
|
||
|
||
使用 `computed` 将会话列表转换为 bucket 数组:
|
||
|
||
```ts
|
||
import { computed } from 'vue';
|
||
|
||
const nowMs = ref(Date.now());
|
||
|
||
const sessionBuckets = computed(() => {
|
||
const buckets: Array<{ key: SessionBucketKey; label: string; sessions: HistoryMessage[] }> = [
|
||
{ key: 'today', label: '今天', sessions: [] },
|
||
{ key: 'yesterday', label: '昨天', sessions: [] },
|
||
{ key: 'withinWeek', label: '近7天', sessions: [] },
|
||
{ key: 'withinTwoWeeks', label: '近14天', sessions: [] },
|
||
{ key: 'withinMonth', label: '近30天', sessions: [] },
|
||
{ key: 'older', label: '更早', sessions: [] },
|
||
];
|
||
const map = Object.fromEntries(buckets.map((b) => [b.key, b])) as Record<SessionBucketKey, typeof buckets[number]>;
|
||
|
||
for (const session of [...groups.value].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0))) {
|
||
const bucketKey = getSessionBucket(session.updatedAt || 0, nowMs.value);
|
||
map[bucketKey].sessions.push(session);
|
||
}
|
||
return buckets;
|
||
});
|
||
```
|
||
|
||
### 4. 模板结构改造
|
||
|
||
替换当前平铺的 `<ul>` 为按 bucket 分组的结构。外层保持 `v-if="!sidebarCollapsed"` 控制折叠显隐:
|
||
|
||
```html
|
||
<div v-if="!sidebarCollapsed" class="overflow-y-auto p-2 flex-1">
|
||
<div v-for="bucket in sessionBuckets" :key="bucket.key" class="mb-2">
|
||
<div
|
||
v-if="bucket.sessions.length > 0"
|
||
class="px-2.5 pb-1 text-[11px] font-medium text-gray-400 tracking-tight"
|
||
>
|
||
{{ bucket.label }}
|
||
</div>
|
||
<div
|
||
v-for="item in bucket.sessions"
|
||
:key="item.conversationId"
|
||
class="group relative flex items-center"
|
||
>
|
||
<div
|
||
@click="selectedHistoryMessage(item.conversationId)"
|
||
:class="[
|
||
'w-full text-left rounded-lg px-2.5 py-1.5 text-[13px] transition-colors cursor-pointer pr-7',
|
||
item.conversationId === selectedConversationId
|
||
? 'bg-white shadow-sm border border-[#E5E8EE] text-gray-800 font-medium'
|
||
: 'text-gray-600 hover:bg-gray-200'
|
||
]"
|
||
>
|
||
<div class="flex min-w-0 items-center gap-2">
|
||
<!-- 如需展示 agent/来源,可在此加 pill 标签 -->
|
||
<span class="truncate">{{ item.conversationTitle }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 悬停删除按钮 -->
|
||
<button
|
||
aria-label="删除会话"
|
||
@click.stop="deleteHistoryMessage(item.conversationId)"
|
||
class="absolute right-1 flex items-center justify-center rounded p-0.5 transition-opacity opacity-0 group-hover:opacity-100 text-gray-400 hover:text-red-500 hover:bg-red-50"
|
||
>
|
||
<RiDeleteBinLine class="h-3.5 w-3.5" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
> **说明**:
|
||
> - 移除了旧版左侧的蓝色圆点,改为更紧凑的行式布局。
|
||
> - 删除按钮改为 hover 显隐,并使用 `@click.stop` 避免触发选择。
|
||
> - 保留原 `selectedHistoryMessage` 选择逻辑。
|
||
> - 重命名功能可继续通过双击或扩展菜单实现;若需保留,可额外加一个 hover 出现的 "更多" 下拉按钮(本计划暂不展开)。
|
||
|
||
### 5. 图标引入
|
||
|
||
在 `<script setup>` 中引入删除图标:
|
||
|
||
```ts
|
||
import { RiDeleteBinLine } from '@remixicon/vue';
|
||
```
|
||
|
||
### 6. 样式与行为保持一致
|
||
|
||
- **选中态**:继续使用 `selectedConversationId` 判断,样式调整为带阴影和边框的白色背景卡片(与源实现高亮思路一致)。
|
||
- **空列表**:若 `groups` 为空,整个列表区域自然无内容,可后续补充 "暂无历史会话" 占位。
|
||
- **定时刷新时间基准**:如需让 "今天/昨天" 标签随时间自动更新,可补充 `setInterval` 每分钟刷新 `nowMs`:
|
||
|
||
```ts
|
||
let timer: number | undefined;
|
||
onMounted(() => {
|
||
getHistoryConversationList();
|
||
timer = window.setInterval(() => {
|
||
nowMs.value = Date.now();
|
||
}, 60 * 1000);
|
||
});
|
||
onBeforeUnmount(() => {
|
||
if (timer) clearInterval(timer);
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 涉及文件
|
||
|
||
| 文件 | 修改内容 |
|
||
|---|---|
|
||
| `zn-ai/src/pages/home/ChatHistory.vue` | 新增时间分桶逻辑、改造历史会话列表模板、引入悬停删除按钮、扩展 `HistoryMessage` 类型 |
|
||
|
||
---
|
||
|
||
## 验收标准
|
||
|
||
- [ ] 历史会话按时间自动分组成:今天、昨天、近7天、近14天、近30天、更早。
|
||
- [ ] 每个分组顶部显示对应中文标签。
|
||
- [ ] 会话项采用紧凑行式布局,标题超长截断。
|
||
- [ ] 鼠标悬停在某会话项上时,右侧出现删除按钮;移开后隐藏。
|
||
- [ ] 点击会话项仍正常触发 `select-chat` 事件。
|
||
- [ ] 删除、重命名(如保留)等原有功能不受影响。
|
||
- [ ] 侧边栏折叠时,历史会话列表跟随隐藏。
|