mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
Refine HTML export layout and theming
This commit is contained in:
@@ -475,6 +475,33 @@ class ExportService {
|
|||||||
return this.escapeHtml(value).replace(/\r?\n/g, '<br />')
|
return this.escapeHtml(value).replace(/\r?\n/g, '<br />')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private formatHtmlMessageText(content: string, localType: number): string {
|
||||||
|
if (!content) return ''
|
||||||
|
|
||||||
|
if (localType === 49) {
|
||||||
|
const typeMatch = /<type>(\d+)<\/type>/i.exec(content)
|
||||||
|
const subType = typeMatch ? parseInt(typeMatch[1], 10) : 0
|
||||||
|
const title = this.extractXmlValue(content, 'title') || this.extractXmlValue(content, 'appname')
|
||||||
|
if (subType === 6) {
|
||||||
|
const fileName = this.extractXmlValue(content, 'filename') || title || '文件'
|
||||||
|
return `[文件] ${fileName}`.trim()
|
||||||
|
}
|
||||||
|
if (subType === 33 || subType === 36) {
|
||||||
|
const appName = this.extractXmlValue(content, 'appname')
|
||||||
|
const miniTitle = title || appName || '小程序'
|
||||||
|
return `[小程序] ${miniTitle}`.trim()
|
||||||
|
}
|
||||||
|
return title || '[链接]'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (localType === 42) {
|
||||||
|
const nickname = this.extractXmlValue(content, 'nickname')
|
||||||
|
return nickname ? `[名片] ${nickname}` : '[名片]'
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.parseMessageContent(content, localType) || ''
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出媒体文件到指定目录
|
* 导出媒体文件到指定目录
|
||||||
*/
|
*/
|
||||||
@@ -2306,16 +2333,21 @@ class ExportService {
|
|||||||
const timeText = this.formatTimestamp(msg.createTime)
|
const timeText = this.formatTimestamp(msg.createTime)
|
||||||
const typeName = this.getMessageTypeName(msg.localType)
|
const typeName = this.getMessageTypeName(msg.localType)
|
||||||
|
|
||||||
let textContent = this.parseMessageContent(msg.content, msg.localType) || ''
|
let textContent = this.formatHtmlMessageText(msg.content, msg.localType)
|
||||||
if (msg.localType === 34 && useVoiceTranscript) {
|
if (msg.localType === 34 && useVoiceTranscript) {
|
||||||
textContent = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]'
|
textContent = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]'
|
||||||
}
|
}
|
||||||
|
if (mediaItem && (msg.localType === 3 || msg.localType === 43 || msg.localType === 47)) {
|
||||||
|
textContent = ''
|
||||||
|
}
|
||||||
|
|
||||||
let mediaHtml = ''
|
let mediaHtml = ''
|
||||||
if (mediaItem?.kind === 'image') {
|
if (mediaItem?.kind === 'image') {
|
||||||
mediaHtml = `<img class="message-media image" src="${this.escapeAttribute(encodeURI(mediaItem.relativePath))}" alt="${this.escapeAttribute(typeName)}" />`
|
const mediaPath = this.escapeAttribute(encodeURI(mediaItem.relativePath))
|
||||||
|
mediaHtml = `<img class="message-media image previewable" src="${mediaPath}" data-full="${mediaPath}" alt="${this.escapeAttribute(typeName)}" />`
|
||||||
} else if (mediaItem?.kind === 'emoji') {
|
} else if (mediaItem?.kind === 'emoji') {
|
||||||
mediaHtml = `<img class="message-media emoji" src="${this.escapeAttribute(encodeURI(mediaItem.relativePath))}" alt="${this.escapeAttribute(typeName)}" />`
|
const mediaPath = this.escapeAttribute(encodeURI(mediaItem.relativePath))
|
||||||
|
mediaHtml = `<img class="message-media emoji previewable" src="${mediaPath}" data-full="${mediaPath}" alt="${this.escapeAttribute(typeName)}" />`
|
||||||
} else if (mediaItem?.kind === 'voice') {
|
} else if (mediaItem?.kind === 'voice') {
|
||||||
mediaHtml = `<audio class="message-media audio" controls src="${this.escapeAttribute(encodeURI(mediaItem.relativePath))}"></audio>`
|
mediaHtml = `<audio class="message-media audio" controls src="${this.escapeAttribute(encodeURI(mediaItem.relativePath))}"></audio>`
|
||||||
} else if (mediaItem?.kind === 'video') {
|
} else if (mediaItem?.kind === 'video') {
|
||||||
@@ -2329,7 +2361,9 @@ class ExportService {
|
|||||||
const senderHtml = isGroup
|
const senderHtml = isGroup
|
||||||
? `<div class="sender-name">${this.escapeHtml(senderName)}</div>`
|
? `<div class="sender-name">${this.escapeHtml(senderName)}</div>`
|
||||||
: ''
|
: ''
|
||||||
|
const timeHtml = `<div class="message-time">${this.escapeHtml(timeText)}</div>`
|
||||||
const messageBody = `
|
const messageBody = `
|
||||||
|
${timeHtml}
|
||||||
${senderHtml}
|
${senderHtml}
|
||||||
<div class="message-content">
|
<div class="message-content">
|
||||||
${mediaHtml}
|
${mediaHtml}
|
||||||
@@ -2339,7 +2373,6 @@ class ExportService {
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="message ${isSenderMe ? 'sent' : 'received'}" data-timestamp="${msg.createTime}" data-index="${index + 1}">
|
<div class="message ${isSenderMe ? 'sent' : 'received'}" data-timestamp="${msg.createTime}" data-index="${index + 1}">
|
||||||
<div class="message-time">${this.escapeHtml(timeText)}</div>
|
|
||||||
<div class="message-row">
|
<div class="message-row">
|
||||||
<div class="avatar">${avatarHtml}</div>
|
<div class="avatar">${avatarHtml}</div>
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
@@ -2437,6 +2470,7 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.control input,
|
.control input,
|
||||||
|
.control select,
|
||||||
.control button {
|
.control button {
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -2481,9 +2515,9 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.message-time {
|
.message-time {
|
||||||
text-align: center;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-row {
|
.message-row {
|
||||||
@@ -2575,6 +2609,72 @@ class ExportService {
|
|||||||
width: 260px;
|
width: 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.image-preview {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(15, 23, 42, 0.7);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview.active {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-preview img {
|
||||||
|
max-width: min(90vw, 1200px);
|
||||||
|
max-height: 90vh;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
background: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="cloud-dancer"] {
|
||||||
|
--accent: #6b8cff;
|
||||||
|
--sent: #e0e7ff;
|
||||||
|
--received: #ffffff;
|
||||||
|
--border: #d8e0f7;
|
||||||
|
--bg: #f6f7fb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="corundum-blue"] {
|
||||||
|
--accent: #2563eb;
|
||||||
|
--sent: #dbeafe;
|
||||||
|
--received: #ffffff;
|
||||||
|
--border: #c7d2fe;
|
||||||
|
--bg: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="kiwi-green"] {
|
||||||
|
--accent: #16a34a;
|
||||||
|
--sent: #dcfce7;
|
||||||
|
--received: #ffffff;
|
||||||
|
--border: #bbf7d0;
|
||||||
|
--bg: #f0fdf4;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="spicy-red"] {
|
||||||
|
--accent: #e11d48;
|
||||||
|
--sent: #ffe4e6;
|
||||||
|
--received: #ffffff;
|
||||||
|
--border: #fecdd3;
|
||||||
|
--bg: #fff1f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-theme="teal-water"] {
|
||||||
|
--accent: #0f766e;
|
||||||
|
--sent: #ccfbf1;
|
||||||
|
--received: #ffffff;
|
||||||
|
--border: #99f6e4;
|
||||||
|
--bg: #f0fdfa;
|
||||||
|
}
|
||||||
|
|
||||||
.highlight {
|
.highlight {
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--accent);
|
||||||
outline-offset: 4px;
|
outline-offset: 4px;
|
||||||
@@ -2606,6 +2706,16 @@ class ExportService {
|
|||||||
<label for="timeInput">按时间跳转</label>
|
<label for="timeInput">按时间跳转</label>
|
||||||
<input id="timeInput" type="datetime-local" />
|
<input id="timeInput" type="datetime-local" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="control">
|
||||||
|
<label for="themeSelect">主题配色</label>
|
||||||
|
<select id="themeSelect">
|
||||||
|
<option value="cloud-dancer">云舞蓝</option>
|
||||||
|
<option value="corundum-blue">珊瑚蓝</option>
|
||||||
|
<option value="kiwi-green">奇异绿</option>
|
||||||
|
<option value="spicy-red">热辣红</option>
|
||||||
|
<option value="teal-water">蓝绿水</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
<button id="jumpBtn" type="button">跳转到时间</button>
|
<button id="jumpBtn" type="button">跳转到时间</button>
|
||||||
@@ -2619,12 +2729,18 @@ class ExportService {
|
|||||||
${renderedMessages || '<div class="empty">暂无消息</div>'}
|
${renderedMessages || '<div class="empty">暂无消息</div>'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="image-preview" id="imagePreview">
|
||||||
|
<img id="imagePreviewTarget" alt="预览" />
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const messages = Array.from(document.querySelectorAll('.message'))
|
const messages = Array.from(document.querySelectorAll('.message'))
|
||||||
const searchInput = document.getElementById('searchInput')
|
const searchInput = document.getElementById('searchInput')
|
||||||
const timeInput = document.getElementById('timeInput')
|
const timeInput = document.getElementById('timeInput')
|
||||||
const jumpBtn = document.getElementById('jumpBtn')
|
const jumpBtn = document.getElementById('jumpBtn')
|
||||||
const resultCount = document.getElementById('resultCount')
|
const resultCount = document.getElementById('resultCount')
|
||||||
|
const themeSelect = document.getElementById('themeSelect')
|
||||||
|
const imagePreview = document.getElementById('imagePreview')
|
||||||
|
const imagePreviewTarget = document.getElementById('imagePreviewTarget')
|
||||||
|
|
||||||
const updateCount = () => {
|
const updateCount = () => {
|
||||||
const visible = messages.filter((msg) => !msg.classList.contains('hidden'))
|
const visible = messages.filter((msg) => !msg.classList.contains('hidden'))
|
||||||
@@ -2660,6 +2776,33 @@ class ExportService {
|
|||||||
setTimeout(() => targetMessage.classList.remove('highlight'), 2000)
|
setTimeout(() => targetMessage.classList.remove('highlight'), 2000)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const applyTheme = (value) => {
|
||||||
|
document.body.setAttribute('data-theme', value)
|
||||||
|
localStorage.setItem('weflow-export-theme', value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedTheme = localStorage.getItem('weflow-export-theme') || 'cloud-dancer'
|
||||||
|
themeSelect.value = storedTheme
|
||||||
|
applyTheme(storedTheme)
|
||||||
|
|
||||||
|
themeSelect.addEventListener('change', (event) => {
|
||||||
|
applyTheme(event.target.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
document.querySelectorAll('.previewable').forEach((img) => {
|
||||||
|
img.addEventListener('click', () => {
|
||||||
|
const full = img.getAttribute('data-full')
|
||||||
|
if (!full) return
|
||||||
|
imagePreviewTarget.src = full
|
||||||
|
imagePreview.classList.add('active')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
imagePreview.addEventListener('click', () => {
|
||||||
|
imagePreview.classList.remove('active')
|
||||||
|
imagePreviewTarget.src = ''
|
||||||
|
})
|
||||||
|
|
||||||
updateCount()
|
updateCount()
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user