|
@@ -23,25 +23,24 @@ const parseDate = (dateStr) => {
|
|
|
// 处理形如 "2025/6/10 17:37:21" 的日期格式
|
|
// 处理形如 "2025/6/10 17:37:21" 的日期格式
|
|
|
const parts = dateStr.split(' ')
|
|
const parts = dateStr.split(' ')
|
|
|
if (parts.length !== 2) return new Date(0)
|
|
if (parts.length !== 2) return new Date(0)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const dateParts = parts[0].split('/')
|
|
const dateParts = parts[0].split('/')
|
|
|
const timeParts = parts[1].split(':')
|
|
const timeParts = parts[1].split(':')
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (dateParts.length !== 3 || timeParts.length !== 3) return new Date(0)
|
|
if (dateParts.length !== 3 || timeParts.length !== 3) return new Date(0)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const year = parseInt(dateParts[0])
|
|
const year = parseInt(dateParts[0])
|
|
|
const month = parseInt(dateParts[1]) - 1 // 月份从0开始
|
|
const month = parseInt(dateParts[1]) - 1 // 月份从0开始
|
|
|
const day = parseInt(dateParts[2])
|
|
const day = parseInt(dateParts[2])
|
|
|
const hour = parseInt(timeParts[0])
|
|
const hour = parseInt(timeParts[0])
|
|
|
const minute = parseInt(timeParts[1])
|
|
const minute = parseInt(timeParts[1])
|
|
|
const second = parseInt(timeParts[2])
|
|
const second = parseInt(timeParts[2])
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 检查是否为有效日期值
|
|
// 检查是否为有效日期值
|
|
|
- if (isNaN(year) || isNaN(month) || isNaN(day) ||
|
|
|
|
|
- isNaN(hour) || isNaN(minute) || isNaN(second)) {
|
|
|
|
|
|
|
+ if (isNaN(year) || isNaN(month) || isNaN(day) || isNaN(hour) || isNaN(minute) || isNaN(second)) {
|
|
|
return new Date(0)
|
|
return new Date(0)
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return new Date(year, month, day, hour, minute, second)
|
|
return new Date(year, month, day, hour, minute, second)
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
console.error('日期解析错误:', e, dateStr)
|
|
console.error('日期解析错误:', e, dateStr)
|
|
@@ -52,25 +51,25 @@ const parseDate = (dateStr) => {
|
|
|
// 创建导出摘要数据
|
|
// 创建导出摘要数据
|
|
|
const createExportSummary = () => {
|
|
const createExportSummary = () => {
|
|
|
if (!exportInfo.value) return
|
|
if (!exportInfo.value) return
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const { user, stats, date } = exportInfo.value
|
|
const { user, stats, date } = exportInfo.value
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 创建导出摘要数据对象
|
|
// 创建导出摘要数据对象
|
|
|
exportSummaryData.value = {
|
|
exportSummaryData.value = {
|
|
|
- name: "导出摘要",
|
|
|
|
|
|
|
+ name: '导出摘要',
|
|
|
messages: [
|
|
messages: [
|
|
|
{
|
|
{
|
|
|
id: 1,
|
|
id: 1,
|
|
|
- title: "Telegram 聊天记录导出",
|
|
|
|
|
|
|
+ title: 'Telegram 聊天记录导出',
|
|
|
exportTime: date.split('T')[0].replace(/-/g, '/') + ' ' + date.split('T')[1].substring(0, 8),
|
|
exportTime: date.split('T')[0].replace(/-/g, '/') + ' ' + date.split('T')[1].substring(0, 8),
|
|
|
userName: user.firstName + ' ' + user.lastName,
|
|
userName: user.firstName + ' ' + user.lastName,
|
|
|
userHandle: user.username,
|
|
userHandle: user.username,
|
|
|
userId: user.id,
|
|
userId: user.id,
|
|
|
dialogCount: stats.dialogs,
|
|
dialogCount: stats.dialogs,
|
|
|
imageCount: stats.images,
|
|
imageCount: stats.images,
|
|
|
- mediaDownload: stats.skipMediaDownload ? "未下载" : "已下载",
|
|
|
|
|
- text: "",
|
|
|
|
|
- mediaType: "None",
|
|
|
|
|
|
|
+ mediaDownload: stats.skipMediaDownload ? '未下载' : '已下载',
|
|
|
|
|
+ text: '',
|
|
|
|
|
+ mediaType: 'None',
|
|
|
date: date.split('T')[0].replace(/-/g, '/') + ' ' + date.split('T')[1].substring(0, 8),
|
|
date: date.split('T')[0].replace(/-/g, '/') + ' ' + date.split('T')[1].substring(0, 8),
|
|
|
imagePath: null
|
|
imagePath: null
|
|
|
}
|
|
}
|
|
@@ -92,10 +91,10 @@ const handleDragLeave = (e) => {
|
|
|
const handleDrop = async (e) => {
|
|
const handleDrop = async (e) => {
|
|
|
e.preventDefault()
|
|
e.preventDefault()
|
|
|
isDragOver.value = false
|
|
isDragOver.value = false
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const files = e.dataTransfer?.files
|
|
const files = e.dataTransfer?.files
|
|
|
if (!files || files.length === 0) return
|
|
if (!files || files.length === 0) return
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const file = files[0]
|
|
const file = files[0]
|
|
|
if (!file.name.endsWith('.zip')) {
|
|
if (!file.name.endsWith('.zip')) {
|
|
|
toast.add({
|
|
toast.add({
|
|
@@ -106,50 +105,49 @@ const handleDrop = async (e) => {
|
|
|
})
|
|
})
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
await processZipFile(file)
|
|
await processZipFile(file)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 处理ZIP文件
|
|
// 处理ZIP文件
|
|
|
const processZipFile = async (file) => {
|
|
const processZipFile = async (file) => {
|
|
|
isLoading.value = true
|
|
isLoading.value = true
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const zip = new JSZip()
|
|
const zip = new JSZip()
|
|
|
const zipDataResult = await zip.loadAsync(file)
|
|
const zipDataResult = await zip.loadAsync(file)
|
|
|
zipData.value = zipDataResult // 保存ZIP数据引用
|
|
zipData.value = zipDataResult // 保存ZIP数据引用
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 查找并解析export_info.json
|
|
// 查找并解析export_info.json
|
|
|
const exportInfoFile = zipDataResult.file('export_info.json')
|
|
const exportInfoFile = zipDataResult.file('export_info.json')
|
|
|
if (!exportInfoFile) {
|
|
if (!exportInfoFile) {
|
|
|
throw new Error('未找到export_info.json文件')
|
|
throw new Error('未找到export_info.json文件')
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const exportInfoContent = await exportInfoFile.async('string')
|
|
const exportInfoContent = await exportInfoFile.async('string')
|
|
|
exportInfo.value = JSON.parse(exportInfoContent)
|
|
exportInfo.value = JSON.parse(exportInfoContent)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 查找并解析telegram_export.json
|
|
// 查找并解析telegram_export.json
|
|
|
const exportDataFile = zipDataResult.file('telegram_export.json')
|
|
const exportDataFile = zipDataResult.file('telegram_export.json')
|
|
|
if (!exportDataFile) {
|
|
if (!exportDataFile) {
|
|
|
throw new Error('未找到telegram_export.json文件')
|
|
throw new Error('未找到telegram_export.json文件')
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const exportDataContent = await exportDataFile.async('string')
|
|
const exportDataContent = await exportDataFile.async('string')
|
|
|
exportData.value = JSON.parse(exportDataContent)
|
|
exportData.value = JSON.parse(exportDataContent)
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 创建导出摘要
|
|
// 创建导出摘要
|
|
|
createExportSummary()
|
|
createExportSummary()
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 默认选择导出摘要会话
|
|
// 默认选择导出摘要会话
|
|
|
selectChat('export_summary')
|
|
selectChat('export_summary')
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
toast.add({
|
|
toast.add({
|
|
|
severity: 'success',
|
|
severity: 'success',
|
|
|
summary: '成功',
|
|
summary: '成功',
|
|
|
detail: '文件解析成功',
|
|
detail: '文件解析成功',
|
|
|
life: 3000
|
|
life: 3000
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error('解析文件失败:', error)
|
|
console.error('解析文件失败:', error)
|
|
|
toast.add({
|
|
toast.add({
|
|
@@ -173,9 +171,9 @@ const selectChat = async (chatId) => {
|
|
|
// 获取聊天列表
|
|
// 获取聊天列表
|
|
|
const getChatList = () => {
|
|
const getChatList = () => {
|
|
|
if (!exportData.value || !exportSummaryData.value) return []
|
|
if (!exportData.value || !exportSummaryData.value) return []
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const chats = []
|
|
const chats = []
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 添加导出摘要作为第一个会话
|
|
// 添加导出摘要作为第一个会话
|
|
|
chats.push({
|
|
chats.push({
|
|
|
chatId: 'export_summary',
|
|
chatId: 'export_summary',
|
|
@@ -183,7 +181,7 @@ const getChatList = () => {
|
|
|
messageCount: exportSummaryData.value.messages.length,
|
|
messageCount: exportSummaryData.value.messages.length,
|
|
|
isSummary: true
|
|
isSummary: true
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 按最新消息时间排序聊天,过滤掉export_summary避免重复
|
|
// 按最新消息时间排序聊天,过滤掉export_summary避免重复
|
|
|
const sortedChats = Object.entries(exportData.value)
|
|
const sortedChats = Object.entries(exportData.value)
|
|
|
.filter(([chatId]) => chatId !== 'export_summary') // 过滤掉export_summary
|
|
.filter(([chatId]) => chatId !== 'export_summary') // 过滤掉export_summary
|
|
@@ -193,7 +191,7 @@ const getChatList = () => {
|
|
|
return { chatId, chat, latestDate }
|
|
return { chatId, chat, latestDate }
|
|
|
})
|
|
})
|
|
|
.sort((a, b) => b.latestDate.getTime() - a.latestDate.getTime())
|
|
.sort((a, b) => b.latestDate.getTime() - a.latestDate.getTime())
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
sortedChats.forEach(({ chatId, chat }) => {
|
|
sortedChats.forEach(({ chatId, chat }) => {
|
|
|
chats.push({
|
|
chats.push({
|
|
|
chatId,
|
|
chatId,
|
|
@@ -202,7 +200,7 @@ const getChatList = () => {
|
|
|
isSummary: false
|
|
isSummary: false
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return chats
|
|
return chats
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -224,11 +222,11 @@ const getCurrentChatTitle = () => {
|
|
|
const getCurrentMessages = () => {
|
|
const getCurrentMessages = () => {
|
|
|
const chat = getCurrentChat()
|
|
const chat = getCurrentChat()
|
|
|
if (!chat) return []
|
|
if (!chat) return []
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (selectedChatId.value === 'export_summary') {
|
|
if (selectedChatId.value === 'export_summary') {
|
|
|
return chat.messages
|
|
return chat.messages
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 确保所有会话消息按日期排序(从旧到新)
|
|
// 确保所有会话消息按日期排序(从旧到新)
|
|
|
return [...chat.messages].sort((a, b) => {
|
|
return [...chat.messages].sort((a, b) => {
|
|
|
const dateA = parseDate(a.date)
|
|
const dateA = parseDate(a.date)
|
|
@@ -247,14 +245,14 @@ const escapeHtml = (html) => {
|
|
|
// 获取图片的base64数据
|
|
// 获取图片的base64数据
|
|
|
const getImageBase64 = async (imagePath) => {
|
|
const getImageBase64 = async (imagePath) => {
|
|
|
if (!zipData.value || !imagePath) return null
|
|
if (!zipData.value || !imagePath) return null
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const imageFile = zipData.value.file(imagePath)
|
|
const imageFile = zipData.value.file(imagePath)
|
|
|
if (!imageFile) {
|
|
if (!imageFile) {
|
|
|
console.warn('图片文件不存在:', imagePath)
|
|
console.warn('图片文件不存在:', imagePath)
|
|
|
return null
|
|
return null
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
const imageData = await imageFile.async('base64')
|
|
const imageData = await imageFile.async('base64')
|
|
|
return imageData
|
|
return imageData
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
@@ -266,10 +264,10 @@ const getImageBase64 = async (imagePath) => {
|
|
|
// 处理消息内容(同步版本,用于模板)
|
|
// 处理消息内容(同步版本,用于模板)
|
|
|
const processMessageContent = (message) => {
|
|
const processMessageContent = (message) => {
|
|
|
let content = ''
|
|
let content = ''
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
// 确保有时间显示,如果没有date字段则显示当前时间或默认值
|
|
// 确保有时间显示,如果没有date字段则显示当前时间或默认值
|
|
|
const messageTime = message.date || '未知时间'
|
|
const messageTime = message.date || '未知时间'
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
if (message.mediaType === 'Photo' && message.imagePath) {
|
|
if (message.mediaType === 'Photo' && message.imagePath) {
|
|
|
content += `
|
|
content += `
|
|
|
<div class="message-text">${escapeHtml(message.text)}</div>
|
|
<div class="message-text">${escapeHtml(message.text)}</div>
|
|
@@ -282,7 +280,7 @@ const processMessageContent = (message) => {
|
|
|
// 尝试提取URL
|
|
// 尝试提取URL
|
|
|
const urlMatch = message.text.match(/https?:\/\/[^\s]+/)
|
|
const urlMatch = message.text.match(/https?:\/\/[^\s]+/)
|
|
|
const url = urlMatch ? urlMatch[0] : message.text
|
|
const url = urlMatch ? urlMatch[0] : message.text
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
content += `
|
|
content += `
|
|
|
<div class="message-text">${escapeHtml(message.text)}</div>
|
|
<div class="message-text">${escapeHtml(message.text)}</div>
|
|
|
<div class="message-web-page">
|
|
<div class="message-web-page">
|
|
@@ -296,7 +294,7 @@ const processMessageContent = (message) => {
|
|
|
<div class="message-time">${messageTime}</div>
|
|
<div class="message-time">${messageTime}</div>
|
|
|
`
|
|
`
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return content
|
|
return content
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -304,7 +302,7 @@ const processMessageContent = (message) => {
|
|
|
const handleImagesAfterUpdate = async () => {
|
|
const handleImagesAfterUpdate = async () => {
|
|
|
await nextTick()
|
|
await nextTick()
|
|
|
const images = document.querySelectorAll('.message-image[data-image-path]')
|
|
const images = document.querySelectorAll('.message-image[data-image-path]')
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
for (const img of images) {
|
|
for (const img of images) {
|
|
|
const imagePath = img.dataset.imagePath
|
|
const imagePath = img.dataset.imagePath
|
|
|
if (imagePath && !img.src.includes('data:image')) {
|
|
if (imagePath && !img.src.includes('data:image')) {
|
|
@@ -335,7 +333,7 @@ const handleImagesAfterUpdate = async () => {
|
|
|
<template>
|
|
<template>
|
|
|
<div class="chat-records-container">
|
|
<div class="chat-records-container">
|
|
|
<!-- 拖拽区域 -->
|
|
<!-- 拖拽区域 -->
|
|
|
- <div
|
|
|
|
|
|
|
+ <div
|
|
|
v-if="!exportData"
|
|
v-if="!exportData"
|
|
|
ref="dropZoneRef"
|
|
ref="dropZoneRef"
|
|
|
class="drop-zone"
|
|
class="drop-zone"
|
|
@@ -360,7 +358,7 @@ const handleImagesAfterUpdate = async () => {
|
|
|
<div class="chat-viewer-header">
|
|
<div class="chat-viewer-header">
|
|
|
<h1 class="text-2xl font-bold">{{ getCurrentChatTitle() }}</h1>
|
|
<h1 class="text-2xl font-bold">{{ getCurrentChatTitle() }}</h1>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
<div class="chat-viewer-content">
|
|
<div class="chat-viewer-content">
|
|
|
<!-- 侧边栏 - 聊天列表 -->
|
|
<!-- 侧边栏 - 聊天列表 -->
|
|
|
<div class="chat-sidebar">
|
|
<div class="chat-sidebar">
|
|
@@ -369,7 +367,7 @@ const handleImagesAfterUpdate = async () => {
|
|
|
v-for="chat in getChatList()"
|
|
v-for="chat in getChatList()"
|
|
|
:key="chat.chatId"
|
|
:key="chat.chatId"
|
|
|
class="chat-item"
|
|
class="chat-item"
|
|
|
- :class="{ 'active': selectedChatId === chat.chatId }"
|
|
|
|
|
|
|
+ :class="{ active: selectedChatId === chat.chatId }"
|
|
|
@click="selectChat(chat.chatId)"
|
|
@click="selectChat(chat.chatId)"
|
|
|
>
|
|
>
|
|
|
<div class="chat-item-name">{{ chat.name }}</div>
|
|
<div class="chat-item-name">{{ chat.name }}</div>
|
|
@@ -385,8 +383,8 @@ const handleImagesAfterUpdate = async () => {
|
|
|
<div class="messages-container">
|
|
<div class="messages-container">
|
|
|
<!-- 导出摘要消息 -->
|
|
<!-- 导出摘要消息 -->
|
|
|
<div v-if="selectedChatId === 'export_summary'" class="export-summary">
|
|
<div v-if="selectedChatId === 'export_summary'" class="export-summary">
|
|
|
- <div
|
|
|
|
|
- v-for="message in getCurrentMessages()"
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="message in getCurrentMessages()"
|
|
|
:key="message.id"
|
|
:key="message.id"
|
|
|
class="message message-incoming export-summary-message"
|
|
class="message message-incoming export-summary-message"
|
|
|
>
|
|
>
|
|
@@ -420,13 +418,12 @@ const handleImagesAfterUpdate = async () => {
|
|
|
|
|
|
|
|
<!-- 普通聊天消息 -->
|
|
<!-- 普通聊天消息 -->
|
|
|
<div v-else class="messages">
|
|
<div v-else class="messages">
|
|
|
- <div
|
|
|
|
|
- v-for="message in getCurrentMessages()"
|
|
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="message in getCurrentMessages()"
|
|
|
:key="message.id"
|
|
:key="message.id"
|
|
|
class="message message-incoming"
|
|
class="message message-incoming"
|
|
|
v-html="processMessageContent(message)"
|
|
v-html="processMessageContent(message)"
|
|
|
- >
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ ></div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -637,16 +634,16 @@ const handleImagesAfterUpdate = async () => {
|
|
|
.chat-viewer-content {
|
|
.chat-viewer-content {
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
.chat-sidebar {
|
|
.chat-sidebar {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 200px;
|
|
height: 200px;
|
|
|
border-right: none;
|
|
border-right: none;
|
|
|
border-bottom: 1px solid #e5e7eb;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
.message {
|
|
.message {
|
|
|
max-width: 95%;
|
|
max-width: 95%;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
-</style>
|
|
|
|
|
|
|
+</style>
|