mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
feat: 一些适配
This commit is contained in:
@@ -18,6 +18,7 @@ import ExportPage from './pages/ExportPage'
|
||||
import VideoWindow from './pages/VideoWindow'
|
||||
import SnsPage from './pages/SnsPage'
|
||||
import ContactsPage from './pages/ContactsPage'
|
||||
import ChatHistoryPage from './pages/ChatHistoryPage'
|
||||
|
||||
import { useAppStore } from './stores/appStore'
|
||||
import { themes, useThemeStore, type ThemeId } from './stores/themeStore'
|
||||
@@ -49,6 +50,7 @@ function App() {
|
||||
const isAgreementWindow = location.pathname === '/agreement-window'
|
||||
const isOnboardingWindow = location.pathname === '/onboarding-window'
|
||||
const isVideoPlayerWindow = location.pathname === '/video-player-window'
|
||||
const isChatHistoryWindow = location.pathname.startsWith('/chat-history/')
|
||||
const [themeHydrated, setThemeHydrated] = useState(false)
|
||||
|
||||
// 锁定状态
|
||||
@@ -298,6 +300,11 @@ function App() {
|
||||
return <VideoWindow />
|
||||
}
|
||||
|
||||
// 独立聊天记录窗口
|
||||
if (isChatHistoryWindow) {
|
||||
return <ChatHistoryPage />
|
||||
}
|
||||
|
||||
// 主窗口 - 完整布局
|
||||
return (
|
||||
<div className="app-container">
|
||||
@@ -392,6 +399,7 @@ function App() {
|
||||
<Route path="/export" element={<ExportPage />} />
|
||||
<Route path="/sns" element={<SnsPage />} />
|
||||
<Route path="/contacts" element={<ContactsPage />} />
|
||||
<Route path="/chat-history/:sessionId/:messageId" element={<ChatHistoryPage />} />
|
||||
</Routes>
|
||||
</RouteGuard>
|
||||
</main>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import './TitleBar.scss'
|
||||
|
||||
function TitleBar() {
|
||||
interface TitleBarProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
function TitleBar({ title }: TitleBarProps = {}) {
|
||||
return (
|
||||
<div className="title-bar">
|
||||
<img src="./logo.png" alt="WeFlow" className="title-logo" />
|
||||
<span className="titles">WeFlow</span>
|
||||
<span className="titles">{title || 'WeFlow'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
132
src/pages/ChatHistoryPage.scss
Normal file
132
src/pages/ChatHistoryPage.scss
Normal file
@@ -0,0 +1,132 @@
|
||||
.chat-history-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--bg-primary);
|
||||
|
||||
.history-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
|
||||
.status-msg {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
|
||||
&.error {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
&.empty {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-tertiary);
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.sender {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.bubble {
|
||||
background: var(--bg-secondary);
|
||||
padding: 10px 14px;
|
||||
border-radius: 18px 18px 18px 4px;
|
||||
word-wrap: break-word;
|
||||
max-width: 100%;
|
||||
display: inline-block;
|
||||
|
||||
&.image-bubble {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.text-content {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: var(--text-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.media-content {
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 300px;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.media-tip {
|
||||
padding: 8px 12px;
|
||||
color: var(--text-tertiary);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.media-placeholder {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
padding: 4px 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
250
src/pages/ChatHistoryPage.tsx
Normal file
250
src/pages/ChatHistoryPage.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useParams, useLocation } from 'react-router-dom'
|
||||
import { ChatRecordItem } from '../types/models'
|
||||
import TitleBar from '../components/TitleBar'
|
||||
import './ChatHistoryPage.scss'
|
||||
|
||||
export default function ChatHistoryPage() {
|
||||
const params = useParams<{ sessionId: string; messageId: string }>()
|
||||
const location = useLocation()
|
||||
const [recordList, setRecordList] = useState<ChatRecordItem[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [title, setTitle] = useState('聊天记录')
|
||||
const [error, setError] = useState('')
|
||||
|
||||
// 简单的 XML 标签内容提取
|
||||
const extractXmlValue = (xml: string, tag: string): string => {
|
||||
const match = new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`).exec(xml)
|
||||
return match ? match[1] : ''
|
||||
}
|
||||
|
||||
// 简单的 HTML 实体解码
|
||||
const decodeHtmlEntities = (text?: string): string | undefined => {
|
||||
if (!text) return text
|
||||
return text
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
// 前端兜底解析合并转发聊天记录
|
||||
const parseChatHistory = (content: string): ChatRecordItem[] | undefined => {
|
||||
try {
|
||||
const type = extractXmlValue(content, 'type')
|
||||
if (type !== '19') return undefined
|
||||
|
||||
const match = /<recorditem>[\s\S]*?<!\[CDATA\[([\s\S]*?)\]\]>[\s\S]*?<\/recorditem>/.exec(content)
|
||||
if (!match) return undefined
|
||||
|
||||
const innerXml = match[1]
|
||||
const items: ChatRecordItem[] = []
|
||||
const itemRegex = /<dataitem\s+(.*?)>([\s\S]*?)<\/dataitem>/g
|
||||
let itemMatch: RegExpExecArray | null
|
||||
|
||||
while ((itemMatch = itemRegex.exec(innerXml)) !== null) {
|
||||
const attrs = itemMatch[1]
|
||||
const body = itemMatch[2]
|
||||
|
||||
const datatypeMatch = /datatype="(\d+)"/.exec(attrs)
|
||||
const datatype = datatypeMatch ? parseInt(datatypeMatch[1]) : 0
|
||||
|
||||
const sourcename = extractXmlValue(body, 'sourcename')
|
||||
const sourcetime = extractXmlValue(body, 'sourcetime')
|
||||
const sourceheadurl = extractXmlValue(body, 'sourceheadurl')
|
||||
const datadesc = extractXmlValue(body, 'datadesc')
|
||||
const datatitle = extractXmlValue(body, 'datatitle')
|
||||
const fileext = extractXmlValue(body, 'fileext')
|
||||
const datasize = parseInt(extractXmlValue(body, 'datasize') || '0')
|
||||
const messageuuid = extractXmlValue(body, 'messageuuid')
|
||||
|
||||
const dataurl = extractXmlValue(body, 'dataurl')
|
||||
const datathumburl = extractXmlValue(body, 'datathumburl') || extractXmlValue(body, 'thumburl')
|
||||
const datacdnurl = extractXmlValue(body, 'datacdnurl') || extractXmlValue(body, 'cdnurl')
|
||||
const aeskey = extractXmlValue(body, 'aeskey') || extractXmlValue(body, 'qaeskey')
|
||||
const md5 = extractXmlValue(body, 'md5') || extractXmlValue(body, 'datamd5')
|
||||
const imgheight = parseInt(extractXmlValue(body, 'imgheight') || '0')
|
||||
const imgwidth = parseInt(extractXmlValue(body, 'imgwidth') || '0')
|
||||
const duration = parseInt(extractXmlValue(body, 'duration') || '0')
|
||||
|
||||
items.push({
|
||||
datatype,
|
||||
sourcename,
|
||||
sourcetime,
|
||||
sourceheadurl,
|
||||
datadesc: decodeHtmlEntities(datadesc),
|
||||
datatitle: decodeHtmlEntities(datatitle),
|
||||
fileext,
|
||||
datasize,
|
||||
messageuuid,
|
||||
dataurl: decodeHtmlEntities(dataurl),
|
||||
datathumburl: decodeHtmlEntities(datathumburl),
|
||||
datacdnurl: decodeHtmlEntities(datacdnurl),
|
||||
aeskey: decodeHtmlEntities(aeskey),
|
||||
md5,
|
||||
imgheight,
|
||||
imgwidth,
|
||||
duration
|
||||
})
|
||||
}
|
||||
|
||||
return items.length > 0 ? items : undefined
|
||||
} catch (e) {
|
||||
console.error('前端解析聊天记录失败:', e)
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
// 统一从路由参数或 pathname 中解析 sessionId / messageId
|
||||
const getIds = () => {
|
||||
const sessionId = params.sessionId || ''
|
||||
const messageId = params.messageId || ''
|
||||
|
||||
if (sessionId && messageId) {
|
||||
return { sid: sessionId, mid: messageId }
|
||||
}
|
||||
|
||||
// 独立窗口场景下没有 Route 包裹,用 pathname 手动解析
|
||||
const match = /^\/chat-history\/([^/]+)\/([^/]+)/.exec(location.pathname)
|
||||
if (match) {
|
||||
return { sid: match[1], mid: match[2] }
|
||||
}
|
||||
|
||||
return { sid: '', mid: '' }
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
const { sid, mid } = getIds()
|
||||
if (!sid || !mid) {
|
||||
setError('无效的聊天记录链接')
|
||||
setLoading(false)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const result = await window.electronAPI.chat.getMessage(sid, parseInt(mid, 10))
|
||||
if (result.success && result.message) {
|
||||
const msg = result.message
|
||||
// 优先使用后端解析好的列表
|
||||
let records: ChatRecordItem[] | undefined = msg.chatRecordList
|
||||
|
||||
// 如果后端没有解析到,则在前端兜底解析一次
|
||||
if ((!records || records.length === 0) && msg.content) {
|
||||
records = parseChatHistory(msg.content) || []
|
||||
}
|
||||
|
||||
if (records && records.length > 0) {
|
||||
setRecordList(records)
|
||||
const match = /<title>(.*?)<\/title>/.exec(msg.content || '')
|
||||
if (match) setTitle(match[1])
|
||||
} else {
|
||||
setError('暂时无法解析这条聊天记录')
|
||||
}
|
||||
} else {
|
||||
setError(result.error || '获取消息失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
setError('加载详情失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
loadData()
|
||||
}, [params.sessionId, params.messageId, location.pathname])
|
||||
|
||||
return (
|
||||
<div className="chat-history-page">
|
||||
<TitleBar title={title} />
|
||||
<div className="history-list">
|
||||
{loading ? (
|
||||
<div className="status-msg">加载中...</div>
|
||||
) : error ? (
|
||||
<div className="status-msg error">{error}</div>
|
||||
) : recordList.length === 0 ? (
|
||||
<div className="status-msg empty">暂无可显示的聊天记录</div>
|
||||
) : (
|
||||
recordList.map((item, i) => (
|
||||
<HistoryItem key={i} item={item} />
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function HistoryItem({ item }: { item: ChatRecordItem }) {
|
||||
// sourcetime 在合并转发里有两种格式:
|
||||
// 1) 时间戳(秒) 2) 已格式化的字符串 "2026-01-21 09:56:46"
|
||||
let time = ''
|
||||
if (item.sourcetime) {
|
||||
if (/^\d+$/.test(item.sourcetime)) {
|
||||
time = new Date(parseInt(item.sourcetime, 10) * 1000).toLocaleString()
|
||||
} else {
|
||||
time = item.sourcetime
|
||||
}
|
||||
}
|
||||
|
||||
const renderContent = () => {
|
||||
if (item.datatype === 1) {
|
||||
// 文本消息
|
||||
return <div className="text-content">{item.datadesc || ''}</div>
|
||||
}
|
||||
if (item.datatype === 3) {
|
||||
// 图片
|
||||
const src = item.datathumburl || item.datacdnurl
|
||||
if (src) {
|
||||
return (
|
||||
<div className="media-content">
|
||||
<img
|
||||
src={src}
|
||||
alt="图片"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement
|
||||
target.style.display = 'none'
|
||||
const placeholder = document.createElement('div')
|
||||
placeholder.className = 'media-tip'
|
||||
placeholder.textContent = '图片无法加载'
|
||||
target.parentElement?.appendChild(placeholder)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <div className="media-placeholder">[图片]</div>
|
||||
}
|
||||
if (item.datatype === 43) {
|
||||
return <div className="media-placeholder">[视频] {item.datatitle}</div>
|
||||
}
|
||||
if (item.datatype === 34) {
|
||||
return <div className="media-placeholder">[语音] {item.duration ? (item.duration / 1000).toFixed(0) + '"' : ''}</div>
|
||||
}
|
||||
// Fallback
|
||||
return <div className="text-content">{item.datadesc || item.datatitle || '[不支持的消息类型]'}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="history-item">
|
||||
<div className="avatar">
|
||||
{item.sourceheadurl ? (
|
||||
<img src={item.sourceheadurl} alt="" referrerPolicy="no-referrer" />
|
||||
) : (
|
||||
<div className="avatar-placeholder">
|
||||
{item.sourcename?.slice(0, 1)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="content-wrapper">
|
||||
<div className="header">
|
||||
<span className="sender">{item.sourcename || '未知发送者'}</span>
|
||||
<span className="time">{time}</span>
|
||||
</div>
|
||||
<div className={`bubble ${item.datatype === 3 ? 'image-bubble' : ''}`}>
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -834,92 +834,93 @@
|
||||
|
||||
// 链接卡片消息样式
|
||||
.link-message {
|
||||
cursor: pointer;
|
||||
width: 280px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-color);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
max-width: 300px;
|
||||
margin-top: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.link-header {
|
||||
padding: 10px 12px 6px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 12px;
|
||||
gap: 12px;
|
||||
gap: 8px;
|
||||
|
||||
.link-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.link-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.link-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 4px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.link-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
line-height: 1.4;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
flex-shrink: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 6px;
|
||||
.link-body {
|
||||
padding: 6px 12px 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
gap: 10px;
|
||||
|
||||
svg {
|
||||
opacity: 0.8;
|
||||
.link-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.link-thumb {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.link-thumb-placeholder {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 4px;
|
||||
background: var(--bg-tertiary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
color: var(--text-tertiary);
|
||||
|
||||
svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 适配发送出去的消息中的链接卡片
|
||||
.message-bubble.sent .link-message {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
.link-title {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.link-title,
|
||||
.link-desc {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.link-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2170,4 +2171,304 @@
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 名片消息
|
||||
.card-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
min-width: 200px;
|
||||
|
||||
.card-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// 通话消息
|
||||
.call-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
|
||||
svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件消息
|
||||
// 文件消息
|
||||
.file-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
min-width: 220px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// 发送的文件消息样式
|
||||
.message-bubble.sent .file-message {
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.1);
|
||||
|
||||
.file-name {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// 聊天记录消息 - 复用 link-message 基础样式
|
||||
.chat-record-message {
|
||||
cursor: pointer;
|
||||
|
||||
.link-header {
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.chat-record-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.chat-record-meta-line {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.chat-record-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-height: 70px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-record-item {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.source-name {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.chat-record-more {
|
||||
font-size: 12px;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.chat-record-desc {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chat-record-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: var(--primary-gradient);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// 小程序消息
|
||||
.miniapp-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
min-width: 200px;
|
||||
|
||||
.miniapp-icon {
|
||||
flex-shrink: 0;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.miniapp-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.miniapp-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.miniapp-label {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
}
|
||||
|
||||
// 转账消息卡片
|
||||
.transfer-message {
|
||||
width: 240px;
|
||||
background: linear-gradient(135deg, #f59e42 0%, #f5a742 100%);
|
||||
border-radius: 12px;
|
||||
padding: 14px 16px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
|
||||
&.received {
|
||||
background: linear-gradient(135deg, #b8b8b8 0%, #a8a8a8 100%);
|
||||
}
|
||||
|
||||
.transfer-icon {
|
||||
flex-shrink: 0;
|
||||
|
||||
svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
}
|
||||
|
||||
.transfer-info {
|
||||
flex: 1;
|
||||
color: white;
|
||||
|
||||
.transfer-amount {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 2px;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.transfer-memo {
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.transfer-label {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息中的特殊消息类型适配(除了文件和转账)
|
||||
.message-bubble.sent {
|
||||
.card-message,
|
||||
.chat-record-message,
|
||||
.miniapp-message {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
|
||||
.card-name,
|
||||
.miniapp-title,
|
||||
.source-name {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.card-label,
|
||||
.miniapp-label,
|
||||
.chat-record-item,
|
||||
.chat-record-meta-line,
|
||||
.chat-record-desc {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.card-icon,
|
||||
.miniapp-icon,
|
||||
.chat-record-icon {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.chat-record-more {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.call-message {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
|
||||
svg {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,15 @@ function isSystemMessage(localType: number): boolean {
|
||||
return SYSTEM_MESSAGE_TYPES.includes(localType)
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
interface ChatPageProps {
|
||||
// 保留接口以备将来扩展
|
||||
}
|
||||
@@ -1476,6 +1485,9 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
||||
const isImage = message.localType === 3
|
||||
const isVideo = message.localType === 43
|
||||
const isVoice = message.localType === 34
|
||||
const isCard = message.localType === 42
|
||||
const isCall = message.localType === 50
|
||||
const isType49 = message.localType === 49
|
||||
const isSent = message.isSend === 1
|
||||
const [senderAvatarUrl, setSenderAvatarUrl] = useState<string | undefined>(undefined)
|
||||
const [senderName, setSenderName] = useState<string | undefined>(undefined)
|
||||
@@ -2438,6 +2450,268 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
||||
)
|
||||
}
|
||||
|
||||
// 名片消息
|
||||
if (isCard) {
|
||||
const cardName = message.cardNickname || message.cardUsername || '未知联系人'
|
||||
return (
|
||||
<div className="card-message">
|
||||
<div className="card-icon">
|
||||
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="card-info">
|
||||
<div className="card-name">{cardName}</div>
|
||||
<div className="card-label">个人名片</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 通话消息
|
||||
if (isCall) {
|
||||
return (
|
||||
<div className="call-message">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" />
|
||||
</svg>
|
||||
<span>{message.parsedContent || '[通话]'}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 链接消息 (AppMessage)
|
||||
const isAppMsg = message.rawContent?.includes('<appmsg') || (message.parsedContent && message.parsedContent.includes('<appmsg'))
|
||||
|
||||
if (isAppMsg) {
|
||||
let title = '链接'
|
||||
let desc = ''
|
||||
let url = ''
|
||||
let appMsgType = ''
|
||||
|
||||
try {
|
||||
const content = message.rawContent || message.parsedContent || ''
|
||||
// 简单清理 XML 前缀(如 wxid:)
|
||||
const xmlContent = content.substring(content.indexOf('<msg>'))
|
||||
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(xmlContent, 'text/xml')
|
||||
|
||||
title = doc.querySelector('title')?.textContent || '链接'
|
||||
desc = doc.querySelector('des')?.textContent || ''
|
||||
url = doc.querySelector('url')?.textContent || ''
|
||||
appMsgType = doc.querySelector('appmsg > type')?.textContent || doc.querySelector('type')?.textContent || ''
|
||||
} catch (e) {
|
||||
console.error('解析 AppMsg 失败:', e)
|
||||
}
|
||||
|
||||
// 聊天记录 (type=19)
|
||||
if (appMsgType === '19') {
|
||||
const recordList = message.chatRecordList || []
|
||||
const displayTitle = title || '群聊的聊天记录'
|
||||
const metaText =
|
||||
recordList.length > 0
|
||||
? `共 ${recordList.length} 条聊天记录`
|
||||
: desc || '聊天记录'
|
||||
|
||||
const previewItems = recordList.slice(0, 4)
|
||||
|
||||
return (
|
||||
<div
|
||||
className="link-message chat-record-message"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
// 打开聊天记录窗口
|
||||
window.electronAPI.window.openChatHistoryWindow(session.username, message.localId)
|
||||
}}
|
||||
title="点击查看详细聊天记录"
|
||||
>
|
||||
<div className="link-header">
|
||||
<div className="link-title" title={displayTitle}>
|
||||
{displayTitle}
|
||||
</div>
|
||||
</div>
|
||||
<div className="link-body">
|
||||
<div className="chat-record-preview">
|
||||
{previewItems.length > 0 ? (
|
||||
<>
|
||||
<div className="chat-record-meta-line" title={metaText}>
|
||||
{metaText}
|
||||
</div>
|
||||
<div className="chat-record-list">
|
||||
{previewItems.map((item, i) => (
|
||||
<div key={i} className="chat-record-item">
|
||||
<span className="source-name">
|
||||
{item.sourcename ? `${item.sourcename}: ` : ''}
|
||||
</span>
|
||||
{item.datadesc || item.datatitle || '[媒体消息]'}
|
||||
</div>
|
||||
))}
|
||||
{recordList.length > previewItems.length && (
|
||||
<div className="chat-record-more">还有 {recordList.length - previewItems.length} 条…</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="chat-record-desc">
|
||||
{desc || '点击打开查看完整聊天记录'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="chat-record-icon">
|
||||
<MessageSquare size={18} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 文件消息 (type=6)
|
||||
if (appMsgType === '6') {
|
||||
const fileName = message.fileName || title || '文件'
|
||||
const fileSize = message.fileSize
|
||||
const fileExt = message.fileExt || fileName.split('.').pop()?.toLowerCase() || ''
|
||||
|
||||
// 根据扩展名选择图标
|
||||
const getFileIcon = () => {
|
||||
const archiveExts = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2']
|
||||
if (archiveExts.includes(fileExt)) {
|
||||
return (
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" />
|
||||
<polyline points="13 2 13 9 20 9" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="file-message">
|
||||
<div className="file-icon">
|
||||
{getFileIcon()}
|
||||
</div>
|
||||
<div className="file-info">
|
||||
<div className="file-name" title={fileName}>{fileName}</div>
|
||||
<div className="file-meta">
|
||||
{fileSize ? formatFileSize(fileSize) : ''}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 转账消息 (type=2000)
|
||||
if (appMsgType === '2000') {
|
||||
try {
|
||||
const content = message.rawContent || message.content || message.parsedContent || ''
|
||||
|
||||
// 添加调试日志
|
||||
console.log('[Transfer Debug] Raw content:', content.substring(0, 500))
|
||||
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(content, 'text/xml')
|
||||
|
||||
const feedesc = doc.querySelector('feedesc')?.textContent || ''
|
||||
const payMemo = doc.querySelector('pay_memo')?.textContent || ''
|
||||
const paysubtype = doc.querySelector('paysubtype')?.textContent || '1'
|
||||
|
||||
console.log('[Transfer Debug] Parsed:', { feedesc, payMemo, paysubtype, title })
|
||||
|
||||
// paysubtype: 1=待收款, 3=已收款
|
||||
const isReceived = paysubtype === '3'
|
||||
|
||||
// 如果 feedesc 为空,使用 title 作为降级
|
||||
const displayAmount = feedesc || title || '微信转账'
|
||||
|
||||
return (
|
||||
<div className={`transfer-message ${isReceived ? 'received' : ''}`}>
|
||||
<div className="transfer-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 40 40" fill="none">
|
||||
<circle cx="20" cy="20" r="18" stroke="white" strokeWidth="2" />
|
||||
<path d="M12 20h16M20 12l8 8-8 8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="transfer-info">
|
||||
<div className="transfer-amount">{displayAmount}</div>
|
||||
{payMemo && <div className="transfer-memo">{payMemo}</div>}
|
||||
<div className="transfer-label">{isReceived ? '已收款' : '微信转账'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
} catch (e) {
|
||||
console.error('[Transfer Debug] Parse error:', e)
|
||||
// 解析失败时的降级处理
|
||||
const feedesc = title || '微信转账'
|
||||
return (
|
||||
<div className="transfer-message">
|
||||
<div className="transfer-icon">
|
||||
<svg width="32" height="32" viewBox="0 0 40 40" fill="none">
|
||||
<circle cx="20" cy="20" r="18" stroke="white" strokeWidth="2" />
|
||||
<path d="M12 20h16M20 12l8 8-8 8" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="transfer-info">
|
||||
<div className="transfer-amount">{feedesc}</div>
|
||||
<div className="transfer-label">微信转账</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 小程序 (type=33/36)
|
||||
if (appMsgType === '33' || appMsgType === '36') {
|
||||
return (
|
||||
<div className="miniapp-message">
|
||||
<div className="miniapp-icon">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="miniapp-info">
|
||||
<div className="miniapp-title">{title}</div>
|
||||
<div className="miniapp-label">小程序</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 有 URL 的链接消息
|
||||
if (url) {
|
||||
return (
|
||||
<div
|
||||
className="link-message"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
if (window.electronAPI?.shell?.openExternal) {
|
||||
window.electronAPI.shell.openExternal(url)
|
||||
} else {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="link-header">
|
||||
<div className="link-title" title={title}>{title}</div>
|
||||
</div>
|
||||
<div className="link-body">
|
||||
<div className="link-desc" title={desc}>{desc}</div>
|
||||
<div className="link-thumb-placeholder">
|
||||
<Link size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 表情包消息
|
||||
if (isEmoji) {
|
||||
// ... (keep existing emoji logic)
|
||||
@@ -2492,67 +2766,6 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o
|
||||
)
|
||||
}
|
||||
|
||||
// 解析引用消息(Links / App Messages)
|
||||
// localType: 21474836529 corresponds to AppMessage which often contains links
|
||||
if (isLinkMessage) {
|
||||
try {
|
||||
// 清理内容:移除可能的 wxid 前缀,找到 XML 起始位置
|
||||
let contentToParse = message.rawContent || message.parsedContent || '';
|
||||
const xmlStartIndex = contentToParse.indexOf('<');
|
||||
if (xmlStartIndex >= 0) {
|
||||
contentToParse = contentToParse.substring(xmlStartIndex);
|
||||
}
|
||||
|
||||
// 处理 HTML 转义字符
|
||||
if (contentToParse.includes('<')) {
|
||||
contentToParse = contentToParse
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(contentToParse, "text/xml");
|
||||
const appMsg = doc.querySelector('appmsg');
|
||||
|
||||
if (appMsg) {
|
||||
const title = doc.querySelector('title')?.textContent || '未命名链接';
|
||||
const des = doc.querySelector('des')?.textContent || '无描述';
|
||||
const url = doc.querySelector('url')?.textContent || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="link-message"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (url) {
|
||||
// 优先使用 electron 接口打开外部浏览器
|
||||
if (window.electronAPI?.shell?.openExternal) {
|
||||
window.electronAPI.shell.openExternal(url);
|
||||
} else {
|
||||
window.open(url, '_blank');
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="link-header">
|
||||
<div className="link-content">
|
||||
<div className="link-title" title={title}>{title}</div>
|
||||
<div className="link-desc" title={des}>{des}</div>
|
||||
</div>
|
||||
<div className="link-icon">
|
||||
<Link size={24} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse app message', e);
|
||||
}
|
||||
}
|
||||
// 普通消息
|
||||
return <div className="bubble-content">{renderTextWithEmoji(cleanMessageContent(message.parsedContent))}</div>
|
||||
}
|
||||
|
||||
@@ -41,11 +41,12 @@ export const MESSAGE_TYPE_LABELS: Record<number, string> = {
|
||||
244813135921: '文本',
|
||||
3: '图片',
|
||||
34: '语音',
|
||||
42: '名片',
|
||||
43: '视频',
|
||||
47: '表情',
|
||||
48: '位置',
|
||||
49: '链接/文件',
|
||||
42: '名片',
|
||||
50: '通话',
|
||||
10000: '系统消息',
|
||||
}
|
||||
|
||||
|
||||
2
src/types/electron.d.ts
vendored
2
src/types/electron.d.ts
vendored
@@ -11,6 +11,7 @@ export interface ElectronAPI {
|
||||
setTitleBarOverlay: (options: { symbolColor: string }) => void
|
||||
openVideoPlayerWindow: (videoPath: string, videoWidth?: number, videoHeight?: number) => Promise<void>
|
||||
resizeToFitVideo: (videoWidth: number, videoHeight: number) => Promise<void>
|
||||
openChatHistoryWindow: (sessionId: string, messageId: number) => Promise<boolean>
|
||||
}
|
||||
config: {
|
||||
get: (key: string) => Promise<unknown>
|
||||
@@ -106,6 +107,7 @@ export interface ElectronAPI {
|
||||
getVoiceTranscript: (sessionId: string, msgId: string, createTime?: number) => Promise<{ success: boolean; transcript?: string; error?: string }>
|
||||
onVoiceTranscriptPartial: (callback: (payload: { msgId: string; text: string }) => void) => () => void
|
||||
execQuery: (kind: string, path: string | null, sql: string) => Promise<{ success: boolean; rows?: any[]; error?: string }>
|
||||
getMessage: (sessionId: string, localId: number) => Promise<{ success: boolean; message?: Message; error?: string }>
|
||||
}
|
||||
|
||||
image: {
|
||||
|
||||
@@ -53,8 +53,44 @@ export interface Message {
|
||||
// 引用消息
|
||||
quotedContent?: string
|
||||
quotedSender?: string
|
||||
// Type 49 细分字段
|
||||
linkTitle?: string // 链接/文件标题
|
||||
linkUrl?: string // 链接 URL
|
||||
linkThumb?: string // 链接缩略图
|
||||
fileName?: string // 文件名
|
||||
fileSize?: number // 文件大小
|
||||
fileExt?: string // 文件扩展名
|
||||
xmlType?: string // XML 中的 type 字段
|
||||
// 名片消息
|
||||
cardUsername?: string // 名片的微信ID
|
||||
cardNickname?: string // 名片的昵称
|
||||
// 聊天记录
|
||||
chatRecordTitle?: string // 聊天记录标题
|
||||
chatRecordList?: ChatRecordItem[] // 聊天记录列表
|
||||
}
|
||||
|
||||
// 聊天记录项
|
||||
export interface ChatRecordItem {
|
||||
datatype: number // 消息类型
|
||||
sourcename: string // 发送者
|
||||
sourcetime: string // 时间
|
||||
sourceheadurl?: string // 发送者头像
|
||||
datadesc?: string // 内容描述
|
||||
datatitle?: string // 标题
|
||||
fileext?: string // 文件扩展名
|
||||
datasize?: number // 文件大小
|
||||
messageuuid?: string // 消息UUID
|
||||
dataurl?: string // 数据URL
|
||||
datathumburl?: string // 缩略图URL
|
||||
datacdnurl?: string // CDN URL
|
||||
aeskey?: string // AES密钥
|
||||
md5?: string // MD5
|
||||
imgheight?: number // 图片高度
|
||||
imgwidth?: number // 图片宽度
|
||||
duration?: number // 时长(毫秒)
|
||||
}
|
||||
|
||||
|
||||
// 分析数据
|
||||
export interface AnalyticsData {
|
||||
totalMessages: number
|
||||
|
||||
Reference in New Issue
Block a user