Merge pull request #99 from zstar1003/dev

feat(会话管理): 新增用户会话管理功能模块
This commit is contained in:
zstar 2025-05-17 11:58:37 +08:00 committed by GitHub
commit ef7a48ace8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1102 additions and 18 deletions

1
.gitignore vendored
View File

@ -52,3 +52,4 @@ management/models--opendatalab--PDF-Extract-Kit-1.0
management/models--hantian--layoutreader
docker/models
management/web/types/auto
node_modules/.cache/logger/umi.log

View File

@ -1,7 +1,6 @@
import database
import jwt
import os
from flask import Flask, jsonify, request
from flask import Flask, request
from flask_cors import CORS
from datetime import datetime, timedelta
from routes import register_routes

View File

@ -7,6 +7,7 @@ teams_bp = Blueprint('teams', __name__, url_prefix='/api/v1/teams')
tenants_bp = Blueprint('tenants', __name__, url_prefix='/api/v1/tenants')
files_bp = Blueprint('files', __name__, url_prefix='/api/v1/files')
knowledgebase_bp = Blueprint('knowledgebases', __name__, url_prefix='/api/v1/knowledgebases')
conversation_bp = Blueprint('conversation', __name__, url_prefix='/api/v1/conversation')
# 导入路由
from .users.routes import *
@ -14,6 +15,7 @@ from .teams.routes import *
from .tenants.routes import *
from .files.routes import *
from .knowledgebases.routes import *
from .conversation.routes import *
def register_routes(app):
@ -23,3 +25,4 @@ def register_routes(app):
app.register_blueprint(tenants_bp)
app.register_blueprint(files_bp)
app.register_blueprint(knowledgebase_bp)
app.register_blueprint(conversation_bp)

View File

@ -0,0 +1,63 @@
from flask import jsonify, request
from services.conversation.service import get_conversations_by_user_id, get_messages_by_conversation_id, get_conversation_detail
from .. import conversation_bp
@conversation_bp.route("", methods=["GET"])
def get_conversations():
"""获取对话列表的API端点支持分页和条件查询"""
try:
# 获取查询参数
user_id = request.args.get("user_id")
page = int(request.args.get("page", 1))
size = int(request.args.get("size", 20))
sort_by = request.args.get("sort_by", "update_time")
sort_order = request.args.get("sort_order", "desc")
# 参数验证
if not user_id:
return jsonify({"code": 400, "message": "用户ID不能为空"}), 400
# 调用服务函数获取分页和筛选后的对话数据
conversations, total = get_conversations_by_user_id(user_id, page, size, sort_by, sort_order)
# 返回符合前端期望格式的数据
return jsonify({"code": 0, "data": {"list": conversations, "total": total}, "message": "获取对话列表成功"})
except Exception as e:
# 错误处理
return jsonify({"code": 500, "message": f"获取对话列表失败: {str(e)}"}), 500
@conversation_bp.route("/<conversation_id>/messages", methods=["GET"])
def get_messages(conversation_id):
"""获取特定对话的消息列表"""
try:
# 获取查询参数
page = int(request.args.get("page", 1))
size = int(request.args.get("size", 30))
# 调用服务函数获取消息数据
messages, total = get_messages_by_conversation_id(conversation_id, page, size)
# 返回符合前端期望格式的数据
return jsonify({"code": 0, "data": {"list": messages, "total": total}, "message": "获取消息列表成功"})
except Exception as e:
# 错误处理
return jsonify({"code": 500, "message": f"获取消息列表失败: {str(e)}"}), 500
@conversation_bp.route("/<conversation_id>", methods=["GET"])
def get_conversation(conversation_id):
"""获取特定对话的详细信息"""
try:
# 调用服务函数获取对话详情
conversation = get_conversation_detail(conversation_id)
if not conversation:
return jsonify({"code": 404, "message": "对话不存在"}), 404
# 返回符合前端期望格式的数据
return jsonify({"code": 0, "data": conversation, "message": "获取对话详情成功"})
except Exception as e:
# 错误处理
return jsonify({"code": 500, "message": f"获取对话详情失败: {str(e)}"}), 500

