Complete admin chat records

This commit is contained in:
2026-07-07 12:17:23 +08:00
parent 08dd371480
commit 08c301c136
9 changed files with 608 additions and 61 deletions

View File

@@ -3,7 +3,16 @@ import { ElMessage } from "element-plus";
import { onMounted, reactive, ref } from "vue";
import { api, clearToken, getToken, saveToken } from "./services/api";
import type { AdminProfile, AdminUser, DashboardStats, KnowledgeItem, ModelItem } from "./types/api";
import type {
AdminProfile,
AdminUser,
AiLogRecord,
ChatDetail,
ChatRecord,
DashboardStats,
KnowledgeItem,
ModelItem,
} from "./types/api";
const admin = ref<AdminProfile | null>(null);
const activeMenu = ref("dashboard");
@@ -16,10 +25,13 @@ const userKeyword = ref("");
const knowledge = ref<KnowledgeItem[]>([]);
const models = ref<ModelItem[]>([]);
const configs = ref<Record<string, unknown>[]>([]);
const chats = ref<Record<string, unknown>[]>([]);
const aiLogs = ref<Record<string, unknown>[]>([]);
const chats = ref<ChatRecord[]>([]);
const aiLogs = ref<AiLogRecord[]>([]);
const operationLogs = ref<Record<string, unknown>[]>([]);
const promptContent = ref("");
const chatDetail = ref<ChatDetail | null>(null);
const chatDetailOpen = ref(false);
const recordTab = ref("chats");
const knowledgeForm = reactive({
name: "",
@@ -45,6 +57,13 @@ const configForm = reactive({
description: "默认每日聊天次数",
});
const chatFilters = reactive({
keyword: "",
userId: undefined as number | undefined,
status: "",
dateRange: [] as string[],
});
onMounted(async () => {
if (!getToken()) return;
try {
@@ -89,7 +108,7 @@ async function loadCurrentMenu() {
if (activeMenu.value === "models") models.value = await api.models();
if (activeMenu.value === "configs") configs.value = await api.configs();
if (activeMenu.value === "records") {
chats.value = await api.chats();
chats.value = await api.chats(buildChatQuery());
aiLogs.value = await api.aiLogs();
operationLogs.value = await api.operationLogs();
}
@@ -149,6 +168,34 @@ async function saveConfig() {
ElMessage.success("配置已保存");
await loadCurrentMenu();
}
async function searchChats() {
chats.value = await api.chats(buildChatQuery());
}
async function resetChatFilters() {
Object.assign(chatFilters, { keyword: "", userId: undefined, status: "", dateRange: [] });
await searchChats();
}
async function openChatDetail(row: ChatRecord) {
chatDetail.value = await api.chatDetail(row.id);
chatDetailOpen.value = true;
}
async function exportChats() {
await api.exportChats(buildChatQuery());
}
function buildChatQuery() {
return {
keyword: chatFilters.keyword,
userId: chatFilters.userId,
status: chatFilters.status,
dateFrom: chatFilters.dateRange?.[0],
dateTo: chatFilters.dateRange?.[1],
};
}
</script>
<template>
@@ -288,13 +335,115 @@ async function saveConfig() {
<template v-if="activeMenu === 'records'">
<div class="page-head"><h2>记录审计</h2><p>聊天AI 请求和后台操作记录</p></div>
<el-tabs>
<el-tab-pane label="聊天会话"><el-table :data="chats" stripe><el-table-column prop="title" label="标题" /><el-table-column prop="messageCount" label="消息数" width="100" /><el-table-column prop="updatedAt" label="更新时间" /></el-table></el-tab-pane>
<el-tab-pane label="AI 请求"><el-table :data="aiLogs" stripe><el-table-column prop="modelName" label="模型" /><el-table-column prop="retrieveCount" label="命中" width="90" /><el-table-column prop="status" label="状态" width="100" /><el-table-column prop="errorMessage" label="错误" /></el-table></el-tab-pane>
<el-tab-pane label="操作日志"><el-table :data="operationLogs" stripe><el-table-column prop="module" label="模块" /><el-table-column prop="action" label="动作" /><el-table-column prop="result" label="结果" /><el-table-column prop="createdAt" label="时间" /></el-table></el-tab-pane>
<el-tabs v-model="recordTab">
<el-tab-pane label="聊天记录" name="chats">
<div class="record-filter">
<el-input v-model="chatFilters.keyword" placeholder="搜索标题、手机号、姓名、聊天内容" clearable />
<el-input-number v-model="chatFilters.userId" :min="1" placeholder="用户ID" />
<el-select v-model="chatFilters.status" placeholder="消息状态" clearable>
<el-option label="全部状态" value="" />
<el-option label="已完成" value="FINISHED" />
<el-option label="生成中" value="GENERATING" />
<el-option label="已停止" value="STOPPED" />
<el-option label="失败" value="FAILED" />
</el-select>
<el-date-picker
v-model="chatFilters.dateRange"
type="datetimerange"
start-placeholder="开始时间"
end-placeholder="结束时间"
value-format="YYYY-MM-DDTHH:mm:ss"
/>
<el-button type="primary" @click="searchChats">搜索</el-button>
<el-button @click="resetChatFilters">重置</el-button>
<el-button @click="exportChats">导出</el-button>
</div>
<el-table :data="chats" stripe>
<el-table-column prop="id" label="会话ID" width="90" />
<el-table-column prop="userPhone" label="手机号" width="140" />
<el-table-column prop="userName" label="用户" width="120" />
<el-table-column prop="title" label="会话标题" min-width="220" show-overflow-tooltip />
<el-table-column prop="messageCount" label="消息数" width="90" />
<el-table-column prop="lastMessageAt" label="最后消息" width="180" />
<el-table-column label="操作" width="100" fixed="right">
<template #default="{ row }">
<el-button size="small" @click="openChatDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="AI 请求" name="aiLogs">
<el-table :data="aiLogs" stripe>
<el-table-column prop="sessionId" label="会话ID" width="90" />
<el-table-column prop="modelName" label="模型" width="160" />
<el-table-column prop="knowledgeIds" label="知识库" width="120" />
<el-table-column prop="retrieveCount" label="命中" width="80" />
<el-table-column prop="totalToken" label="Token" width="90" />
<el-table-column prop="costMs" label="耗时(ms)" width="100" />
<el-table-column prop="status" label="状态" width="100" />
<el-table-column prop="errorMessage" label="错误" min-width="220" show-overflow-tooltip />
</el-table>
</el-tab-pane>
<el-tab-pane label="操作日志" name="operationLogs">
<el-table :data="operationLogs" stripe>
<el-table-column prop="module" label="模块" />
<el-table-column prop="action" label="动作" />
<el-table-column prop="result" label="结果" />
<el-table-column prop="createdAt" label="时间" />
</el-table>
</el-tab-pane>
</el-tabs>
</template>
</section>
</section>
<el-drawer v-model="chatDetailOpen" size="760px" title="聊天详情">
<template v-if="chatDetail">
<section class="chat-summary">
<div><span>会话ID</span><strong>{{ chatDetail.session.id }}</strong></div>
<div><span>用户</span><strong>{{ chatDetail.session.userName || '-' }}</strong></div>
<div><span>手机号</span><strong>{{ chatDetail.session.userPhone || '-' }}</strong></div>
<div><span>消息数</span><strong>{{ chatDetail.session.messageCount }}</strong></div>
</section>
<h3 class="detail-title">完整对话</h3>
<section class="conversation-list">
<article
v-for="message in chatDetail.messages"
:key="message.id"
class="conversation-message"
:class="message.role"
>
<div class="message-meta">
<strong>{{ message.role === 'user' ? '用户' : 'AI 助手' }}</strong>
<span>{{ message.createdAt }}</span>
</div>
<pre>{{ message.content }}</pre>
<div class="message-extra">
<span>状态{{ message.messageStatus }}</span>
<span v-if="message.tokenInput">输入 Token{{ message.tokenInput }}</span>
<span v-if="message.tokenOutput">输出 Token{{ message.tokenOutput }}</span>
<span v-if="message.responseTimeMs">耗时{{ message.responseTimeMs }}ms</span>
</div>
</article>
</section>
<h3 class="detail-title">关联 AI 请求</h3>
<el-collapse>
<el-collapse-item v-for="log in chatDetail.aiLogs" :key="log.id" :title="`请求 #${log.id} / ${log.status}`">
<div class="ai-log-grid">
<span>模型{{ log.modelName || '-' }}</span>
<span>知识库{{ log.knowledgeIds || '-' }}</span>
<span>命中数{{ log.retrieveCount }}</span>
<span>Token{{ log.totalToken || '-' }}</span>
<span>耗时{{ log.costMs || '-' }}ms</span>
<span>时间{{ log.createdAt }}</span>
</div>
<pre class="prompt-preview">{{ log.prompt || '无 Prompt 记录' }}</pre>
<p v-if="log.errorMessage" class="error-text">{{ log.errorMessage }}</p>
</el-collapse-item>
</el-collapse>
</template>
</el-drawer>
</main>
</template>