Files
WeChatDataAnalysis/frontend/components/EditedMessagePreview.vue
T
2977094657 949990d125 feat(chat-edit-ui): 新增聊天编辑入口与修改记录页面
- 聊天右键菜单新增修改消息、字段编辑、恢复原消息、修复发送者、反转微信气泡位置

- 新增 /edits 页面,支持会话维度浏览修改记录与原/现对比

- 新增 EditedMessagePreview 组件复用聊天气泡样式

- 侧边栏新增修改记录入口

- useApi 增加 chat edits 相关接口,nuxt 配置支持可配置 API base

- 增加会话级反转消息位置前端显示开关(本地存储)
2026-02-22 11:56:28 +08:00

135 lines
4.7 KiB
Vue
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div v-if="!message" class="flex items-center justify-center py-4">
<span class="text-sm text-gray-400">无数据</span>
</div>
<!-- 完全复用聊天页消息结构外层 flex + 头像 + 气泡 -->
<div v-else class="flex items-center" :class="message.isSent ? 'justify-end' : 'justify-start'">
<div class="flex items-start" :class="message.isSent ? 'flex-row-reverse' : ''">
<!-- 头像与聊天页完全一致 -->
<div class="relative">
<div
class="w-[calc(42px/var(--dpr,1))] h-[calc(42px/var(--dpr,1))] rounded-md overflow-hidden bg-gray-300 flex-shrink-0"
:class="message.isSent ? 'ml-3' : 'mr-3'"
>
<div v-if="resolvedAvatar" class="w-full h-full">
<img
:src="resolvedAvatar"
alt="avatar"
class="w-full h-full object-cover"
referrerpolicy="no-referrer"
/>
</div>
<div
v-else
class="w-full h-full flex items-center justify-center text-white text-xs font-bold"
:style="{ backgroundColor: message.avatarColor || (message.isSent ? '#4B5563' : '#6B7280') }"
>
{{ avatarLetter }}
</div>
</div>
</div>
<!-- 消息内容气泡与聊天页完全一致 -->
<div
class="flex flex-col relative group"
:class="message.isSent ? 'items-end' : 'items-start'"
>
<!-- 群聊发送者名可选 -->
<div
v-if="senderName && !message.isSent"
class="text-[11px] text-gray-500 mb-1"
:class="message.isSent ? 'text-right' : 'text-left'"
>
{{ senderName }}
</div>
<!-- 时间悬浮 tooltip -->
<div
v-if="message.fullTime"
class="absolute -top-6 z-10 rounded bg-black/70 text-white text-[10px] px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none whitespace-nowrap"
:class="message.isSent ? 'right-0' : 'left-0'"
>
{{ message.fullTime }}
</div>
<!-- 表情 -->
<div v-if="renderType === 'emoji' && message.emojiUrl">
<img :src="normalizeMaybeUrl(message.emojiUrl)" alt="emoji" class="w-24 h-24 object-contain" />
</div>
<!-- 图片 -->
<div v-else-if="renderType === 'image' && message.imageUrl" class="max-w-sm">
<div class="msg-radius overflow-hidden">
<img :src="normalizeMaybeUrl(message.imageUrl)" alt="图片" class="max-w-[240px] max-h-[240px] object-cover" />
</div>
</div>
<!-- 视频 -->
<div
v-else-if="renderType === 'video'"
class="px-3 py-2 text-sm max-w-sm relative msg-bubble whitespace-pre-wrap break-words leading-relaxed"
:class="message.isSent ? 'bg-[#95EC69] text-black bubble-tail-r' : 'bg-white text-gray-800 bubble-tail-l'"
>
[视频]
</div>
<!-- 语音 -->
<div
v-else-if="renderType === 'voice'"
class="px-3 py-2 text-sm max-w-sm relative msg-bubble whitespace-pre-wrap break-words leading-relaxed"
:class="message.isSent ? 'bg-[#95EC69] text-black bubble-tail-r' : 'bg-white text-gray-800 bubble-tail-l'"
>
[语音]
</div>
<!-- 默认文本消息 -->
<div
v-else
class="px-3 py-2 text-sm max-w-sm relative msg-bubble whitespace-pre-wrap break-words leading-relaxed"
:class="message.isSent ? 'bg-[#95EC69] text-black bubble-tail-r' : 'bg-white text-gray-800 bubble-tail-l'"
>
{{ message.content || '' }}
</div>
</div>
</div>
</div>
</template>
<script setup>
const props = defineProps({
message: { type: Object, default: null },
})
const mediaBase = process.client ? 'http://localhost:8000' : ''
const normalizeMaybeUrl = (u) => {
const raw = String(u || '').trim()
if (!raw) return ''
if (/^https?:\/\//i.test(raw) || /^blob:/i.test(raw) || /^data:/i.test(raw)) return raw
if (/^\/api\//i.test(raw)) return `${mediaBase}${raw}`
return raw
}
const renderType = computed(() => String(props.message?.renderType || '').trim())
const resolvedAvatar = computed(() => {
const m = props.message
if (!m) return ''
return normalizeMaybeUrl(m.avatar || m.senderAvatar || '')
})
const avatarLetter = computed(() => {
const m = props.message
if (!m) return '?'
const name = m.senderDisplayName || m.senderUsername || m.sender || ''
return name.charAt(0) || '?'
})
const senderName = computed(() => {
const m = props.message
if (!m) return ''
return m.senderDisplayName || m.senderUsername || ''
})
</script>