Files
zn-ai/src/pages/home/ChatHistory.vue
2026-04-14 07:36:40 +08:00

244 lines
7.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<aside :class="['h-full box-border flex flex-col transition-all duration-300', sidebarCollapsed ? 'w-16' : 'w-50']">
<div class="flex items-center justify-center m-2">
<img v-if="!sidebarCollapsed" class="w-10 h-10 rounded-md" src="@assets/images/login/white_logo.png" />
<div v-if="!sidebarCollapsed" class="font-bold text-gray-80">YINIAN</div>
<RiSidebarFoldLine
v-if="!sidebarCollapsed"
class="ml-auto cursor-pointer hover:text-[#2B7FFF]"
@click="sidebarCollapsed = true"
/>
<RiSideBarLine
v-else
class="cursor-pointer hover:text-[#2B7FFF]"
@click="sidebarCollapsed = false"
/>
</div>
<div
class="flex items-center justify-center m-2 bg-white rounded-lg p-2.5 border-[#E5E8EE] shadow-sm text-center cursor-pointer hover:bg-[#F5F7FA] hover:text-[#2B7FFF] hover:border-[#2B7FFF]"
@click="addNewChat">
<RiAddLine />
<span v-if="!sidebarCollapsed" class="whitespace-nowrap">新对话</span>
</div>
<div v-if="!sidebarCollapsed" class="overflow-y-auto p-2">
<template v-for="bucket in sessionBuckets">
<div v-if="bucket.sessions.length > 0" :key="bucket.key" class="mb-2 last:mb-0">
<div class="px-2 pb-1 text-[11px] font-medium text-gray-400 tracking-tight">
{{ bucket.label }}
</div>
<ul class="list-none">
<li v-for="item in bucket.sessions" :key="item.conversationId" @click="selectedHistoryMessage(item.conversationId)"
:class="[
'flex items-center gap-2 p-2 text-gray-600 rounded-lg cursor-pointer transition-colors',
item.conversationId === selectedConversationId ? 'bg-white shadow-sm border-[#E5E8EE] py-1.5 relative z-10' : 'hover:bg-gray-200'
]">
<span class="w-2 h-2 rounded-full bg-[#BEDBFF] flex-none"></span>
<div class="flex-1 min-w-0">
<div class="truncate text-sm">{{ item.conversationTitle }}</div>
</div>
<el-dropdown v-if="item.conversationId === selectedConversationId" placement="bottom-end">
<el-icon class="el-icon--right">
...
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="renameHistoryMessage(item.conversationId)">重命名</el-dropdown-item>
<el-dropdown-item @click="deleteHistoryMessage(item.conversationId)">删除</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</li>
</ul>
</div>
</template>
</div>
</aside>
<!-- 重命名对话框 -->
<el-dialog v-model="renameDialogFormVisible" title="重命名对话" width="500">
<el-form :model="newMessageName">
<el-form-item label="对话名称" :label-width="formLabelWidth">
<el-input v-model="newMessageName" autocomplete="off" />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="renameDialogFormVisible = false">取消</el-button>
<el-button type="primary" @click="submitNameChange">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 删除确认对话框 -->
<el-dialog v-model="deleteDialogVisible" title="温馨提示" width="500">
<span>您确定删除该会话吗删除后将无法恢复</span>
<template #footer>
<div class="dialog-footer">
<el-button @click="deleteDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitDelete">确定</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount, computed } from 'vue'
import { RiSideBarLine, RiSidebarFoldLine, RiArrowDownSLine, RiAddLine } from '@remixicon/vue'
import { getSessionList, deleteSession, updateSession } from '../../api/SessionsApi';
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';
}
const sidebarCollapsed = ref(false)
const deleteDialogVisible = ref(false)
const renameDialogFormVisible = ref(false)
const newMessageName = ref('')
const formLabelWidth = '100px'
const nowMs = ref(Date.now())
let timer: number | undefined
interface HistoryMessage {
conversationId: string;
conversationTitle: string;
updatedAt?: number;
}
/// 记录选择的历史消息ID
const selectedConversationId = ref<string>('')
/// 历史消息分组数据
const groups = ref<Array<HistoryMessage>>([])
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 = {} as Record<SessionBucketKey, typeof buckets[number]>;
for (const b of buckets) {
map[b.key] = b;
}
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;
});
/// 定义事件
const emit = defineEmits(['new-chat', 'select-chat'])
/// 添加新对话
const addNewChat = () => {
console.log('add new chat')
updateNewChat()
}
const updateNewChat = () => {
// 触发新对话事件
emit('new-chat')
// 清空选择的历史消息ID
selectedConversationId.value = ''
// 获取最新的历史会话列表
getHistoryConversationList()
}
/// 选择历史消息
const selectedHistoryMessage = (conversationId: string) => {
selectedConversationId.value = conversationId
emit('select-chat', conversationId)
}
/// 重命名历史消息
const renameHistoryMessage = (conversationId: string) => {
console.log('rename message', conversationId)
renameDialogFormVisible.value = true
}
/// 删除历史消息
const deleteHistoryMessage = (conversationId: string) => {
console.log('delete message', conversationId)
deleteDialogVisible.value = true
}
/// 提交重命名
const submitNameChange = async () => {
console.log('submit name change', newMessageName.value)
renameDialogFormVisible.value = false
const res = await updateSession({
session_id: selectedConversationId.value,
title: newMessageName.value
})
if (res && res.success) {
updateNewChat()
}
}
/// 提交删除
const submitDelete = async () => {
console.log('submit delete')
deleteDialogVisible.value = false
const res = await deleteSession({
session_id: selectedConversationId.value
})
if (res && res.success) {
updateNewChat()
}
}
/// 页面加载时获取历史会话列表
onMounted(() => {
getHistoryConversationList()
timer = window.setInterval(() => {
nowMs.value = Date.now()
}, 60 * 1000)
})
onBeforeUnmount(() => {
if (timer) clearInterval(timer)
})
/// 获取历史会话列表
const getHistoryConversationList = async () => {
const list = await getSessionList({ limit: 50, offset: 0 })
if (!list || !list.sessions) return;
// 使用整体赋值替换 push避免重复累加
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(),
}))
}
</script>