Commit 136d1a8c authored by 水玉婷's avatar 水玉婷
Browse files

feat:添加历史记录页面

parent c865fc04
...@@ -8,6 +8,12 @@ const routes = [ ...@@ -8,6 +8,12 @@ const routes = [
component: () => import('../views/Home.vue'), component: () => import('../views/Home.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{
path: '/history',
name: 'History',
component: () => import('../views/History.vue'),
meta: { requiresAuth: true }
},
{ {
path: '/login', path: '/login',
name: 'Login', name: 'Login',
......
<template>
<div class="history-container">
<!-- 头部栏 -->
<div class="history-header">
<div class="header-left">
<button class="menu-button" @click="toggleHistoryPanel">
<menu-outlined />
</button>
<h2 class="header-title">{{ appName || '国械小智'}}</h2>
</div>
<div class="header-right">
<button class="new-chat-button" @click="createNewChat">
<plus-outlined />
</button>
</div>
</div>
<!-- 历史记录侧边栏 -->
<div class="history-sidebar" :class="{ 'sidebar-open': isHistoryPanelOpen }">
<div class="sidebar-header">
<h3>历史记录</h3>
<button class="close-button" @click="toggleHistoryPanel">
<close-outlined />
</button>
</div>
<div class="search-box">
<div class="search-input">
<search-outlined class="search-icon" />
<input
v-model="searchKeyword"
type="text"
placeholder="搜索历史记录..."
@input="filterHistory"
/>
</div>
</div>
<div class="history-list">
<div
v-for="session in recentSessions"
:key="session.id"
:class="['history-item', { active: session.id === currentSessionId }]"
@click="selectSession(session)"
>
<div class="session-info">
<div class="session-title">{{ session.title }}</div>
</div>
<button class="delete-button" @click.stop="deleteSession(session)">
<delete-outlined />
</button>
</div>
<div v-if="recentSessions.length === 0" class="empty-state">
<file-search-outlined class="empty-icon" />
<p>暂无历史记录</p>
</div>
</div>
<div class="history-list-header" v-if="totalCount > 0">
<span class="total-count">{{ totalCount }} 条记录</span>
<span class="display-count">显示最近 {{ displayLimit }}</span>
</div>
</div>
<!-- 遮罩层 -->
<div
class="sidebar-overlay"
:class="{ 'overlay-visible': isHistoryPanelOpen }"
@click="toggleHistoryPanel"
></div>
<!-- 主内容区域 -->
<div class="main-content">
<AiChat
:params="chatParams"
:dialogSessionId="currentSessionId"
:detailData="currentSessionDetail"
:apiBaseUrl="apiBaseUrl"
:token="userToken"
:appCode="appCode"
customClass="chat-history"
:key="currentSessionId+new Date().getTime()"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import AiChat from './components/AiChat.vue';
import axios from '../utils/axios';
import { message } from 'ant-design-vue';
import {
MenuOutlined,
PlusOutlined,
CloseOutlined,
SearchOutlined,
DeleteOutlined,
FileSearchOutlined
} from '@ant-design/icons-vue';
// 响应式数据
const isHistoryPanelOpen = ref(false);
const searchKeyword = ref('');
const currentSessionId = ref('');
const currentSessionDetail = ref({ title: '' });
// API配置
const getApiBaseUrl = () => {
const basePath = import.meta.env.VITE_API_BASE_PATH;
if (basePath === '/') {
return '';
}
return basePath;
};
const apiBaseUrl = getApiBaseUrl();
const userInfo = localStorage.getItem('wechat_user');
const { extMap = {} } = JSON.parse(userInfo || '{}');
const userToken = extMap.sessionId;
const appCode = import.meta.env.VITE_APP_CODE || 'ped.qywx';
const chatParams = {
appId: '83b2664019a945d0a438abe6339758d8',
stage: 'wechat-demo',
};
const totalCount = ref(0);
const appName = ref('');
interface Session {
id: string;
title: string;
version: number;
}
const historyList = ref<Session[]>([]);
// 获取对话记录
const getChatRecordList = async () => {
let res = await axios.post(`${apiBaseUrl}/aiService/ask/history`, {
pageNum: 1,
pageSize: 20,
queryObject: {
appId: chatParams.appId,
offsetDay: 365,
},
});
if (res.data.code === 0) {
let { data = [], total = 0, totalInfo = {} } = res.data.data || [];
historyList.value = (data || []).map((item) => ({
...item,
isEdit: false,
}));
totalCount.value = total;
appName.value = totalInfo?.app_name || '';
}
};
// 显示限制
const displayLimit = 20;
// 计算属性:过滤后的会话列表
const filteredSessions = computed(() => {
if (!searchKeyword.value) {
return historyList.value;
}
return historyList.value.filter((session: any) =>
session.title.toLowerCase().includes(searchKeyword.value.toLowerCase())
);
});
// 计算属性:最近显示的会话列表
const recentSessions = computed(() => {
const sessions = filteredSessions.value;
return sessions.slice(0, displayLimit);
});
// 方法
const toggleHistoryPanel = () => {
isHistoryPanelOpen.value = !isHistoryPanelOpen.value;
};
const filterHistory = () => {
// 搜索功能已通过计算属性实现
};
const selectSession = (session: any) => {
currentSessionId.value = session.id;
currentSessionDetail.value = { title: session.title };
isHistoryPanelOpen.value = false;
};
const createNewChat = () => {
// 生成新的会话ID
currentSessionId.value = ''
currentSessionDetail.value = { title: '新对话' };
isHistoryPanelOpen.value = false;
};
const deleteSession = async (record: Session) => {
const res = await axios.post(`${apiBaseUrl}/aiService/ask/delete/history`, {
id: record.id,
version: record.version,
});
if (res.data.code === 0) {
message.success('删除成功');
// 检查是否删除的是当前活跃的会话
if (currentSessionId.value === record.id) {
// 如果删除的是当前会话,切换到新会话或清空状态
if (historyList.value.length > 1) {
// 如果有其他会话,切换到第一个会话
const remainingSessions = historyList.value.filter(session => session.id !== record.id);
if (remainingSessions.length > 0) {
selectSession(remainingSessions[0]);
} else {
// 如果没有其他会话,创建新会话
createNewChat();
}
} else {
// 如果没有其他会话,创建新会话
createNewChat();
}
}
getChatRecordList(); // 删除后重新获取对话记录
}
};
// 生命周期
onMounted(() => {
getChatRecordList();
});
</script>
<style lang="less" scoped>
@import './components/style.less';
.chat-history {
height: calc(100vh - 52px);
}
// 历史记录容器
.history-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f5f5f5;
position: relative;
overflow: hidden;
}
// 头部样式
.history-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
background: #ffffff;
border-bottom: 1px solid #e8e8e8;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
z-index: 100;
.header-left {
display: flex;
align-items: center;
gap: 12px;
.menu-button {
background: none;
border: none;
padding: 8px;
border-radius: 6px;
cursor: pointer;
color: #666;
transition: all 0.2s ease;
&:hover {
background: #f0f0f0;
color: #1890ff;
}
}
.header-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin: 0;
}
}
.header-right {
.new-chat-button {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: #f0f0f0;
color: #666;
border: none;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #e0e0e0;
color: #1890ff;
transform: scale(1.05);
}
}
}
}
// 侧边栏样式
.history-sidebar {
position: fixed;
top: 0;
left: -300px;
width: 300px;
height: 100vh;
background: #ffffff;
box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
transition: left 0.3s ease;
z-index: 1000;
display: flex;
flex-direction: column;
&.sidebar-open {
left: 0;
}
.sidebar-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
h3 {
font-size: 16px;
font-weight: 600;
color: #333;
margin: 0;
}
.close-button {
background: none;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
color: #666;
&:hover {
background: #f0f0f0;
color: #1890ff;
}
}
}
.search-box {
padding: 16px 20px;
border-bottom: 1px solid #e8e8e8;
.search-input {
position: relative;
.search-icon {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #999;
}
input {
width: 100%;
padding: 8px 12px 8px 36px;
border: 1px solid #d9d9d9;
border-radius: 6px;
font-size: 14px;
&:focus {
outline: none;
border-color: #1890ff;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
&::placeholder {
color: #999;
}
}
}
}
.history-list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 20px;
border-bottom: 1px solid #f0f0f0;
background: #fafafa;
font-size: 12px;
.total-count {
color: #666;
font-weight: 500;
}
.display-count {
color: #999;
}
}
.history-list {
flex: 1;
overflow-y: auto;
padding: 8px 0;
.history-item {
display: flex;
align-items: center;
padding: 12px 20px;
cursor: pointer;
transition: background-color 0.2s ease;
&:hover {
background: #f5f5f5;
}
&.active {
background: #e6f7ff;
border-right: 3px solid #1890ff;
}
.session-info {
flex: 1;
min-width: 0;
.session-title {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.session-time {
font-size: 12px;
color: #999;
margin-bottom: 2px;
}
.session-preview {
font-size: 12px;
color: #666;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.delete-button {
background: none;
border: none;
padding: 4px;
border-radius: 4px;
cursor: pointer;
color: #ccc;
opacity: 0;
transition: all 0.2s ease;
&:hover {
color: #ff4d4f;
background: #fff2f0;
}
}
&:hover .delete-button {
opacity: 1;
}
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #999;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: #d9d9d9;
}
p {
font-size: 14px;
margin: 0;
}
}
}
}
// 遮罩层
.sidebar-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
&.overlay-visible {
opacity: 1;
visibility: visible;
}
}
// 主内容区域
.main-content {
flex: 1;
display: flex;
flex-direction: column;
}
// 响应式设计
@media (max-width: 768px) {
.history-sidebar {
width: 280px;
&.sidebar-open {
left: 0;
}
}
.history-header {
padding: 10px 16px;
.header-title {
font-size: 16px;
}
.new-chat-button {
padding: 6px 12px;
font-size: 13px;
}
}
}
</style>
\ No newline at end of file
...@@ -53,12 +53,16 @@ ...@@ -53,12 +53,16 @@
<div v-else v-html="item.content" class="message-inner-box"></div> <div v-else v-html="item.content" class="message-inner-box"></div>
<!-- 思考过程框 --> <!-- 思考过程框 -->
<div v-if="item.hasThinkBox" class="think-box-wrapper"> <div v-if="item.hasThinkBox" class="think-box-wrapper">
<div class="think-box-toggle" @click="toggleThinkBox(index, i)">{{ <div class="think-box-toggle" @click="toggleThinkBox(index, i)">
item.thinkBoxExpanded ? '▲ ' : '▼ ' <component
}}<span v-if="item.thinkingTimeText" class="thinking-text">{{ item.thinkingTimeText }}</span></div> :is="item.thinkBoxExpanded ? UpOutlined : DownOutlined"
class="toggle-icon"
/>
<span v-if="item.thinkingTimeText" class="thinking-text">{{ item.thinkingTimeText }}</span>
</div>
<div v-if="item.thinkBoxExpanded" class="think-box-content" <div v-if="item.thinkBoxExpanded" class="think-box-content"
v-html="templateService.generateThinkingTemplate(item.thinkContent || '')"></div> v-html="templateService.generateThinkingTemplate(item.thinkContent || '')"></div>
</div> </div>
</template> </template>
</div> </div>
<!-- 操作按钮 --> <!-- 操作按钮 -->
...@@ -99,7 +103,7 @@ import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue'; ...@@ -99,7 +103,7 @@ import { ref, onMounted, onBeforeUnmount, nextTick, watch } from 'vue';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { tableTemplate } from './utils/tableTemplate'; import { tableTemplate } from './utils/tableTemplate';
import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './utils/markdownTemplate'; import { markdownTemplate, isLastBlockMarkdown, getLastMarkdownBlockIndex, mergeMarkdownContent } from './utils/markdownTemplate';
import { SendOutlined, UserOutlined } from '@ant-design/icons-vue'; import { SendOutlined, UserOutlined, UpOutlined, DownOutlined } from '@ant-design/icons-vue';
import defaultAvatar from '@/assets/logo.png'; import defaultAvatar from '@/assets/logo.png';
import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件 import ChartComponent from './ChartComponent.vue'; // 导入独立的图表组件
import VoiceRecognition from './VoiceRecognition.vue'; // 导入语音识别组件 import VoiceRecognition from './VoiceRecognition.vue'; // 导入语音识别组件
......
...@@ -657,6 +657,12 @@ defineExpose({ ...@@ -657,6 +657,12 @@ defineExpose({
z-index: 100; z-index: 100;
background: transparent !important; background: transparent !important;
// 禁止用户选择文本
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
&.recording { &.recording {
background: @primary-color; background: @primary-color;
top:0 !important; top:0 !important;
......
...@@ -35,12 +35,6 @@ ...@@ -35,12 +35,6 @@
padding: 0; padding: 0;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
// 禁止用户选择文本
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
// Webkit浏览器滚动条样式(Chrome, Safari, Edge) // Webkit浏览器滚动条样式(Chrome, Safari, Edge)
&::-webkit-scrollbar { &::-webkit-scrollbar {
...@@ -315,7 +309,7 @@ li { ...@@ -315,7 +309,7 @@ li {
:deep(.think-box-toggle) { :deep(.think-box-toggle) {
color: @primary-color; color: @primary-color;
cursor: pointer; cursor: pointer;
font-size: 12px; font-size: 14px;
text-decoration: none; text-decoration: none;
align-items: center; align-items: center;
gap: 4px; gap: 4px;
......
...@@ -170,13 +170,21 @@ export const parseMarkdown = (text: string): string => { ...@@ -170,13 +170,21 @@ export const parseMarkdown = (text: string): string => {
return `<a href="${url}" ${target}>${text}</a>`; return `<a href="${url}" ${target}>${text}</a>`;
}); });
// 处理有序列表 // 处理有序列表(支持列表项中包含图片)
text = text.replace(/^\d+\.\s+(.*)$/gim, '<li>$1</li>'); text = text.replace(/^(\d+\.\s+)(.*)$/gim, (match, prefix, content) => {
// 先处理内容中的Markdown格式(包括图片)
const processedContent = parseMarkdown(content);
return `<li>${processedContent}</li>`;
});
text = text.replace(/(<li>.*<\/li>)(?=\s*<li>)/gim, '$1'); text = text.replace(/(<li>.*<\/li>)(?=\s*<li>)/gim, '$1');
text = text.replace(/(<li>.*<\/li>)/gim, '<ol>$1</ol>'); text = text.replace(/(<li>.*<\/li>)/gim, '<ol>$1</ol>');
// 处理无序列表 // 处理无序列表(支持列表项中包含图片)
text = text.replace(/^[-*+]\s+(.*)$/gim, '<li>$1</li>'); text = text.replace(/^([-*+]\s+)(.*)$/gim, (match, prefix, content) => {
// 先处理内容中的Markdown格式(包括图片)
const processedContent = parseMarkdown(content);
return `<li>${processedContent}</li>`;
});
text = text.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>'); text = text.replace(/(<li>.*<\/li>)/gim, '<ul>$1</ul>');
// 处理水平分割线 // 处理水平分割线
...@@ -589,12 +597,12 @@ class StreamingListProcessor { ...@@ -589,12 +597,12 @@ class StreamingListProcessor {
if (this.listType === 'ordered') { if (this.listType === 'ordered') {
// 有序列表:移除数字标记,保留内容,但保持正确的序号 // 有序列表:移除数字标记,保留内容,但保持正确的序号
const content = item.replace(/^\d+\.\s+/, '').trim(); const content = item.replace(/^\d+\.\s+/, '').trim();
const processedContent = processMarkdownFormat(content); const processedContent = parseMarkdown(content);
return `<li value="${this.orderedListStartNumber + index}">${processedContent}</li>`; return `<li value="${this.orderedListStartNumber + index}">${processedContent}</li>`;
} else { } else {
// 无序列表:移除标记,保留内容 // 无序列表:移除标记,保留内容
const content = item.replace(/^[-*+]\s+/, '').trim(); const content = item.replace(/^[-*+]\s+/, '').trim();
const processedContent = processMarkdownFormat(content); const processedContent = parseMarkdown(content);
return `<li>${processedContent}</li>`; return `<li>${processedContent}</li>`;
} }
}); });
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment