Browse Source
三期- 前端会话管理功能
三期- 前端会话管理功能
- 统计卡片:展示总会话数、总消息数、今日会话、今日消息
- 搜索栏:支持按会话ID或消息内容模糊搜索
- 会话列表表格:展示会话ID、消息数、最后消息时间、预览
- 消息详情弹窗:分类展示用户/AI/系统消息,带时间戳
- 导出功能:一键下载 .txt 格式的会话记录
- 删除功能:逻辑删除整会话的所有消息
master
7 changed files with 781 additions and 0 deletions
-
28src/main/java/com/wok/supportbot/config/DatabaseInitConfig.java
-
175src/main/java/com/wok/supportbot/controller/ConversationController.java
-
291src/main/java/com/wok/supportbot/service/ConversationService.java
-
224src/main/resources/static/components/ConversationManager.js
-
6src/main/resources/static/css/main.css
-
47src/main/resources/static/js/api.js
-
10src/main/resources/static/js/app.js
@ -0,0 +1,175 @@ |
|||
package com.wok.supportbot.controller; |
|||
|
|||
import com.wok.supportbot.service.ConversationService; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.http.MediaType; |
|||
import org.springframework.http.ResponseEntity; |
|||
import org.springframework.web.bind.annotation.*; |
|||
|
|||
import java.util.List; |
|||
import java.util.Map; |
|||
|
|||
/** |
|||
* 会话管理控制器 |
|||
* 提供会话列表、详情、消息、删除、导出等 API |
|||
*/ |
|||
@RestController |
|||
public class ConversationController { |
|||
|
|||
@Autowired |
|||
private ConversationService conversationService; |
|||
|
|||
// ==================== 会话列表 ==================== |
|||
|
|||
/** |
|||
* 获取会话列表(分页) |
|||
* |
|||
* @param page 页码(默认1) |
|||
* @param size 每页大小(默认10) |
|||
* @param keyword 搜索关键词(可选,按会话ID或消息内容模糊匹配) |
|||
* @return 分页会话列表 |
|||
*/ |
|||
@GetMapping("/conversation/list") |
|||
public ResponseEntity<Map<String, Object>> listConversations( |
|||
@RequestParam(defaultValue = "1") int page, |
|||
@RequestParam(defaultValue = "10") int size, |
|||
@RequestParam(required = false) String keyword) { |
|||
try { |
|||
Map<String, Object> result = conversationService.listConversations(page, size, keyword); |
|||
Map<String, Object> data = new java.util.HashMap<>(); |
|||
data.put("success", true); |
|||
data.put("data", result.get("records")); |
|||
data.put("total", result.get("total")); |
|||
data.put("page", result.get("page")); |
|||
data.put("size", result.get("size")); |
|||
data.put("pages", result.get("pages")); |
|||
return ResponseEntity.ok(data); |
|||
} catch (Exception e) { |
|||
return ResponseEntity.status(500).body(Map.of( |
|||
"success", false, |
|||
"message", "查询失败:" + e.getMessage() |
|||
)); |
|||
} |
|||
} |
|||
|
|||
// ==================== 会话详情 ==================== |
|||
|
|||
/** |
|||
* 获取会话详情 |
|||
* |
|||
* @param conversationId 会话ID |
|||
* @return 会话详情 |
|||
*/ |
|||
@GetMapping("/conversation/{id}") |
|||
public ResponseEntity<Map<String, Object>> getConversationDetail(@PathVariable("id") String conversationId) { |
|||
try { |
|||
Map<String, Object> detail = conversationService.getConversationDetail(conversationId); |
|||
if ((int) detail.get("messageCount") == 0) { |
|||
return ResponseEntity.status(404).body(Map.of( |
|||
"success", false, |
|||
"message", "会话不存在或无任何消息" |
|||
)); |
|||
} |
|||
return ResponseEntity.ok(Map.of( |
|||
"success", true, |
|||
"data", detail |
|||
)); |
|||
} catch (Exception e) { |
|||
return ResponseEntity.status(500).body(Map.of( |
|||
"success", false, |
|||
"message", "查询失败:" + e.getMessage() |
|||
)); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取会话消息列表 |
|||
* |
|||
* @param conversationId 会话ID |
|||
* @return 消息列表 |
|||
*/ |
|||
@GetMapping("/conversation/{id}/messages") |
|||
public ResponseEntity<Map<String, Object>> getConversationMessages(@PathVariable("id") String conversationId) { |
|||
try { |
|||
List<Map<String, Object>> messages = conversationService.getConversationMessages(conversationId); |
|||
return ResponseEntity.ok(Map.of( |
|||
"success", true, |
|||
"data", messages, |
|||
"total", messages.size() |
|||
)); |
|||
} catch (Exception e) { |
|||
return ResponseEntity.status(500).body(Map.of( |
|||
"success", false, |
|||
"message", "查询失败:" + e.getMessage() |
|||
)); |
|||
} |
|||
} |
|||
|
|||
// ==================== 删除会话 ==================== |
|||
|
|||
/** |
|||
* 删除会话(逻辑删除该会话下的所有消息) |
|||
* |
|||
* @param conversationId 会话ID |
|||
* @return 删除结果 |
|||
*/ |
|||
@DeleteMapping("/conversation/{id}") |
|||
public ResponseEntity<Map<String, Object>> deleteConversation(@PathVariable("id") String conversationId) { |
|||
try { |
|||
int count = conversationService.deleteConversation(conversationId); |
|||
return ResponseEntity.ok(Map.of( |
|||
"success", true, |
|||
"message", "会话删除成功", |
|||
"deletedMessages", count |
|||
)); |
|||
} catch (Exception e) { |
|||
return ResponseEntity.status(500).body(Map.of( |
|||
"success", false, |
|||
"message", "删除失败:" + e.getMessage() |
|||
)); |
|||
} |
|||
} |
|||
|
|||
// ==================== 导出会话 ==================== |
|||
|
|||
/** |
|||
* 导出会话记录为文本 |
|||
* |
|||
* @param conversationId 会话ID |
|||
* @return 格式化后的会话文本 |
|||
*/ |
|||
@GetMapping(value = "/conversation/{id}/export", produces = MediaType.TEXT_PLAIN_VALUE + ";charset=UTF-8") |
|||
public ResponseEntity<String> exportConversation(@PathVariable("id") String conversationId) { |
|||
try { |
|||
String content = conversationService.exportConversation(conversationId); |
|||
return ResponseEntity.ok() |
|||
.header("Content-Disposition", "attachment; filename=conversation_" + conversationId + ".txt") |
|||
.body(content); |
|||
} catch (Exception e) { |
|||
return ResponseEntity.status(500).body("导出失败:" + e.getMessage()); |
|||
} |
|||
} |
|||
|
|||
// ==================== 统计 ==================== |
|||
|
|||
/** |
|||
* 获取会话统计信息 |
|||
* |
|||
* @return 统计信息 |
|||
*/ |
|||
@GetMapping("/conversation/stats") |
|||
public ResponseEntity<Map<String, Object>> getConversationStats() { |
|||
try { |
|||
Map<String, Object> stats = conversationService.getConversationStats(); |
|||
return ResponseEntity.ok(Map.of( |
|||
"success", true, |
|||
"data", stats |
|||
)); |
|||
} catch (Exception e) { |
|||
return ResponseEntity.status(500).body(Map.of( |
|||
"success", false, |
|||
"message", "查询统计失败:" + e.getMessage() |
|||
)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,291 @@ |
|||
package com.wok.supportbot.service; |
|||
|
|||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
|||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
|||
import com.wok.supportbot.dao.ChatMessageMapper; |
|||
import com.wok.supportbot.entity.ChatMessage; |
|||
import lombok.extern.slf4j.Slf4j; |
|||
import org.springframework.beans.factory.annotation.Autowired; |
|||
import org.springframework.jdbc.core.JdbcTemplate; |
|||
import org.springframework.stereotype.Service; |
|||
|
|||
import java.util.*; |
|||
import java.util.stream.Collectors; |
|||
|
|||
/** |
|||
* 会话管理服务 |
|||
* 提供会话列表、详情、消息列表、删除、导出等功能 |
|||
*/ |
|||
@Service |
|||
@Slf4j |
|||
public class ConversationService { |
|||
|
|||
@Autowired |
|||
private ChatMessageMapper chatMessageMapper; |
|||
|
|||
@Autowired |
|||
private JdbcTemplate jdbcTemplate; |
|||
|
|||
// ==================== 会话列表 ==================== |
|||
|
|||
/** |
|||
* 获取会话列表(分页) |
|||
* |
|||
* @param page 页码(从1开始) |
|||
* @param size 每页大小 |
|||
* @param keyword 搜索关键词(按会话ID或消息内容模糊匹配) |
|||
* @return 分页会话列表 |
|||
*/ |
|||
public Map<String, Object> listConversations(int page, int size, String keyword) { |
|||
// 构建基础 SQL 条件 |
|||
StringBuilder whereClause = new StringBuilder("WHERE is_delete = false"); |
|||
List<Object> params = new ArrayList<>(); |
|||
|
|||
if (keyword != null && !keyword.isEmpty()) { |
|||
// 先找出包含关键词的 conversation_id |
|||
String matchSql = "SELECT DISTINCT conversation_id FROM chat_message " + |
|||
"WHERE is_delete = false AND (conversation_id ILIKE ? OR content ILIKE ?)"; |
|||
List<String> matchedIds = jdbcTemplate.queryForList(matchSql, String.class, |
|||
"%" + keyword + "%", "%" + keyword + "%"); |
|||
|
|||
if (matchedIds.isEmpty()) { |
|||
// 无匹配结果,返回空列表 |
|||
Map<String, Object> emptyResult = new LinkedHashMap<>(); |
|||
emptyResult.put("records", List.of()); |
|||
emptyResult.put("total", 0L); |
|||
emptyResult.put("page", page); |
|||
emptyResult.put("size", size); |
|||
emptyResult.put("pages", 0L); |
|||
return emptyResult; |
|||
} |
|||
|
|||
// 构建 IN 子句 |
|||
String inClause = matchedIds.stream() |
|||
.map(id -> "'" + id.replace("'", "''") + "'") |
|||
.collect(Collectors.joining(",")); |
|||
whereClause.append(" AND conversation_id IN (").append(inClause).append(")"); |
|||
} |
|||
|
|||
// 查询总会话数 |
|||
String countSql = "SELECT COUNT(DISTINCT conversation_id) FROM chat_message " + whereClause; |
|||
Long total = jdbcTemplate.queryForObject(countSql, Long.class, params.toArray()); |
|||
if (total == null) total = 0L; |
|||
|
|||
// 查询会话列表(带统计信息) |
|||
String listSql = """ |
|||
SELECT |
|||
conversation_id, |
|||
COUNT(*) as message_count, |
|||
MIN(create_time) as first_message_time, |
|||
MAX(create_time) as last_message_time, |
|||
(SELECT content FROM chat_message cm2 |
|||
WHERE cm2.conversation_id = cm1.conversation_id AND cm2.is_delete = false |
|||
ORDER BY cm2.create_time DESC LIMIT 1) as last_message_preview |
|||
FROM chat_message cm1 |
|||
""" + whereClause + """ |
|||
GROUP BY conversation_id |
|||
ORDER BY last_message_time DESC |
|||
LIMIT ? OFFSET ? |
|||
"""; |
|||
|
|||
List<Map<String, Object>> records = jdbcTemplate.queryForList(listSql, |
|||
size, (page - 1) * size); |
|||
|
|||
// 格式化结果 |
|||
List<Map<String, Object>> formattedRecords = new ArrayList<>(); |
|||
for (Map<String, Object> record : records) { |
|||
Map<String, Object> formatted = new LinkedHashMap<>(); |
|||
formatted.put("conversationId", record.get("conversation_id")); |
|||
formatted.put("messageCount", ((Number) record.get("message_count")).intValue()); |
|||
formatted.put("firstMessageTime", record.get("first_message_time")); |
|||
formatted.put("lastMessageTime", record.get("last_message_time")); |
|||
String preview = (String) record.get("last_message_preview"); |
|||
formatted.put("lastMessagePreview", preview != null && preview.length() > 100 |
|||
? preview.substring(0, 100) + "..." : preview); |
|||
formattedRecords.add(formatted); |
|||
} |
|||
|
|||
Map<String, Object> result = new LinkedHashMap<>(); |
|||
result.put("records", formattedRecords); |
|||
result.put("total", total); |
|||
result.put("page", page); |
|||
result.put("size", size); |
|||
result.put("pages", (total + size - 1) / size); |
|||
return result; |
|||
} |
|||
|
|||
// ==================== 会话详情 ==================== |
|||
|
|||
/** |
|||
* 获取会话详情 |
|||
* |
|||
* @param conversationId 会话ID |
|||
* @return 会话详情 |
|||
*/ |
|||
public Map<String, Object> getConversationDetail(String conversationId) { |
|||
// 查询会话统计信息 |
|||
String statsSql = """ |
|||
SELECT |
|||
COUNT(*) as message_count, |
|||
COUNT(*) FILTER (WHERE message_type = 'USER') as user_message_count, |
|||
COUNT(*) FILTER (WHERE message_type = 'ASSISTANT') as assistant_message_count, |
|||
MIN(create_time) as first_message_time, |
|||
MAX(create_time) as last_message_time |
|||
FROM chat_message |
|||
WHERE conversation_id = ? AND is_delete = false |
|||
"""; |
|||
|
|||
Map<String, Object> stats; |
|||
try { |
|||
stats = jdbcTemplate.queryForMap(statsSql, conversationId); |
|||
} catch (Exception e) { |
|||
stats = new HashMap<>(); |
|||
stats.put("message_count", 0); |
|||
stats.put("user_message_count", 0); |
|||
stats.put("assistant_message_count", 0); |
|||
} |
|||
|
|||
Map<String, Object> detail = new LinkedHashMap<>(); |
|||
detail.put("conversationId", conversationId); |
|||
detail.put("messageCount", ((Number) stats.get("message_count")).intValue()); |
|||
detail.put("userMessageCount", ((Number) stats.get("user_message_count")).intValue()); |
|||
detail.put("assistantMessageCount", ((Number) stats.get("assistant_message_count")).intValue()); |
|||
detail.put("firstMessageTime", stats.get("first_message_time")); |
|||
detail.put("lastMessageTime", stats.get("last_message_time")); |
|||
|
|||
return detail; |
|||
} |
|||
|
|||
// ==================== 会话消息 ==================== |
|||
|
|||
/** |
|||
* 获取会话消息列表 |
|||
* |
|||
* @param conversationId 会话ID |
|||
* @return 消息列表 |
|||
*/ |
|||
public List<Map<String, Object>> getConversationMessages(String conversationId) { |
|||
LambdaQueryWrapper<ChatMessage> wrapper = new LambdaQueryWrapper<>(); |
|||
wrapper.eq(ChatMessage::getConversationId, conversationId) |
|||
.orderByAsc(ChatMessage::getCreateTime); |
|||
|
|||
List<ChatMessage> messages = chatMessageMapper.selectList(wrapper); |
|||
|
|||
List<Map<String, Object>> result = new ArrayList<>(); |
|||
for (ChatMessage msg : messages) { |
|||
Map<String, Object> item = new LinkedHashMap<>(); |
|||
item.put("id", msg.getId()); |
|||
item.put("conversationId", msg.getConversationId()); |
|||
item.put("messageType", msg.getMessageType()); |
|||
item.put("content", msg.getContent()); |
|||
item.put("metadata", msg.getMetadata()); |
|||
item.put("createTime", msg.getCreateTime()); |
|||
result.add(item); |
|||
} |
|||
return result; |
|||
} |
|||
|
|||
// ==================== 删除会话 ==================== |
|||
|
|||
/** |
|||
* 删除会话(逻辑删除该会话下的所有消息) |
|||
* |
|||
* @param conversationId 会话ID |
|||
* @return 删除的消息数量 |
|||
*/ |
|||
public int deleteConversation(String conversationId) { |
|||
LambdaQueryWrapper<ChatMessage> wrapper = new LambdaQueryWrapper<>(); |
|||
wrapper.eq(ChatMessage::getConversationId, conversationId); |
|||
|
|||
// MyBatis Plus 的 @TableLogic 会自动处理逻辑删除 |
|||
int count = chatMessageMapper.delete(wrapper); |
|||
log.info("删除会话: conversationId={}, 删除消息数={}", conversationId, count); |
|||
return count; |
|||
} |
|||
|
|||
// ==================== 导出会话 ==================== |
|||
|
|||
/** |
|||
* 导出会话记录为文本格式 |
|||
* |
|||
* @param conversationId 会话ID |
|||
* @return 格式化后的会话文本 |
|||
*/ |
|||
public String exportConversation(String conversationId) { |
|||
Map<String, Object> detail = getConversationDetail(conversationId); |
|||
List<Map<String, Object>> messages = getConversationMessages(conversationId); |
|||
|
|||
if (messages.isEmpty()) { |
|||
return "会话不存在或无任何消息"; |
|||
} |
|||
|
|||
StringBuilder sb = new StringBuilder(); |
|||
sb.append("====================================\n"); |
|||
sb.append("会话导出记录\n"); |
|||
sb.append("====================================\n"); |
|||
sb.append("会话ID: ").append(conversationId).append("\n"); |
|||
sb.append("消息总数: ").append(detail.get("messageCount")).append("\n"); |
|||
sb.append("首次消息时间: ").append(detail.get("firstMessageTime")).append("\n"); |
|||
sb.append("最后消息时间: ").append(detail.get("lastMessageTime")).append("\n"); |
|||
sb.append("====================================\n\n"); |
|||
|
|||
for (Map<String, Object> msg : messages) { |
|||
String type = String.valueOf(msg.get("messageType")); |
|||
String content = String.valueOf(msg.get("content")); |
|||
String time = String.valueOf(msg.get("createTime")); |
|||
|
|||
sb.append("[").append(time).append("] "); |
|||
switch (type) { |
|||
case "USER" -> sb.append("用户"); |
|||
case "ASSISTANT" -> sb.append("AI助手"); |
|||
case "SYSTEM" -> sb.append("系统"); |
|||
default -> sb.append(type); |
|||
} |
|||
sb.append("\n"); |
|||
sb.append(content).append("\n\n"); |
|||
} |
|||
|
|||
sb.append("====================================\n"); |
|||
sb.append("导出时间: ").append(new Date()).append("\n"); |
|||
sb.append("====================================\n"); |
|||
|
|||
return sb.toString(); |
|||
} |
|||
|
|||
// ==================== 统计 ==================== |
|||
|
|||
/** |
|||
* 获取会话统计信息 |
|||
* |
|||
* @return 统计信息 |
|||
*/ |
|||
public Map<String, Object> getConversationStats() { |
|||
// 总会话数(不重复 conversation_id) |
|||
String totalSql = "SELECT COUNT(DISTINCT conversation_id) FROM chat_message WHERE is_delete = false"; |
|||
Long totalConversations = jdbcTemplate.queryForObject(totalSql, Long.class); |
|||
if (totalConversations == null) totalConversations = 0L; |
|||
|
|||
// 总消息数 |
|||
String messageSql = "SELECT COUNT(*) FROM chat_message WHERE is_delete = false"; |
|||
Long totalMessages = jdbcTemplate.queryForObject(messageSql, Long.class); |
|||
if (totalMessages == null) totalMessages = 0L; |
|||
|
|||
// 今日消息数 |
|||
String todaySql = "SELECT COUNT(*) FROM chat_message WHERE is_delete = false AND create_time >= CURRENT_DATE"; |
|||
Long todayMessages = jdbcTemplate.queryForObject(todaySql, Long.class); |
|||
if (todayMessages == null) todayMessages = 0L; |
|||
|
|||
// 今日会话数 |
|||
String todayConvSql = "SELECT COUNT(DISTINCT conversation_id) FROM chat_message WHERE is_delete = false AND create_time >= CURRENT_DATE"; |
|||
Long todayConversations = jdbcTemplate.queryForObject(todayConvSql, Long.class); |
|||
if (todayConversations == null) todayConversations = 0L; |
|||
|
|||
Map<String, Object> stats = new LinkedHashMap<>(); |
|||
stats.put("totalConversations", totalConversations); |
|||
stats.put("totalMessages", totalMessages); |
|||
stats.put("todayConversations", todayConversations); |
|||
stats.put("todayMessages", todayMessages); |
|||
|
|||
return stats; |
|||
} |
|||
} |
|||
@ -0,0 +1,224 @@ |
|||
/** |
|||
* 💬 会话管理组件 |
|||
* 展示会话列表、消息详情、删除、导出功能 |
|||
*/ |
|||
import { ref } from 'vue' |
|||
import { listConversations, deleteConversation, getConversationMessages, getConversationStats, exportConversation } from '../js/api.js' |
|||
import { toast, formatDate } from '../js/utils.js' |
|||
|
|||
export default { |
|||
template: `
|
|||
<div class="card"> |
|||
<h2>💬 会话列表</h2> |
|||
|
|||
<!-- 统计卡片 --> |
|||
<div class="stat-grid" v-if="stats" style="margin-bottom:16px;"> |
|||
<div class="stat-card"> |
|||
<div class="number">{{ stats.totalConversations || 0 }}</div> |
|||
<div class="label">总会话数</div> |
|||
</div> |
|||
<div class="stat-card"> |
|||
<div class="number">{{ stats.totalMessages || 0 }}</div> |
|||
<div class="label">总消息数</div> |
|||
</div> |
|||
<div class="stat-card"> |
|||
<div class="number">{{ stats.todayConversations || 0 }}</div> |
|||
<div class="label">今日会话</div> |
|||
</div> |
|||
<div class="stat-card"> |
|||
<div class="number">{{ stats.todayMessages || 0 }}</div> |
|||
<div class="label">今日消息</div> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 搜索栏 --> |
|||
<div class="input-row"> |
|||
<input type="text" class="input" v-model="keyword" placeholder="搜索会话ID或消息内容..." @keyup.enter="load(1)"> |
|||
<button class="btn btn-primary btn-sm" @click="load(1)">🔍 搜索</button> |
|||
<button class="btn btn-outline btn-sm" @click="load()">🔄 刷新</button> |
|||
</div> |
|||
|
|||
<!-- 会话列表表格 --> |
|||
<div style="overflow-x:auto;"> |
|||
<table class="data-table"> |
|||
<thead> |
|||
<tr> |
|||
<th>会话ID</th> |
|||
<th>消息数</th> |
|||
<th>最后消息时间</th> |
|||
<th>最后消息预览</th> |
|||
<th>操作</th> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr v-if="conversations.length === 0"> |
|||
<td colspan="5" style="text-align:center;color:var(--sub);">暂无会话记录</td> |
|||
</tr> |
|||
<tr v-for="c in conversations" :key="c.conversationId"> |
|||
<td> |
|||
<code style="font-size:12px;background:#f3f4f6;padding:2px 6px;border-radius:4px;">{{ c.conversationId }}</code> |
|||
</td> |
|||
<td>{{ c.messageCount }}</td> |
|||
<td>{{ formatDate(c.lastMessageTime) }}</td> |
|||
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"> |
|||
{{ c.lastMessagePreview || '-' }} |
|||
</td> |
|||
<td> |
|||
<button class="btn btn-sm btn-outline" @click="viewMessages(c.conversationId)">查看消息</button> |
|||
<button class="btn btn-sm btn-primary" @click="downloadExport(c.conversationId)">导出</button> |
|||
<button class="btn btn-sm btn-danger" @click="remove(c.conversationId)">删除</button> |
|||
</td> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
|
|||
<!-- 分页 --> |
|||
<div class="pagination" v-if="pages > 1"> |
|||
<button :disabled="page <= 1" @click="load(page - 1)">上一页</button> |
|||
<template v-for="i in pages" :key="i"> |
|||
<button v-if="i === 1 || i === pages || (i >= page - 2 && i <= page + 2)" |
|||
:class="{ active: i === page }" @click="load(i)">{{ i }}</button> |
|||
<span v-else-if="i === page - 3 || i === page + 3" style="padding:6px;">...</span> |
|||
</template> |
|||
<button :disabled="page >= pages" @click="load(page + 1)">下一页</button> |
|||
</div> |
|||
</div> |
|||
|
|||
<!-- 消息详情弹窗 --> |
|||
<div class="modal-overlay" :class="{ active: messageModal.visible }" @click.self="closeMessageModal"> |
|||
<div class="modal-box" style="max-width:900px;"> |
|||
<button class="modal-close" @click="closeMessageModal">×</button> |
|||
<h2>💬 会话消息详情</h2> |
|||
<p style="font-size:13px;color:var(--sub);margin-bottom:12px;"> |
|||
会话ID: <code>{{ messageModal.conversationId }}</code> |
|||
· 共 {{ messageModal.messages.length }} 条消息 |
|||
</p> |
|||
|
|||
<div v-if="messageModal.messages.length === 0" style="text-align:center;color:var(--sub);padding:40px;"> |
|||
暂无消息 |
|||
</div> |
|||
|
|||
<div v-else class="message-list" style="max-height:500px;overflow-y:auto;"> |
|||
<div v-for="msg in messageModal.messages" :key="msg.id" |
|||
:class="['msg-item', msg.messageType === 'USER' ? 'msg-user' : msg.messageType === 'ASSISTANT' ? 'msg-assistant' : 'msg-system']" |
|||
style="margin-bottom:12px;padding:12px;border-radius:8px;"> |
|||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;"> |
|||
<span class="badge" :class="msg.messageType === 'USER' ? 'badge-get' : msg.messageType === 'ASSISTANT' ? 'badge-post' : ''"> |
|||
{{ msg.messageType === 'USER' ? '用户' : msg.messageType === 'ASSISTANT' ? 'AI助手' : '系统' }} |
|||
</span> |
|||
<span style="font-size:11px;color:var(--sub);">{{ formatDate(msg.createTime) }}</span> |
|||
</div> |
|||
<div style="font-size:13px;line-height:1.6;white-space:pre-wrap;word-break:break-word;">{{ msg.content }}</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
`,
|
|||
setup() { |
|||
const conversations = ref([]) |
|||
const page = ref(1) |
|||
const pages = ref(1) |
|||
const total = ref(0) |
|||
const keyword = ref('') |
|||
const stats = ref(null) |
|||
|
|||
// 消息详情弹窗
|
|||
const messageModal = ref({ |
|||
visible: false, |
|||
conversationId: '', |
|||
messages: [] |
|||
}) |
|||
|
|||
async function load(p = 1) { |
|||
page.value = p |
|||
try { |
|||
const json = await listConversations(p, 10, keyword.value || undefined) |
|||
if (json.success) { |
|||
conversations.value = json.data || [] |
|||
total.value = json.total || 0 |
|||
pages.value = json.pages || 1 |
|||
} else { |
|||
toast(json.message || '查询失败', 'error') |
|||
} |
|||
} catch (e) { |
|||
toast('加载会话失败:' + e.message, 'error') |
|||
} |
|||
} |
|||
|
|||
async function loadStats() { |
|||
try { |
|||
const json = await getConversationStats() |
|||
if (json.success) { |
|||
stats.value = json.data |
|||
} |
|||
} catch (e) { |
|||
console.error('加载会话统计失败', e) |
|||
} |
|||
} |
|||
|
|||
async function viewMessages(conversationId) { |
|||
try { |
|||
const json = await getConversationMessages(conversationId) |
|||
if (json.success) { |
|||
messageModal.value.conversationId = conversationId |
|||
messageModal.value.messages = json.data || [] |
|||
messageModal.value.visible = true |
|||
} else { |
|||
toast(json.message || '加载消息失败', 'error') |
|||
} |
|||
} catch (e) { |
|||
toast('加载消息失败:' + e.message, 'error') |
|||
} |
|||
} |
|||
|
|||
function closeMessageModal() { |
|||
messageModal.value.visible = false |
|||
messageModal.value.conversationId = '' |
|||
messageModal.value.messages = [] |
|||
} |
|||
|
|||
async function remove(conversationId) { |
|||
if (!confirm('确定删除此会话?该会话下的所有消息将被逻辑删除')) return |
|||
try { |
|||
const json = await deleteConversation(conversationId) |
|||
if (json.success) { |
|||
toast(`会话删除成功,共删除 ${json.deletedMessages || 0} 条消息`, 'success') |
|||
load(page.value) |
|||
loadStats() |
|||
} else { |
|||
toast(json.message || '删除失败', 'error') |
|||
} |
|||
} catch (e) { |
|||
toast('删除失败:' + e.message, 'error') |
|||
} |
|||
} |
|||
|
|||
async function downloadExport(conversationId) { |
|||
try { |
|||
const content = await exportConversation(conversationId) |
|||
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }) |
|||
const url = URL.createObjectURL(blob) |
|||
const a = document.createElement('a') |
|||
a.href = url |
|||
a.download = `conversation_${conversationId}.txt` |
|||
document.body.appendChild(a) |
|||
a.click() |
|||
document.body.removeChild(a) |
|||
URL.revokeObjectURL(url) |
|||
toast('导出成功', 'success') |
|||
} catch (e) { |
|||
toast('导出失败:' + e.message, 'error') |
|||
} |
|||
} |
|||
|
|||
// 初始加载
|
|||
load() |
|||
loadStats() |
|||
|
|||
return { |
|||
conversations, page, pages, total, keyword, stats, messageModal, |
|||
load, loadStats, viewMessages, closeMessageModal, remove, downloadExport, formatDate |
|||
} |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue