- 管理端和用户中心统一切换到 chatTime 时间线模型,按真实 assistant/tool 时序渲染 - 收紧工具气泡与 JSON 展示样式,保留同气泡内的工具状态更新 - 回形针直接触发文件选择,附件列表上移到输入框上方并补充共享 helper 测试
636 lines
17 KiB
Vue
636 lines
17 KiB
Vue
<script setup lang="ts">
|
|
import type { ChatTimeTimelineItem } from '@easyflow/types';
|
|
|
|
import { computed, onMounted, ref } from 'vue';
|
|
|
|
import { useEasyFlowDrawer } from '@easyflow/common-ui';
|
|
import { ChatTimeHistoryMapper } from '@easyflow/utils';
|
|
|
|
import { Search } from '@element-plus/icons-vue';
|
|
import {
|
|
ElButton,
|
|
ElDatePicker,
|
|
ElEmpty,
|
|
ElInput,
|
|
ElPagination,
|
|
ElSelect,
|
|
ElTable,
|
|
ElTableColumn,
|
|
} from 'element-plus';
|
|
import { tryit } from 'radash';
|
|
|
|
import { api } from '#/api/request';
|
|
import ChatHistoryDetailDrawer from '#/components/chat-history/ChatHistoryDetailDrawer.vue';
|
|
import ListPageShell from '#/components/page/ListPageShell.vue';
|
|
|
|
const assistantList = ref<any[]>([]);
|
|
const sessions = ref<any[]>([]);
|
|
const loading = ref(false);
|
|
const query = ref({
|
|
assistantId: undefined as number | undefined,
|
|
userAccount: '',
|
|
timeRange: [] as string[],
|
|
pageNumber: 1,
|
|
pageSize: 20,
|
|
});
|
|
const quickRange = ref<'' | 'last7' | 'last30' | 'today'>('');
|
|
const pageState = ref({
|
|
total: 0,
|
|
});
|
|
const pageSizeOptions = [20, 50, 100];
|
|
const quickRangeOptions = [
|
|
{ label: '今天', value: 'today' as const },
|
|
{ label: '最近 7 天', value: 'last7' as const },
|
|
{ label: '最近 30 天', value: 'last30' as const },
|
|
];
|
|
|
|
const drawerLoading = ref(false);
|
|
const currentSession = ref<any>();
|
|
const messageList = ref<ChatTimeTimelineItem[]>([]);
|
|
const loadedMessageRecordCount = ref(0);
|
|
const messagePage = ref({
|
|
total: 0,
|
|
pageNumber: 1,
|
|
pageSize: 20,
|
|
});
|
|
|
|
const [Drawer, drawerApi] = useEasyFlowDrawer({
|
|
appendToMain: false,
|
|
class:
|
|
'!w-[820px] max-w-[calc(100vw-24px)] !border-0 !bg-[hsl(var(--glass-tint))/0.28] shadow-[0_24px_48px_-36px_hsl(var(--foreground)/0.12)] supports-[backdrop-filter]:!bg-[hsl(var(--glass-tint))/0.2]',
|
|
closable: false,
|
|
contentClass: 'p-0',
|
|
footer: false,
|
|
header: false,
|
|
modal: false,
|
|
placement: 'right',
|
|
});
|
|
|
|
const hasMoreMessages = computed(
|
|
() => loadedMessageRecordCount.value < messagePage.value.total,
|
|
);
|
|
|
|
const selectedSessionId = computed(() =>
|
|
String(currentSession.value?.id || ''),
|
|
);
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([fetchAssistants(), fetchSessions()]);
|
|
});
|
|
|
|
async function fetchAssistants() {
|
|
const [, res] = await tryit(api.get)('/api/v1/bot/list', {
|
|
params: { status: 1 },
|
|
});
|
|
if (res?.errorCode === 0) {
|
|
assistantList.value = (res.data || []).map((item: any) => ({
|
|
label: item.title,
|
|
value: item.id,
|
|
}));
|
|
}
|
|
}
|
|
|
|
async function fetchSessions() {
|
|
loading.value = true;
|
|
const [, res] = await tryit(api.get)('/api/v1/chatHistory/sessions', {
|
|
params: {
|
|
assistantId: query.value.assistantId,
|
|
userAccount: query.value.userAccount || undefined,
|
|
startTime: query.value.timeRange?.[0],
|
|
endTime: query.value.timeRange?.[1],
|
|
pageNumber: query.value.pageNumber,
|
|
pageSize: query.value.pageSize,
|
|
},
|
|
});
|
|
loading.value = false;
|
|
if (res?.errorCode === 0) {
|
|
sessions.value = res.data?.records || [];
|
|
pageState.value.total = res.data?.total || 0;
|
|
}
|
|
}
|
|
|
|
async function openSession(sessionId: number | string) {
|
|
drawerLoading.value = true;
|
|
currentSession.value =
|
|
sessions.value.find((item: any) => String(item.id) === String(sessionId)) ||
|
|
undefined;
|
|
messageList.value = [];
|
|
loadedMessageRecordCount.value = 0;
|
|
messagePage.value = {
|
|
total: 0,
|
|
pageNumber: 1,
|
|
pageSize: 20,
|
|
};
|
|
drawerApi.open();
|
|
|
|
const [, summaryRes] = await tryit(api.get)(
|
|
`/api/v1/chatHistory/sessions/${sessionId}`,
|
|
);
|
|
if (summaryRes?.errorCode !== 0) {
|
|
drawerLoading.value = false;
|
|
drawerApi.close();
|
|
return;
|
|
}
|
|
|
|
currentSession.value = summaryRes.data;
|
|
|
|
await loadMessages(true);
|
|
drawerLoading.value = false;
|
|
}
|
|
|
|
async function loadMessages(reset = false) {
|
|
if (!currentSession.value?.id) {
|
|
return;
|
|
}
|
|
const nextPageNumber = reset ? 1 : messagePage.value.pageNumber + 1;
|
|
const [, res] = await tryit(api.get)(
|
|
`/api/v1/chatHistory/sessions/${currentSession.value.id}/messages`,
|
|
{
|
|
params: {
|
|
pageNumber: nextPageNumber,
|
|
pageSize: messagePage.value.pageSize,
|
|
},
|
|
},
|
|
);
|
|
if (res?.errorCode !== 0) {
|
|
return;
|
|
}
|
|
const normalized = normalizeMessages(res.data?.records || []);
|
|
messageList.value = reset
|
|
? normalized
|
|
: [...normalized, ...messageList.value];
|
|
loadedMessageRecordCount.value = reset
|
|
? (res.data?.records || []).length
|
|
: loadedMessageRecordCount.value + (res.data?.records || []).length;
|
|
messagePage.value.total = res.data?.total || 0;
|
|
messagePage.value.pageNumber = nextPageNumber;
|
|
}
|
|
|
|
function normalizeMessages(records: any[]) {
|
|
return ChatTimeHistoryMapper.fromHistoryRecords([...records].reverse());
|
|
}
|
|
|
|
function changePage(pageNumber: number) {
|
|
query.value.pageNumber = pageNumber;
|
|
fetchSessions();
|
|
}
|
|
|
|
function changePageSize(pageSize: number) {
|
|
query.value.pageNumber = 1;
|
|
query.value.pageSize = pageSize;
|
|
fetchSessions();
|
|
}
|
|
|
|
function formatTime(value?: string) {
|
|
if (!value) {
|
|
return '-';
|
|
}
|
|
const time = new Date(value);
|
|
if (Number.isNaN(time.getTime())) {
|
|
return value;
|
|
}
|
|
const year = time.getFullYear();
|
|
const month = String(time.getMonth() + 1).padStart(2, '0');
|
|
const day = String(time.getDate()).padStart(2, '0');
|
|
const hour = String(time.getHours()).padStart(2, '0');
|
|
const minute = String(time.getMinutes()).padStart(2, '0');
|
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
}
|
|
|
|
function handleSearch() {
|
|
query.value.pageNumber = 1;
|
|
fetchSessions();
|
|
}
|
|
|
|
function applyQuickRange(range: 'last7' | 'last30' | 'today') {
|
|
quickRange.value = range;
|
|
query.value.timeRange = resolveQuickRange(range);
|
|
handleSearch();
|
|
}
|
|
|
|
function handleTimeRangeChange(value?: string[]) {
|
|
quickRange.value = '';
|
|
query.value.timeRange = normalizeTimeRange(value);
|
|
handleSearch();
|
|
}
|
|
|
|
function resolveQuickRange(range: 'last7' | 'last30' | 'today') {
|
|
const now = new Date();
|
|
const end = endOfDay(now);
|
|
let start = startOfDay(now);
|
|
|
|
if (range === 'last7') {
|
|
start = startOfDay(addDays(now, -6));
|
|
} else if (range === 'last30') {
|
|
start = startOfDay(addDays(now, -29));
|
|
}
|
|
|
|
return [formatDateTime(start), formatDateTime(end)];
|
|
}
|
|
|
|
function normalizeTimeRange(value?: string[]) {
|
|
if (!value?.length) {
|
|
return [];
|
|
}
|
|
const [startValue, endValue] = value;
|
|
if (!startValue || !endValue) {
|
|
return [];
|
|
}
|
|
return [
|
|
formatDateTime(startOfDay(new Date(startValue))),
|
|
formatDateTime(endOfDay(new Date(endValue))),
|
|
];
|
|
}
|
|
|
|
function startOfDay(date: Date) {
|
|
const value = new Date(date);
|
|
value.setHours(0, 0, 0, 0);
|
|
return value;
|
|
}
|
|
|
|
function endOfDay(date: Date) {
|
|
const value = new Date(date);
|
|
value.setHours(23, 59, 59, 0);
|
|
return value;
|
|
}
|
|
|
|
function addDays(date: Date, days: number) {
|
|
const value = new Date(date);
|
|
value.setDate(value.getDate() + days);
|
|
return value;
|
|
}
|
|
|
|
function formatDateTime(value: Date) {
|
|
const year = value.getFullYear();
|
|
const month = String(value.getMonth() + 1).padStart(2, '0');
|
|
const day = String(value.getDate()).padStart(2, '0');
|
|
const hour = String(value.getHours()).padStart(2, '0');
|
|
const minute = String(value.getMinutes()).padStart(2, '0');
|
|
const second = String(value.getSeconds()).padStart(2, '0');
|
|
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
|
}
|
|
|
|
function closeDetail() {
|
|
drawerApi.close();
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="chat-history-page flex h-full flex-col gap-6 p-6">
|
|
<ListPageShell class="flex-1" :content-padding="20">
|
|
<template #filters>
|
|
<div class="chat-history-page__filters">
|
|
<ElSelect
|
|
v-model="query.assistantId"
|
|
clearable
|
|
placeholder="筛选聊天助手"
|
|
:options="assistantList"
|
|
class="chat-history-page__filter-control is-select"
|
|
@change="handleSearch"
|
|
/>
|
|
<ElInput
|
|
v-model="query.userAccount"
|
|
class="chat-history-page__filter-control is-input"
|
|
placeholder="搜索聊天用户"
|
|
:prefix-icon="Search"
|
|
@keyup.enter="handleSearch"
|
|
/>
|
|
<div class="chat-history-page__quick-ranges">
|
|
<ElButton
|
|
v-for="item in quickRangeOptions"
|
|
:key="item.value"
|
|
:type="quickRange === item.value ? 'primary' : 'default'"
|
|
class="chat-history-page__quick-range-button"
|
|
@click="applyQuickRange(item.value)"
|
|
>
|
|
{{ item.label }}
|
|
</ElButton>
|
|
</div>
|
|
<ElDatePicker
|
|
v-model="query.timeRange"
|
|
class="chat-history-page__filter-control is-range"
|
|
type="daterange"
|
|
value-format="YYYY-MM-DD HH:mm:ss"
|
|
start-placeholder="开始时间"
|
|
end-placeholder="结束时间"
|
|
@change="handleTimeRangeChange"
|
|
/>
|
|
<ElButton type="primary" @click="handleSearch">查询</ElButton>
|
|
</div>
|
|
</template>
|
|
|
|
<div v-loading="loading" class="chat-history-page__content">
|
|
<div v-if="sessions.length > 0" class="chat-history-page__table-shell">
|
|
<ElTable
|
|
:data="sessions"
|
|
:current-row-key="selectedSessionId || undefined"
|
|
row-key="id"
|
|
class="chat-history-page__table"
|
|
height="100%"
|
|
highlight-current-row
|
|
empty-text=""
|
|
@row-click="(row) => openSession(row.id)"
|
|
>
|
|
<ElTableColumn label="会话信息" min-width="280">
|
|
<template #default="{ row }">
|
|
<div class="chat-history-page__session-cell">
|
|
<div class="chat-history-page__session-title">
|
|
{{ row.title || '未命名会话' }}
|
|
</div>
|
|
<div
|
|
v-if="row.lastMessagePreview"
|
|
class="chat-history-page__session-preview"
|
|
>
|
|
{{ row.lastMessagePreview }}
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</ElTableColumn>
|
|
|
|
<ElTableColumn label="聊天助手" min-width="160">
|
|
<template #default="{ row }">
|
|
<span class="chat-history-page__assistant-chip">
|
|
{{ row.assistantName || '聊天助手' }}
|
|
</span>
|
|
</template>
|
|
</ElTableColumn>
|
|
|
|
<ElTableColumn prop="userAccount" label="聊天用户" min-width="160">
|
|
<template #default="{ row }">
|
|
<span class="chat-history-page__user-cell">
|
|
{{ row.userAccount || '-' }}
|
|
</span>
|
|
</template>
|
|
</ElTableColumn>
|
|
|
|
<ElTableColumn label="消息数" width="110" align="center">
|
|
<template #default="{ row }">
|
|
<span class="chat-history-page__count-pill">
|
|
{{ row.messageCount || 0 }}
|
|
</span>
|
|
</template>
|
|
</ElTableColumn>
|
|
|
|
<ElTableColumn label="最近活跃" width="180">
|
|
<template #default="{ row }">
|
|
<span class="chat-history-page__time-cell">
|
|
{{ formatTime(row.lastMessageAt || row.accessAt) }}
|
|
</span>
|
|
</template>
|
|
</ElTableColumn>
|
|
|
|
<ElTableColumn width="108" align="right">
|
|
<template #default="{ row }">
|
|
<ElButton
|
|
link
|
|
type="primary"
|
|
class="chat-history-page__detail-action"
|
|
@click.stop="openSession(row.id)"
|
|
>
|
|
查看详情
|
|
</ElButton>
|
|
</template>
|
|
</ElTableColumn>
|
|
</ElTable>
|
|
</div>
|
|
|
|
<div v-else class="chat-history-page__empty">
|
|
<ElEmpty description="暂无聊天历史" />
|
|
</div>
|
|
|
|
<div v-if="pageState.total > 0" class="chat-history-page__pagination">
|
|
<ElPagination
|
|
background
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
:current-page="query.pageNumber"
|
|
:page-size="query.pageSize"
|
|
:page-sizes="pageSizeOptions"
|
|
:total="pageState.total"
|
|
@current-change="changePage"
|
|
@size-change="changePageSize"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</ListPageShell>
|
|
|
|
<Drawer>
|
|
<ChatHistoryDetailDrawer
|
|
:visible="true"
|
|
:loading="drawerLoading"
|
|
:session="currentSession"
|
|
:messages="messageList"
|
|
:has-more="hasMoreMessages"
|
|
:on-load-more="() => loadMessages(false)"
|
|
@close="closeDetail"
|
|
/>
|
|
</Drawer>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.chat-history-page__filters {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
align-items: center;
|
|
}
|
|
|
|
.chat-history-page__filter-control {
|
|
min-width: 0;
|
|
}
|
|
|
|
.chat-history-page__filter-control.is-select,
|
|
.chat-history-page__filter-control.is-input {
|
|
width: 220px;
|
|
}
|
|
|
|
.chat-history-page__filter-control.is-range {
|
|
width: 360px;
|
|
}
|
|
|
|
.chat-history-page__quick-ranges {
|
|
display: inline-flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px;
|
|
}
|
|
|
|
.chat-history-page__quick-range-button {
|
|
min-width: 88px;
|
|
}
|
|
|
|
.chat-history-page__content {
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
min-height: 0;
|
|
}
|
|
|
|
.chat-history-page__table-shell {
|
|
flex: 1;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background: linear-gradient(
|
|
180deg,
|
|
hsl(var(--glass-border) / 28%) 0%,
|
|
hsl(var(--glass-tint) / 40%) 14%,
|
|
hsl(var(--surface-panel) / 94%) 100%
|
|
);
|
|
border: 1px solid hsl(var(--glass-border) / 42%);
|
|
border-radius: 24px;
|
|
box-shadow:
|
|
inset 0 1px 0 hsl(var(--glass-border) / 54%),
|
|
0 24px 42px -36px hsl(var(--foreground) / 16%);
|
|
}
|
|
|
|
.chat-history-page__table {
|
|
height: 100%;
|
|
background: transparent;
|
|
}
|
|
|
|
.chat-history-page__session-cell {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.chat-history-page__session-title {
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: hsl(var(--text-strong));
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.chat-history-page__session-preview {
|
|
display: -webkit-box;
|
|
overflow: hidden;
|
|
-webkit-line-clamp: 1;
|
|
font-size: 12px;
|
|
line-height: 1.5;
|
|
color: hsl(var(--text-muted));
|
|
overflow-wrap: anywhere;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.chat-history-page__assistant-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
max-width: 100%;
|
|
min-height: 28px;
|
|
padding: 0 12px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: hsl(var(--nav-item-active-foreground));
|
|
white-space: nowrap;
|
|
background: hsl(var(--glass-tint) / 76%);
|
|
border: 1px solid hsl(var(--glass-border) / 48%);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.chat-history-page__user-cell,
|
|
.chat-history-page__time-cell {
|
|
font-size: 13px;
|
|
color: hsl(var(--text-secondary));
|
|
}
|
|
|
|
.chat-history-page__count-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-width: 42px;
|
|
padding: 4px 10px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: hsl(var(--text-strong));
|
|
background: hsl(var(--surface-contrast-soft) / 88%);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.chat-history-page__detail-action {
|
|
padding-right: 2px;
|
|
}
|
|
|
|
.chat-history-page__empty {
|
|
display: flex;
|
|
flex: 1;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 360px;
|
|
}
|
|
|
|
.chat-history-page__pagination {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
padding-top: 20px;
|
|
}
|
|
|
|
.chat-history-page__content :deep(.el-table),
|
|
.chat-history-page__content :deep(.el-table__inner-wrapper),
|
|
.chat-history-page__content :deep(.el-table__body-wrapper),
|
|
.chat-history-page__content :deep(.el-scrollbar),
|
|
.chat-history-page__content :deep(.el-scrollbar__wrap),
|
|
.chat-history-page__content :deep(.el-scrollbar__view) {
|
|
height: 100%;
|
|
}
|
|
|
|
.chat-history-page__content :deep(.el-table::before) {
|
|
display: none;
|
|
}
|
|
|
|
.chat-history-page__content :deep(.el-table tr) {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.chat-history-page__content :deep(.el-table th.el-table__cell) {
|
|
height: 48px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
color: hsl(var(--text-muted));
|
|
background: hsl(var(--surface-contrast-soft) / 54%);
|
|
border-bottom-color: hsl(var(--divider-faint) / 28%);
|
|
}
|
|
|
|
.chat-history-page__content :deep(.el-table td.el-table__cell) {
|
|
padding: 14px 0;
|
|
background: transparent;
|
|
border-bottom-color: hsl(var(--divider-faint) / 22%);
|
|
}
|
|
|
|
.chat-history-page__content
|
|
:deep(.el-table__body tr:hover > td.el-table__cell) {
|
|
background: hsl(var(--primary) / 4%);
|
|
}
|
|
|
|
.chat-history-page__content
|
|
:deep(.el-table__body tr.current-row > td.el-table__cell) {
|
|
background: hsl(var(--primary) / 8%);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.chat-history-page__filter-control.is-range {
|
|
width: min(100%, 360px);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.chat-history-page {
|
|
padding: 16px;
|
|
}
|
|
|
|
.chat-history-page__filters {
|
|
gap: 10px;
|
|
}
|
|
|
|
.chat-history-page__filter-control.is-select,
|
|
.chat-history-page__filter-control.is-input,
|
|
.chat-history-page__filter-control.is-range {
|
|
width: 100%;
|
|
}
|
|
}
|
|
</style>
|