mirror of
https://github.com/ILoveBingLu/CipherTalk.git
synced 2026-05-18 02:09:16 +08:00
ff05dbaa32
- 新增聊天记录独立窗口(ChatHistoryPage),支持在单独窗口中查看完整聊天记录 - 实现 createChatHistoryWindow 函数,支持窗口复用和主题适配 - 新增 IPC 处理器用于打开聊天记录窗口和获取单条消息 - 添加 getMessagesByDate 和 getDatesWithMessages 方法,支持按日期查询消息 - 在 preload.ts 中暴露新的 IPC 调用接口 - 新增 ChatHistoryPage.tsx 和 ChatHistoryPage.scss 组件文件 - 更新 package.json 依赖项和 package-lock.json - 更新 README.md,新增爱发电赞助支持入口 - 添加爱发电二维码图片资源 - 版本号更新至 2.1.6 - 优化聊天页面和设置页面的用户体验 - 更新类型定义和配置文件以支持新功能
865 lines
19 KiB
TypeScript
865 lines
19 KiB
TypeScript
/**
|
|
* HTML 导出生成器
|
|
* 负责生成聊天记录的 HTML 展示页面
|
|
* 使用外部资源引用,避免文件过大
|
|
*/
|
|
|
|
export interface HtmlExportMessage {
|
|
timestamp: number
|
|
sender: string
|
|
senderName: string
|
|
type: number
|
|
content: string | null
|
|
rawContent: string
|
|
isSend: boolean
|
|
chatRecords?: HtmlChatRecord[]
|
|
}
|
|
|
|
export interface HtmlChatRecord {
|
|
sender: string
|
|
senderDisplayName: string
|
|
timestamp: number
|
|
formattedTime: string
|
|
type: string
|
|
datatype: number
|
|
content: string
|
|
senderAvatar?: string
|
|
fileExt?: string
|
|
fileSize?: number
|
|
}
|
|
|
|
export interface HtmlMember {
|
|
id: string
|
|
name: string
|
|
avatar?: string
|
|
}
|
|
|
|
export interface HtmlExportData {
|
|
meta: {
|
|
sessionId: string
|
|
sessionName: string
|
|
isGroup: boolean
|
|
exportTime: number
|
|
messageCount: number
|
|
dateRange: { start: number; end: number } | null
|
|
}
|
|
members: HtmlMember[]
|
|
messages: HtmlExportMessage[]
|
|
}
|
|
|
|
export class HtmlExportGenerator {
|
|
/**
|
|
* 生成 HTML 主文件(引用外部 CSS 和 JS)
|
|
*/
|
|
static generateHtmlWithData(exportData: HtmlExportData): string {
|
|
const escapedSessionName = this.escapeHtml(exportData.meta.sessionName)
|
|
const dateRangeText = exportData.meta.dateRange
|
|
? `${new Date(exportData.meta.dateRange.start * 1000).toLocaleDateString('zh-CN')} - ${new Date(exportData.meta.dateRange.end * 1000).toLocaleDateString('zh-CN')}`
|
|
: ''
|
|
|
|
return `<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>${escapedSessionName} - 聊天记录</title>
|
|
<link rel="stylesheet" href="./styles.css">
|
|
<style>
|
|
/* 仅保留关键的内联样式,确保基本布局 */
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>${escapedSessionName}</h1>
|
|
<div class="meta">
|
|
<span>共 ${exportData.messages.length} 条消息</span>
|
|
${dateRangeText ? `<span> | ${dateRangeText}</span>` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="controls">
|
|
<input type="text" id="searchInput" placeholder="搜索消息内容..." />
|
|
<button onclick="app.searchMessages()">搜索</button>
|
|
<button onclick="app.clearSearch()">清除</button>
|
|
<div class="stats">
|
|
<span id="messageStats">共 ${exportData.messages.length} 条消息</span>
|
|
<span id="loadedStats"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="scrollContainer" class="scroll-container">
|
|
<div id="messagesContainer" class="messages">
|
|
<div class="loading">正在加载聊天记录...</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
由 CipherTalk 导出 | ${new Date(exportData.meta.exportTime).toLocaleString('zh-CN')}
|
|
</div>
|
|
</div>
|
|
|
|
<script src="./data.js"></script>
|
|
<script src="./app.js"></script>
|
|
</body>
|
|
</html>`;
|
|
}
|
|
|
|
/**
|
|
* 生成外部 CSS 文件
|
|
*/
|
|
static generateCss(): string {
|
|
return `/* CipherTalk 聊天记录导出样式 */
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
line-height: 1.6;
|
|
color: #333;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1000px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 16px;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
overflow: hidden;
|
|
animation: slideIn 0.5s ease-out;
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from {
|
|
opacity: 0;
|
|
transform: translateY(30px);
|
|
}
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
/* 头部样式 */
|
|
.header {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
padding: 40px 30px;
|
|
text-align: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -50%;
|
|
left: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
|
animation: pulse 15s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { transform: scale(1); }
|
|
50% { transform: scale(1.1); }
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 32px;
|
|
margin-bottom: 12px;
|
|
font-weight: 700;
|
|
position: relative;
|
|
z-index: 1;
|
|
text-shadow: 0 2px 10px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.header .meta {
|
|
font-size: 15px;
|
|
opacity: 0.95;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* 控制栏样式 */
|
|
.controls {
|
|
position: sticky;
|
|
top: 0;
|
|
background: white;
|
|
padding: 20px;
|
|
border-bottom: 2px solid #f0f0f0;
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
z-index: 100;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.controls input[type="text"] {
|
|
flex: 1;
|
|
min-width: 250px;
|
|
padding: 12px 16px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
transition: all 0.3s;
|
|
}
|
|
|
|
.controls input[type="text"]:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
}
|
|
|
|
.controls button {
|
|
padding: 12px 24px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
transition: all 0.3s;
|
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
|
|
}
|
|
|
|
.controls button:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.4);
|
|
}
|
|
|
|
.controls button:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.controls .stats {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: center;
|
|
margin-left: auto;
|
|
font-size: 14px;
|
|
color: #666;
|
|
}
|
|
|
|
.controls .stats span {
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* 滚动容器 */
|
|
.scroll-container {
|
|
height: calc(100vh - 280px);
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
position: relative;
|
|
will-change: scroll-position;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.scroll-container::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
|
|
.scroll-container::-webkit-scrollbar-track {
|
|
background: #f1f1f1;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.scroll-container::-webkit-scrollbar-thumb {
|
|
background: #888;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.scroll-container::-webkit-scrollbar-thumb:hover {
|
|
background: #555;
|
|
}
|
|
|
|
/* 消息容器 */
|
|
.messages {
|
|
padding: 20px;
|
|
background: #fafafa;
|
|
}
|
|
|
|
.message-placeholder {
|
|
height: 80px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #999;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.loading,
|
|
.error,
|
|
.no-messages {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.loading {
|
|
color: #999;
|
|
}
|
|
|
|
.error {
|
|
color: #d32f2f;
|
|
}
|
|
|
|
.no-messages {
|
|
color: #999;
|
|
}
|
|
|
|
/* 消息样式 */
|
|
.message {
|
|
display: flex;
|
|
margin-bottom: 20px;
|
|
opacity: 1;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.message:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.message.sent {
|
|
flex-direction: row-reverse;
|
|
}
|
|
|
|
.message .avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
flex-shrink: 0;
|
|
overflow: hidden;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: 700;
|
|
font-size: 16px;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.message .avatar img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.message .content-wrapper {
|
|
max-width: 65%;
|
|
margin: 0 10px;
|
|
}
|
|
|
|
.message.sent .content-wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.message .sender-name {
|
|
font-size: 12px;
|
|
color: #666;
|
|
margin-bottom: 4px;
|
|
font-weight: 500;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.message .bubble {
|
|
background: white;
|
|
padding: 10px 14px;
|
|
border-radius: 12px;
|
|
word-wrap: break-word;
|
|
word-break: break-word;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: break-word;
|
|
position: relative;
|
|
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
|
transition: box-shadow 0.2s;
|
|
max-width: 100%;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.message .bubble:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.12);
|
|
}
|
|
|
|
.message.sent .bubble {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
|
|
}
|
|
|
|
.message .time {
|
|
font-size: 11px;
|
|
color: #999;
|
|
margin-top: 4px;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.message.sent .time {
|
|
text-align: right;
|
|
}
|
|
|
|
/* 聊天记录引用 */
|
|
.chat-records {
|
|
margin-top: 8px;
|
|
padding: 8px 10px;
|
|
background: rgba(0,0,0,0.04);
|
|
border-radius: 8px;
|
|
border-left: 3px solid #667eea;
|
|
}
|
|
|
|
.message.sent .chat-records {
|
|
background: rgba(255,255,255,0.15);
|
|
border-left-color: rgba(255,255,255,0.6);
|
|
}
|
|
|
|
.chat-records .title {
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
margin-bottom: 6px;
|
|
color: #667eea;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.message.sent .chat-records .title {
|
|
color: rgba(255,255,255,0.95);
|
|
}
|
|
|
|
.chat-record-item {
|
|
font-size: 12px;
|
|
padding: 6px 0;
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.chat-record-item:last-child {
|
|
border-bottom: none;
|
|
padding-bottom: 0;
|
|
}
|
|
|
|
.chat-record-item .record-sender {
|
|
font-weight: 600;
|
|
color: #333;
|
|
}
|
|
|
|
.message.sent .chat-record-item .record-sender {
|
|
color: rgba(255,255,255,0.95);
|
|
}
|
|
|
|
.chat-record-item .record-time {
|
|
font-size: 10px;
|
|
color: #999;
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.message.sent .chat-record-item .record-time {
|
|
color: rgba(255,255,255,0.75);
|
|
}
|
|
|
|
.chat-record-item .record-content {
|
|
margin-top: 2px;
|
|
color: #666;
|
|
line-height: 1.4;
|
|
word-wrap: break-word;
|
|
word-break: break-word;
|
|
white-space: pre-wrap;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.message.sent .chat-record-item .record-content {
|
|
color: rgba(255,255,255,0.9);
|
|
}
|
|
|
|
/* 页脚 */
|
|
.footer {
|
|
text-align: center;
|
|
padding: 24px;
|
|
color: #999;
|
|
font-size: 13px;
|
|
border-top: 2px solid #f0f0f0;
|
|
background: #fafafa;
|
|
}
|
|
|
|
/* 响应式设计 */
|
|
@media (max-width: 768px) {
|
|
body {
|
|
padding: 10px;
|
|
}
|
|
|
|
.container {
|
|
border-radius: 12px;
|
|
}
|
|
|
|
.header {
|
|
padding: 30px 20px;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 24px;
|
|
}
|
|
|
|
.controls {
|
|
padding: 15px;
|
|
}
|
|
|
|
.controls input[type="text"] {
|
|
min-width: 100%;
|
|
}
|
|
|
|
.controls .stats {
|
|
width: 100%;
|
|
justify-content: center;
|
|
margin-left: 0;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.scroll-container {
|
|
height: calc(100vh - 320px);
|
|
}
|
|
|
|
.messages {
|
|
padding: 20px 15px;
|
|
}
|
|
|
|
.message .content-wrapper {
|
|
max-width: 75%;
|
|
}
|
|
}
|
|
|
|
/* 打印样式 */
|
|
@media print {
|
|
body {
|
|
background: white;
|
|
padding: 0;
|
|
}
|
|
|
|
.container {
|
|
box-shadow: none;
|
|
border-radius: 0;
|
|
}
|
|
|
|
.controls {
|
|
display: none;
|
|
}
|
|
|
|
.message {
|
|
page-break-inside: avoid;
|
|
}
|
|
}`;
|
|
}
|
|
|
|
/**
|
|
* 生成数据 JS 文件(作为全局变量)
|
|
*/
|
|
static generateDataJs(exportData: HtmlExportData): string {
|
|
return `// CipherTalk 聊天记录数据
|
|
window.CHAT_DATA = ${JSON.stringify(exportData, null, 2)};`;
|
|
}
|
|
|
|
/**
|
|
* 生成外部 JavaScript 文件
|
|
*/
|
|
static generateJs(): string {
|
|
return `// CipherTalk 聊天记录导出应用
|
|
|
|
class ChatApp {
|
|
constructor() {
|
|
this.allData = window.CHAT_DATA;
|
|
this.filteredMessages = this.allData.messages;
|
|
|
|
// 无感加载配置
|
|
this.batchSize = 30; // 每次加载30条
|
|
this.loadedCount = 0; // 已加载数量
|
|
this.isLoading = false; // 是否正在加载
|
|
|
|
// DOM 元素
|
|
this.scrollContainer = null;
|
|
this.messagesContainer = null;
|
|
this.loadMoreObserver = null;
|
|
this.sentinel = null; // 哨兵元素
|
|
|
|
this.init();
|
|
}
|
|
|
|
init() {
|
|
try {
|
|
if (!this.allData) {
|
|
throw new Error('数据加载失败');
|
|
}
|
|
|
|
// 获取DOM元素
|
|
this.scrollContainer = document.getElementById('scrollContainer');
|
|
this.messagesContainer = document.getElementById('messagesContainer');
|
|
|
|
// 清空容器
|
|
this.messagesContainer.innerHTML = '';
|
|
|
|
// 绑定事件
|
|
this.bindEvents();
|
|
|
|
// 设置 Intersection Observer(必须在 loadMoreMessages 之前)
|
|
this.setupIntersectionObserver();
|
|
|
|
// 初始加载
|
|
this.loadMoreMessages();
|
|
|
|
// 更新统计信息
|
|
this.updateStats();
|
|
} catch (error) {
|
|
console.error('初始化失败:', error);
|
|
document.getElementById('messagesContainer').innerHTML =
|
|
\`<div class="error">加载失败: \${error.message}</div>\`;
|
|
}
|
|
}
|
|
|
|
bindEvents() {
|
|
// 搜索框回车
|
|
const searchInput = document.getElementById('searchInput');
|
|
searchInput.addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') {
|
|
this.searchMessages();
|
|
}
|
|
});
|
|
}
|
|
|
|
setupIntersectionObserver() {
|
|
// 创建哨兵元素
|
|
this.sentinel = document.createElement('div');
|
|
this.sentinel.className = 'message-placeholder';
|
|
this.sentinel.textContent = '加载中...';
|
|
this.sentinel.style.display = 'none';
|
|
|
|
// 创建 Intersection Observer
|
|
this.loadMoreObserver = new IntersectionObserver((entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting && !this.isLoading) {
|
|
this.loadMoreMessages();
|
|
}
|
|
});
|
|
}, {
|
|
root: this.scrollContainer,
|
|
rootMargin: '200px', // 提前200px开始加载
|
|
threshold: 0.1
|
|
});
|
|
}
|
|
|
|
loadMoreMessages() {
|
|
if (this.isLoading) return;
|
|
if (this.loadedCount >= this.filteredMessages.length) {
|
|
// 所有消息已加载完毕
|
|
if (this.sentinel && this.sentinel.parentNode) {
|
|
this.sentinel.remove();
|
|
}
|
|
return;
|
|
}
|
|
|
|
this.isLoading = true;
|
|
|
|
// 计算本次加载的范围
|
|
const start = this.loadedCount;
|
|
const end = Math.min(start + this.batchSize, this.filteredMessages.length);
|
|
const batch = this.filteredMessages.slice(start, end);
|
|
|
|
// 创建文档片段
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
// 渲染消息
|
|
batch.forEach(msg => {
|
|
const messageElement = this.createMessageElement(msg);
|
|
fragment.appendChild(messageElement);
|
|
});
|
|
|
|
// 移除旧的哨兵
|
|
if (this.sentinel && this.sentinel.parentNode) {
|
|
this.sentinel.remove();
|
|
}
|
|
|
|
// 添加消息到容器
|
|
this.messagesContainer.appendChild(fragment);
|
|
|
|
// 更新已加载数量
|
|
this.loadedCount = end;
|
|
|
|
// 如果还有更多消息,添加哨兵
|
|
if (this.loadedCount < this.filteredMessages.length) {
|
|
this.sentinel.style.display = 'flex';
|
|
this.messagesContainer.appendChild(this.sentinel);
|
|
|
|
// 观察哨兵
|
|
this.loadMoreObserver.observe(this.sentinel);
|
|
}
|
|
|
|
this.isLoading = false;
|
|
this.updateStats();
|
|
}
|
|
|
|
createMessageElement(msg) {
|
|
const div = document.createElement('div');
|
|
div.className = msg.isSend ? 'message sent' : 'message';
|
|
div.innerHTML = this.renderMessage(msg);
|
|
return div;
|
|
}
|
|
|
|
renderMessage(msg) {
|
|
const member = this.allData.members.find(m => m.id === msg.sender);
|
|
const senderName = member ? member.name : msg.senderName;
|
|
const avatar = member && member.avatar ? member.avatar : null;
|
|
const time = new Date(msg.timestamp * 1000).toLocaleString('zh-CN');
|
|
|
|
// 生成头像
|
|
let avatarHtml = '';
|
|
if (avatar) {
|
|
avatarHtml = \`<img src="\${this.escapeHtml(avatar)}" alt="\${this.escapeHtml(senderName)}" onerror="this.style.display='none';this.parentElement.textContent='\${senderName.charAt(0).toUpperCase()}'" />\`;
|
|
} else {
|
|
avatarHtml = senderName.charAt(0).toUpperCase();
|
|
}
|
|
|
|
// 生成消息内容
|
|
let contentHtml = msg.content ? this.escapeHtml(msg.content) : '<em style="opacity:0.6">无内容</em>';
|
|
|
|
// 如果有聊天记录,添加聊天记录展示
|
|
let chatRecordsHtml = '';
|
|
if (msg.chatRecords && msg.chatRecords.length > 0) {
|
|
chatRecordsHtml = '<div class="chat-records">';
|
|
chatRecordsHtml += '<div class="title">📋 聊天记录引用</div>';
|
|
for (const record of msg.chatRecords) {
|
|
chatRecordsHtml += \`
|
|
<div class="chat-record-item">
|
|
<div>
|
|
<span class="record-sender">\${this.escapeHtml(record.senderDisplayName)}</span>
|
|
<span class="record-time">\${this.escapeHtml(record.formattedTime)}</span>
|
|
</div>
|
|
<div class="record-content">\${this.escapeHtml(record.content)}</div>
|
|
</div>
|
|
\`;
|
|
}
|
|
chatRecordsHtml += '</div>';
|
|
}
|
|
|
|
return \`
|
|
<div class="avatar">\${avatarHtml}</div>
|
|
<div class="content-wrapper">
|
|
<div class="sender-name">\${this.escapeHtml(senderName)}</div>
|
|
<div class="bubble">
|
|
\${contentHtml}
|
|
\${chatRecordsHtml}
|
|
</div>
|
|
<div class="time">\${time}</div>
|
|
</div>
|
|
\`;
|
|
}
|
|
|
|
searchMessages() {
|
|
const keyword = document.getElementById('searchInput').value.trim().toLowerCase();
|
|
if (!keyword) {
|
|
this.filteredMessages = this.allData.messages;
|
|
} else {
|
|
this.filteredMessages = this.allData.messages.filter(msg => {
|
|
// 搜索消息内容
|
|
if (msg.content && msg.content.toLowerCase().includes(keyword)) {
|
|
return true;
|
|
}
|
|
// 搜索发送者名称
|
|
const member = this.allData.members.find(m => m.id === msg.sender);
|
|
const senderName = member ? member.name : msg.senderName;
|
|
if (senderName.toLowerCase().includes(keyword)) {
|
|
return true;
|
|
}
|
|
// 搜索聊天记录内容
|
|
if (msg.chatRecords) {
|
|
for (const record of msg.chatRecords) {
|
|
if (record.content.toLowerCase().includes(keyword) ||
|
|
record.senderDisplayName.toLowerCase().includes(keyword)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
}
|
|
|
|
// 重置并重新加载
|
|
this.reset();
|
|
}
|
|
|
|
clearSearch() {
|
|
document.getElementById('searchInput').value = '';
|
|
this.filteredMessages = this.allData.messages;
|
|
this.reset();
|
|
}
|
|
|
|
reset() {
|
|
// 停止观察
|
|
if (this.loadMoreObserver && this.sentinel && this.sentinel.parentNode) {
|
|
this.loadMoreObserver.unobserve(this.sentinel);
|
|
}
|
|
|
|
// 清空容器
|
|
this.messagesContainer.innerHTML = '';
|
|
|
|
// 重置状态
|
|
this.loadedCount = 0;
|
|
this.isLoading = false;
|
|
|
|
// 滚动到顶部
|
|
this.scrollContainer.scrollTop = 0;
|
|
|
|
// 重新设置观察器(必须在 loadMoreMessages 之前)
|
|
this.setupIntersectionObserver();
|
|
|
|
// 重新加载
|
|
this.loadMoreMessages();
|
|
}
|
|
|
|
updateStats() {
|
|
const totalCount = this.filteredMessages.length;
|
|
document.getElementById('messageStats').textContent = \`共 \${totalCount} 条消息\`;
|
|
document.getElementById('loadedStats').textContent = \`已加载 \${this.loadedCount} 条\`;
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
// 初始化应用
|
|
const app = new ChatApp();`;
|
|
}
|
|
|
|
/**
|
|
* 生成数据 JSON 文件
|
|
*/
|
|
static generateDataJson(exportData: HtmlExportData): string {
|
|
return JSON.stringify(exportData, null, 2);
|
|
}
|
|
|
|
/**
|
|
* HTML 转义
|
|
*/
|
|
private static escapeHtml(text: string): string {
|
|
const map: Record<string, string> = {
|
|
'&': '&',
|
|
'<': '<',
|
|
'>': '>',
|
|
'"': '"',
|
|
"'": '''
|
|
}
|
|
return text.replace(/[&<>"']/g, m => map[m])
|
|
}
|
|
} |