View File

@ -0,0 +1,267 @@
import mysql.connector
from database import DB_CONFIG
def get_conversations_by_user_id(user_id, page=1, size=20, sort_by="update_time", sort_order="desc"):
"""
根据用户ID获取对话列表
参数:
user_id (str): 用户ID
page (int): 当前页码
size (int): 每页大小
sort_by (str): 排序字段
sort_order (str): 排序方式 (asc/desc)
返回:
tuple: (对话列表, 总数)
"""
try:
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor(dictionary=True)
# 直接使用user_id作为tenant_id
tenant_id = user_id
print(f"查询用户ID: {user_id}, 租户ID: {tenant_id}")
# 查询总记录数
count_sql = """
SELECT COUNT(*) as total
FROM dialog d
WHERE d.tenant_id = %s
"""
cursor.execute(count_sql, (tenant_id,))
total = cursor.fetchone()["total"]
print(f"查询到总记录数: {total}")
# 计算分页偏移量
offset = (page - 1) * size
# 确定排序方向
sort_direction = "DESC" if sort_order.lower() == "desc" else "ASC"
# 执行分页查询
query = f"""
SELECT
d.id,
d.name,
d.create_date,
d.update_date,
d.tenant_id
FROM
dialog d
WHERE
d.tenant_id = %s
ORDER BY
d.{sort_by} {sort_direction}
LIMIT %s OFFSET %s
"""
print(f"执行查询: {query}")
print(f"参数: tenant_id={tenant_id}, size={size}, offset={offset}")
cursor.execute(query, (tenant_id, size, offset))
results = cursor.fetchall()
print(f"查询结果数量: {len(results)}")
# 获取每个对话的最新消息
conversations = []
for dialog in results:
# 查询对话的所有消息
conv_query = """
SELECT id, message, name
FROM conversation
WHERE dialog_id = %s
ORDER BY create_date DESC
"""
cursor.execute(conv_query, (dialog["id"],))
conv_results = cursor.fetchall()
latest_message = ""
conversation_name = dialog["name"] # 默认使用dialog的name
if conv_results and len(conv_results) > 0:
# 获取最新的一条对话记录
latest_conv = conv_results[0]
# 如果conversation有name优先使用conversation的name
if latest_conv and latest_conv.get("name"):
conversation_name = latest_conv["name"]
if latest_conv and latest_conv["message"]:
# 获取最后一条消息内容
messages = latest_conv["message"]
if messages and len(messages) > 0:
# 检查消息类型,处理字符串和字典两种情况
if isinstance(messages[-1], dict):
latest_message = messages[-1].get("content", "")
elif isinstance(messages[-1], str):
latest_message = messages[-1]
else:
latest_message = str(messages[-1])
conversations.append(
{
"id": dialog["id"],
"name": conversation_name,
"latestMessage": latest_message[:100] + "..." if len(latest_message) > 100 else latest_message,
"createTime": dialog["create_date"].strftime("%Y-%m-%d %H:%M:%S") if dialog["create_date"] else "",
"updateTime": dialog["update_date"].strftime("%Y-%m-%d %H:%M:%S") if dialog["update_date"] else "",
}
)
# 关闭连接
cursor.close()
conn.close()
return conversations, total
except mysql.connector.Error as err:
print(f"数据库错误: {err}")
# 更详细的错误日志
import traceback
traceback.print_exc()
return [], 0
except Exception as e:
print(f"未知错误: {e}")
import traceback
traceback.print_exc()
return [], 0
def get_messages_by_conversation_id(conversation_id, page=1, size=30):
"""
获取特定对话的详细信息
参数:
conversation_id (str): 对话ID
page (int): 当前页码
size (int): 每页大小
返回:
tuple: (对话详情, 总数)
"""
try:
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor(dictionary=True)
# 查询对话信息
query = """
SELECT *
FROM conversation
WHERE dialog_id = %s
ORDER BY create_date DESC
"""
cursor.execute(query, (conversation_id,))
result = cursor.fetchall() # 确保读取所有结果
if not result:
print(f"未找到对话ID: {conversation_id}")
cursor.close()
conn.close()
return None, 0
# 获取第一条记录作为对话详情
conversation = None
if len(result) > 0:
conversation = {
"id": result[0]["id"],
"dialogId": result[0].get("dialog_id", ""),
"createTime": result[0]["create_date"].strftime("%Y-%m-%d %H:%M:%S") if result[0].get("create_date") else "",
"updateTime": result[0]["update_date"].strftime("%Y-%m-%d %H:%M:%S") if result[0].get("update_date") else "",
"messages": result[0].get("message", []),
}
# 打印调试信息
print(f"获取到对话详情: ID={conversation_id}")
print(f"消息长度: {len(conversation['messages']) if conversation and conversation.get('messages') else 0}")
# 关闭连接
cursor.close()
conn.close()
# 返回对话详情和消息总数
total = len(conversation["messages"]) if conversation and conversation.get("messages") else 0
return conversation, total
except mysql.connector.Error as err:
print(f"数据库错误: {err}")
# 更详细的错误日志
import traceback
traceback.print_exc()
return None, 0
except Exception as e:
print(f"未知错误: {e}")
import traceback
traceback.print_exc()
return None, 0
def get_conversation_detail(conversation_id):
"""
获取特定对话的详细信息
参数:
conversation_id (str): 对话ID
返回:
dict: 对话详情
"""
try:
conn = mysql.connector.connect(**DB_CONFIG)
cursor = conn.cursor(dictionary=True)
# 查询对话信息
query = """
SELECT c.*, d.name as dialog_name, d.icon as dialog_icon
FROM conversation c
LEFT JOIN dialog d ON c.dialog_id = d.id
WHERE c.id = %s
"""
cursor.execute(query, (conversation_id,))
result = cursor.fetchone()
if not result:
print(f"未找到对话ID: {conversation_id}")
return None
# 格式化对话详情
conversation = {
"id": result["id"],
"name": result.get("name", ""),
"dialogId": result.get("dialog_id", ""),
"dialogName": result.get("dialog_name", ""),
"dialogIcon": result.get("dialog_icon", ""),
"createTime": result["create_date"].strftime("%Y-%m-%d %H:%M:%S") if result.get("create_date") else "",
"updateTime": result["update_date"].strftime("%Y-%m-%d %H:%M:%S") if result.get("update_date") else "",
"messages": result.get("message", []),
}
# 打印调试信息
print(f"获取到对话详情: ID={conversation_id}")
print(f"消息数量: {len(conversation['messages']) if conversation['messages'] else 0}")
# 关闭连接
cursor.close()
conn.close()
return conversation
except mysql.connector.Error as err:
print(f"数据库错误: {err}")
# 更详细的错误日志
import traceback
traceback.print_exc()
return None
except Exception as e:
print(f"未知错误: {e}")
import traceback
traceback.print_exc()
return None

View File

@ -205,7 +205,7 @@ class KnowledgebaseService:
SELECT llm_name
FROM tenant_llm
WHERE model_type = 'embedding'
ORDER BY create_time ASC
ORDER BY create_time DESC
LIMIT 1
"""
cursor.execute(query_embedding_model)
@ -213,6 +213,9 @@ class KnowledgebaseService:
if embedding_model and embedding_model.get('llm_name'):
dynamic_embd_id = embedding_model['llm_name']
# 对硅基流动平台进行特异性处理
if dynamic_embd_id == "netease-youdao/bce-embedding-base_v1":
dynamic_embd_id = "BAAI/bge-m3"
print(f"动态获取到的 embedding 模型 ID: {dynamic_embd_id}")
else:
dynamic_embd_id = default_embd_id

View File

@ -0,0 +1,21 @@
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<!-- 气泡主体(深色区) -->
<path d="M832 192H320c-70.7 0-128 57.3-128 128v448c0 70.7 57.3 128 128 128h384l160 160V896c70.7 0 128-57.3 128-128V320c0-70.7-57.3-128-128-128z" fill="#3B78E7"/>
<!-- 气泡高光(浅色区) -->
<path d="M832 192H320c-35.3 0-64 28.7-64 64v448c0 35.3 28.7 64 64 64h384l160 160V768c35.3 0 64-28.7 64-64V320c0-35.3-28.7-64-64-64z" fill="#4D90FE"/>
<!-- 消息横条1长条 -->
<path d="M256 384h512v64H256z" fill="#FFFFFF" opacity="0.8"/>
<!-- 消息横条2中长条 -->
<path d="M256 512h384v64H256z" fill="#FFFFFF" opacity="0.9"/>
<!-- 消息横条3短条 -->
<path d="M256 640h256v64H256z" fill="#FFFFFF"/>
<!-- 消息横条4长条 -->
<path d="M256 768h448v64H256z" fill="#FFFFFF" opacity="0.7"/>
</svg>

After

Width:  |  Height:  |  Size: 893 B

View File

@ -0,0 +1,709 @@
<script lang="ts" setup>
import type { CreateOrUpdateTableRequestData, TableData } from "@@/apis/tables/type"
import type { FormInstance, FormRules } from "element-plus"
import { createTableDataApi, deleteTableDataApi, getTableDataApi, resetPasswordApi, updateTableDataApi } from "@@/apis/tables"
import { usePagination } from "@@/composables/usePagination"
import { ChatDotRound, CirclePlus, Delete, Edit, Key, Refresh, RefreshRight, Search, User } from "@element-plus/icons-vue"
import axios from "axios"
import { cloneDeep } from "lodash-es"
defineOptions({
//
name: "ConversationManagement"
})
// #region
const userList = ref<TableData[]>([])
const searchData = reactive({
username: "",
email: ""
})
//
const userLoading = ref(false)
const userHasMore = ref(true)
const userPage = ref(1)
const userPageSize = 20
//
const sortData = reactive({
sortBy: "create_date",
sortOrder: "desc" // ()
})
/**
* 获取用户列表数据
* @param isLoadMore 是否为加载更多操作
*/
function getUserData(isLoadMore = false) {
if (!isLoadMore) {
userPage.value = 1
userList.value = []
}
userLoading.value = true
getTableDataApi({
currentPage: userPage.value,
size: userPageSize,
username: searchData.username,
email: searchData.email,
sort_by: sortData.sortBy,
sort_order: sortData.sortOrder
}).then(({ data }) => {
if (isLoadMore) {
userList.value = [...userList.value, ...data.list]
} else {
userList.value = data.list
}
//
userHasMore.value = userList.value.length < data.total
}).catch(() => {
if (!isLoadMore) {
userList.value = []
}
}).finally(() => {
userLoading.value = false
})
}
/**
* 加载更多用户
*/
function loadMoreUsers() {
if (userLoading.value || !userHasMore.value) return
userPage.value++
getUserData(true)
}
/**
* 监听用户列表滚动事件
* @param event 滚动事件
*/
function handleUserListScroll(event) {
const { scrollTop, scrollHeight, clientHeight } = event.target
// 100px
if (scrollHeight - scrollTop - clientHeight < 100 && userHasMore.value && !userLoading.value) {
loadMoreUsers()
}
}
// #endregion
// #region
//
interface ConversationData {
id: string // string
name: string // titlename
latestMessage: string // latestMessage
createTime: string // create_timecreateTime
updateTime: string
}
//
interface MessageData {
id: number
conversation_id: number
role: string
content: string
create_time: string
}
const conversationList = ref<ConversationData[]>([])
const messageList = ref<MessageData[]>([])
const conversationLoading = ref(false)
const messageLoading = ref(false)
//
const conversationHasMore = ref(true)
const conversationPage = ref(1)
const conversationPageSize = 20
//
const messageHasMore = ref(true)
const messagePage = ref(1)
const messagePageSize = 30
//
const selectedUser = ref<TableData | null>(null)
const selectedConversation = ref<ConversationData | null>(null)
/**
* 选择用户
* @param user 用户数据
*/
function selectUser(user: TableData) {
selectedUser.value = user
selectedConversation.value = null
messageList.value = []
conversationPage.value = 1
conversationHasMore.value = true
getConversationsByUserId(user.id)
}
/**
* 选择对话
* @param conversation 对话数据
*/
function selectConversation(conversation: ConversationData) {
selectedConversation.value = conversation
messagePage.value = 1
messageHasMore.value = true
messageList.value = []
getMessagesByConversationId(conversation.id)
}
/**
* 获取用户的对话列表
* @param userId 用户ID
* @param isLoadMore 是否为加载更多操作
*/
function getConversationsByUserId(userId: number, isLoadMore = false) {
conversationLoading.value = true
// API
axios.get(`/api/v1/conversation`, {
params: {
user_id: userId,
page: conversationPage.value,
size: conversationPageSize,
sort_by: "update_time",
sort_order: "desc"
}
}).then((response) => {
const data = response.data.data
if (isLoadMore) {
conversationList.value = [...conversationList.value, ...(data.list || [])]
} else {
conversationList.value = data.list || []
}
//
conversationHasMore.value = conversationList.value.length < (data.total || 0)
}).catch((error) => {
console.error("获取对话列表失败:", error)
ElMessage.error("获取对话列表失败")
if (!isLoadMore) {
conversationList.value = []
}
}).finally(() => {
conversationLoading.value = false
})
}
/**
* 加载更多对话
*/
function loadMoreConversations() {
if (conversationLoading.value || !conversationHasMore.value || !selectedUser.value) return
conversationPage.value++
getConversationsByUserId(selectedUser.value.id, true)
}
/**
* 监听对话列表滚动事件
* @param event 滚动事件
*/
function handleConversationListScroll(event) {
const { scrollTop, scrollHeight, clientHeight } = event.target
// 100px
if (scrollHeight - scrollTop - clientHeight < 100 && conversationHasMore.value && !conversationLoading.value) {
loadMoreConversations()
}
}
/**
* 获取对话的消息列表
* @param conversationId 对话ID
* @param isLoadMore 是否为加载更多操作
*/
function getMessagesByConversationId(conversationId: string, isLoadMore = false) {
messageLoading.value = true
// API
axios.get(`/api/v1/conversation/${conversationId}/messages`, {
params: {
page: messagePage.value,
size: messagePageSize,
sort_by: "create_time",
sort_order: "asc" //
}
})
.then((response) => {
const data = response.data
//
console.log("获取到的消息数据:", data)
//
let processedMessages = []
if (data.data && data.data.list) {
const conversation = data.data.list
// messagesJSON
if (conversation.messages && typeof conversation.messages === "string") {
try {
const parsedMessages = JSON.parse(conversation.messages)
//
processedMessages = parsedMessages.map((msg, index) => {
return {
id: msg.id || `msg-${index}`,
conversation_id: conversationId,
role: msg.role || "unknown",
content: msg.content || "",
create_time: msg.created_at ? new Date(msg.created_at * 1000).toISOString() : conversation.createTime
}
})
} catch (error) {
console.error("解析消息数据失败:", error)
processedMessages = []
}
}
}
console.log("处理后的消息数据:", processedMessages)
if (isLoadMore) {
//
const existingIds = new Set(messageList.value.map(msg => msg.id))
const uniqueNewMessages = processedMessages.filter(msg => !existingIds.has(msg.id))
//
messageList.value = [...messageList.value, ...uniqueNewMessages]
console.log(`加载了 ${uniqueNewMessages.length} 条新消息,过滤掉 ${processedMessages.length - uniqueNewMessages.length} 条重复消息`)
} else {
messageList.value = processedMessages
}
//
messageHasMore.value = messageList.value.length < (data.data.total || 0)
//
if (!isLoadMore && messageList.value.length > 0) {
setTimeout(() => {
const messageListEl = document.querySelector(".message-list")
if (messageListEl) {
messageListEl.scrollTop = messageListEl.scrollHeight
}
}, 100)
}
})
.catch((error) => {
console.error("获取消息列表失败:", error)
ElMessage.error("获取消息列表失败")
if (!isLoadMore) {
messageList.value = []
}
})
.finally(() => {
messageLoading.value = false
})
}
/**
* 渲染消息内容处理图片和链接
* @param content 消息内容
* @returns 处理后的HTML内容
*/
function renderMessageContent(content) {
if (!content) return ""
// Markdown
let processedContent = content.replace(/!\[.*?\]\((.*?)\)/g, "<img src=\"$1\" class=\"message-image\" />")
//
processedContent = processedContent.replace(/\n/g, "<br>")
return processedContent
}
/**
* 加载更多消息
*/
function loadMoreMessages() {
if (messageLoading.value || !messageHasMore.value || !selectedConversation.value) return
messagePage.value++
getMessagesByConversationId(selectedConversation.value.id, true)
}
/**
* 监听消息列表滚动事件
* @param event 滚动事件
*/
function handleMessageListScroll(event) {
const { scrollTop, scrollHeight, clientHeight } = event.target
// 100px
if (scrollHeight - scrollTop - clientHeight < 100 && messageHasMore.value && !messageLoading.value) {
loadMoreMessages()
}
}
//
onMounted(() => {
getUserData()
})
</script>
<template>
<div class="app-container">
<!-- 多级卡片区域 -->
<div class="conversation-cards-container">
<!-- 第一个卡片用户列表 -->
<el-card shadow="hover" class="user-card">
<template #header>
<div class="card-header">
<span>用户列表</span>
</div>
</template>
<div class="user-list" @scroll="handleUserListScroll">
<div
v-for="user in userList"
:key="user.id"
class="user-item"
:class="{ active: selectedUser?.id === user.id }"
@click="selectUser(user)"
>
<el-avatar :size="32" :icon="User" />
<div class="user-info">
<div class="username">
{{ user.username }}
</div>
<div class="email">
{{ user.email }}
</div>
</div>
</div>
<div v-if="userLoading" class="loading-more">
<el-icon class="loading-icon">
<Loading />
</el-icon>
<span>加载中...</span>
</div>
<el-empty v-if="userList.length === 0 && !userLoading" description="暂无用户数据" />
</div>
</el-card>
<!-- 第二个卡片对话标题列表 -->
<el-card shadow="hover" class="conversation-card">
<template #header>
<div class="card-header">
<span>对话列表</span>
</div>
</template>
<div class="conversation-list" @scroll="handleConversationListScroll">
<template v-if="selectedUser">
<div
v-for="conversation in conversationList"
:key="conversation.id"
class="conversation-item"
:class="{ active: selectedConversation?.id === conversation.id }"
@click="selectConversation(conversation)"
>
<div class="conversation-icon">
<el-icon><ChatDotRound /></el-icon>
</div>
<div class="conversation-info">
<div class="conversation-title">
{{ conversation.name }}
</div>
<div class="conversation-meta">
<span>{{ new Date(conversation.updateTime).toLocaleString() }}</span>
</div>
</div>
</div>
<div v-if="conversationLoading" class="loading-more">
<el-icon class="loading-icon">
<Loading />
</el-icon>
<span>加载中...</span>
</div>
<el-empty v-if="conversationList.length === 0 && !conversationLoading" description="暂无对话数据" />
</template>
<el-empty v-else description="请先选择用户" />
</div>
</el-card>
<!-- 第三个卡片对话内容 -->
<el-card shadow="hover" class="message-card">
<template #header>
<div class="card-header">
<span>对话标题: {{ selectedConversation?.name || '未选择对话' }}</span>
</div>
</template>
<div class="message-list" @scroll="handleMessageListScroll">
<template v-if="selectedConversation">
<div
v-for="message in messageList"
:key="message.id"
class="message-item"
:class="{ 'user-message': message.role === 'user', 'assistant-message': message.role === 'assistant' }"
>
<div class="message-header">
<span class="message-role">{{ message.role === 'user' ? '用户' : '助手' }}</span>
<span class="message-time">{{ new Date(message.create_time).toLocaleString() }}</span>
</div>
<div class="message-content" v-html="renderMessageContent(message.content)" />
</div>
<el-empty v-if="messageList.length === 0 && !messageLoading" description="暂无消息数据" />
<!-- 加载提示 -->
<!-- <div v-if="messageHasMore" class="loading-more bottom-loading">
<el-icon class="loading-icon" :class="{ 'is-loading': messageLoading }">
<Loading />
</el-icon>
<span>{{ messageLoading ? '加载中...' : '向下滚动加载更多' }}</span>
</div> -->
</template>
<el-empty v-else description="请先选择对话" />
</div>
</el-card>
</div>
</div>
</template>
<style lang="scss" scoped>
.search-wrapper {
margin-bottom: 20px;
:deep(.el-card__body) {
padding-bottom: 2px;
}
}
.conversation-cards-container {
display: flex;
gap: 20px;
height: calc(100vh - 240px);
min-height: 750px;
overflow-x: auto; /* 添加水平滚动 */
padding-bottom: 10px; /* 添加底部内边距,避免滚动条遮挡内容 */
}
.user-card {
width: 25%;
min-width: 250px; /* 设置最小宽度,避免卡片过小 */
display: flex;
flex-direction: column;
flex-shrink: 0; /* 防止卡片被压缩 */
}
.conversation-card {
width: 25%;
min-width: 250px; /* 设置最小宽度,避免卡片过小 */
display: flex;
flex-direction: column;
flex-shrink: 0; /* 防止卡片被压缩 */
}
.message-card {
flex: 1;
min-width: 300px; /* 设置最小宽度,避免卡片过小 */
display: flex;
flex-direction: column;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
}
.user-list,
.conversation-list,
.message-list {
overflow-y: auto;
flex: 1;
position: relative;
padding: 0 4px;
max-height: calc(100vh - 300px); /* 设置最大高度,确保内容可滚动 */
&::-webkit-scrollbar {
width: 6px;
height: 6px; /* 添加水平滚动条高度 */
}
&::-webkit-scrollbar-thumb {
background-color: var(--el-border-color-darker);
border-radius: 3px;
}
&::-webkit-scrollbar-track {
background-color: var(--el-fill-color-lighter);
border-radius: 3px;
}
}
.loading-more {
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
color: var(--el-text-color-secondary);
font-size: 14px;
.loading-icon {
margin-right: 6px;
animation: rotating 2s linear infinite;
}
&.top-loading {
position: sticky;
top: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
}
&.bottom-loading {
position: sticky;
bottom: 0;
background-color: rgba(255, 255, 255, 0.8);
z-index: 1;
}
}
@keyframes rotating {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.user-item {
display: flex;
align-items: center;
padding: 10px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
margin-bottom: 8px;
&:hover {
background-color: var(--el-fill-color-light);
}
&.active {
background-color: var(--el-color-primary-light-9);
}
.user-info {
margin-left: 10px;
.username {
font-weight: bold;
}
.email {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
.conversation-item {
display: flex;
align-items: center;
padding: 10px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
margin-bottom: 8px;
&:hover {
background-color: var(--el-fill-color-light);
}
&.active {
background-color: var(--el-color-primary-light-9);
}
.conversation-icon {
font-size: 20px;
color: var(--el-color-primary);
}
.conversation-info {
margin-left: 10px;
flex: 1;
.conversation-title {
font-weight: bold;
margin-bottom: 4px;
}
.conversation-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
}
.message-item {
padding: 12px;
border-radius: 8px;
margin-bottom: 16px;
&.user-message {
background-color: var(--el-color-primary-light-9);
margin-left: 20px;
}
&.assistant-message {
background-color: var(--el-fill-color-light);
margin-right: 20px;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
.message-role {
font-weight: bold;
}
.message-time {
font-size: 12px;
color: var(--el-text-color-secondary);
}
}
.message-content {
white-space: pre-wrap;
word-break: break-word;
.message-image {
max-width: 100%;
border-radius: 4px;
margin: 8px 0;
}
}
}
</style>
<!-- 添加全局滚动条样式 -->
<style lang="scss">
/* 全局滚动条样式 */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-thumb {
background-color: var(--el-border-color);
border-radius: 4px;
&:hover {
background-color: var(--el-border-color-darker);
}
}
::-webkit-scrollbar-track {
background-color: var(--el-fill-color-lighter);
border-radius: 4px;
}
</style>

View File

@ -134,6 +134,24 @@ export const constantRoutes: RouteRecordRaw[] = [
}
}
]
},
{
path: "/conversation",
component: Layouts,
redirect: "/conversation/index",
children: [
{
path: "index",
component: () => import("@/pages/conversation/index.vue"),
name: "conversation",
meta: {
title: "用户会话管理",
svgIcon: "conversation",
affix: false,
keepAlive: true
}
}
]
}
]

View File

@ -9,18 +9,18 @@ declare module 'vue' {
export interface GlobalComponents {
SvgIcon: import("vue").DefineComponent<{
name: {
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
type: import("vue").PropType<"conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
default: string;
required: true;
};
}, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
name: {
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
type: import("vue").PropType<"conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
default: string;
required: true;
};
}>>, {
name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
name: "conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
}>;
}
}

View File

@ -7,20 +7,20 @@
declare module '~virtual/svg-component' {
const SvgIcon: import("vue").DefineComponent<{
name: {
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
type: import("vue").PropType<"conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
default: string;
required: true;
};
}, {}, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<import("vue").ExtractPropTypes<{
name: {
type: import("vue").PropType<"dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
type: import("vue").PropType<"conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management">;
default: string;
required: true;
};
}>>, {
name: "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
name: "conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
}>;
export const svgNames: ["dashboard", "file", "fullscreen-exit", "fullscreen", "kb", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search", "team-management", "user-config", "user-management"];
export type SvgName = "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
export const svgNames: ["conversation", "dashboard", "file", "fullscreen-exit", "fullscreen", "kb", "keyboard-down", "keyboard-enter", "keyboard-esc", "keyboard-up", "search", "team-management", "user-config", "user-management"];
export type SvgName = "conversation" | "dashboard" | "file" | "fullscreen-exit" | "fullscreen" | "kb" | "keyboard-down" | "keyboard-enter" | "keyboard-esc" | "keyboard-up" | "search" | "team-management" | "user-config" | "user-management";
export default SvgIcon;
}

View File

@ -11,9 +11,9 @@ import classNames from 'classnames';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
import OperateDropdown from '@/components/operate-dropdown';
// import OperateDropdown from '@/components/operate-dropdown';
import { useTheme } from '@/components/theme-provider';
import { useDeleteKnowledge } from '@/hooks/knowledge-hooks';
// import { useDeleteKnowledge } from '@/hooks/knowledge-hooks';
import { useFetchUserInfo } from '@/hooks/user-setting-hooks';
import styles from './index.less';
@ -26,11 +26,11 @@ const KnowledgeCard = ({ item }: IProps) => {
const { t } = useTranslation();
const { data: userInfo } = useFetchUserInfo();
const { theme } = useTheme();
const { deleteKnowledge } = useDeleteKnowledge();
// const { deleteKnowledge } = useDeleteKnowledge();
const removeKnowledge = async () => {
return deleteKnowledge(item.id);
};
// const removeKnowledge = async () => {
// return deleteKnowledge(item.id);
// };
const handleCardClick = () => {
navigate(`/knowledge/${KnowledgeRouteKey.Dataset}?id=${item.id}`, {
@ -50,7 +50,7 @@ const KnowledgeCard = ({ item }: IProps) => {
<div className={styles.container}>
<div className={styles.content}>
<Avatar size={34} icon={<UserOutlined />} src={item.avatar} />
<OperateDropdown deleteItem={removeKnowledge}></OperateDropdown>
{/* <OperateDropdown deleteItem={removeKnowledge}></OperateDropdown> */}
</div>
<div className={styles.titleWrapper}>
<span