feat(chat): 聊天页支持可选 username 路由

- 将聊天页迁移为 pages/chat/[[username]].vue(/chat 与 /chat/:username)

- 选中会话时同步 URL,支持路由直达指定会话

- 文件消息卡片补充文件类型图标与 WeChat PC 标识资源
This commit is contained in:
2977094657
2025-12-21 20:54:58 +08:00
parent 2dc355cca7
commit 41a2b546b8
6 changed files with 283 additions and 52 deletions

View File

@@ -201,16 +201,20 @@
:from="message.from"
/>
<div v-else-if="message.renderType === 'file'"
class="max-w-80 py-2.5 pr-2 pl-4 flex items-start bg-white space-x-2.5 msg-radius cursor-pointer border border-neutral-200 hover:bg-gray-50 transition-colors"
class="wechat-redpacket-card wechat-special-card wechat-file-card msg-radius"
:class="message.isSent ? 'wechat-special-sent-side' : ''"
@click="onFileClick(message)"
@contextmenu="openMediaContextMenu($event, message, 'file')">
<div class="flex-1 min-w-0">
<h4 class="break-words font-medium text-sm text-gray-900">{{ message.title || message.content }}</h4>
<small class="text-neutral-500 text-xs" v-if="message.fileSize">{{ formatFileSize(message.fileSize) }}</small>
<div class="wechat-redpacket-content">
<div class="wechat-redpacket-info wechat-file-info">
<span class="wechat-file-name">{{ message.title || message.content || '文件' }}</span>
<span class="wechat-file-size" v-if="message.fileSize">{{ formatFileSize(message.fileSize) }}</span>
</div>
<img :src="getFileIconUrl(message.title)" alt="" class="wechat-file-icon" />
</div>
<div class="shrink-0 w-10 h-10 flex items-center justify-center">
<!-- 根据文件类型显示图标 -->
<component :is="getFileIcon(message.title || message.content)" class="w-8 h-8" />
<div class="wechat-redpacket-bottom wechat-file-bottom">
<img :src="wechatPcLogoUrl" alt="" class="wechat-file-logo" />
<span>微信电脑版</span>
</div>
</div>
<div v-else-if="message.renderType === 'image'"
@@ -333,13 +337,13 @@
</div>
</div>
<!-- 红包消息 - 微信风格橙色卡片 -->
<div v-else-if="message.renderType === 'redPacket'" class="wechat-redpacket-card msg-radius"
:class="{ 'wechat-redpacket-received': message.redPacketReceived }">
<div v-else-if="message.renderType === 'redPacket'" class="wechat-redpacket-card wechat-special-card msg-radius"
:class="[{ 'wechat-redpacket-received': message.redPacketReceived }, message.isSent ? 'wechat-special-sent-side' : '']">
<div class="wechat-redpacket-content">
<img src="/assets/images/wechat/wechat-trans-icon3.png" v-if="!message.redPacketReceived" class="wechat-redpacket-icon" alt="">
<img src="/assets/images/wechat/wechat-trans-icon4.png" v-else class="wechat-redpacket-icon" alt="">
<div class="wechat-redpacket-info">
<span class="wechat-redpacket-text">{{ message.content || '恭喜发财,大吉大利' }}</span>
<span class="wechat-redpacket-text">{{ getRedPacketText(message) }}</span>
<span class="wechat-redpacket-status" v-if="message.redPacketReceived">已领取</span>
</div>
</div>
@@ -419,16 +423,60 @@
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted, nextTick, defineComponent, h } from 'vue'
import { ref, computed, watch, onMounted, onUnmounted, nextTick, defineComponent, h } from 'vue'
definePageMeta({
key: 'chat'
})
import { useApi } from '~/composables/useApi'
import { parseTextWithEmoji } from '~/utils/wechat-emojis'
import wechatPcLogoUrl from '~/assets/images/wechat/WeChat-Icon-Logo.wine.svg'
import zipIconUrl from '~/assets/images/wechat/zip.png'
import pdfIconUrl from '~/assets/images/wechat/pdf.png'
import wordIconUrl from '~/assets/images/wechat/word.png'
import excelIconUrl from '~/assets/images/wechat/excel.png'
// URL
const getFileIconUrl = (fileName) => {
if (!fileName) return zipIconUrl
const ext = String(fileName).split('.').pop()?.toLowerCase() || ''
switch (ext) {
case 'pdf':
return pdfIconUrl
case 'doc':
case 'docx':
return wordIconUrl
case 'xls':
case 'xlsx':
case 'csv':
return excelIconUrl
case 'zip':
case 'rar':
case '7z':
case 'tar':
case 'gz':
default:
return zipIconUrl
}
}
//
useHead({
title: '聊天记录查看器 - 微信数据分析助手'
})
const route = useRoute()
const routeUsername = computed(() => {
const raw = route.params.username
return (Array.isArray(raw) ? raw[0] : raw) || ''
})
const buildChatPath = (username) => {
return username ? `/chat/${encodeURIComponent(username)}` : '/chat'
}
//
const selectedContact = ref(null)
@@ -611,23 +659,55 @@ const messages = computed(() => {
return allMessages.value[selectedContact.value.username] || []
})
const formatTimeDivider = (ts) => {
// " HH:MM""X HH:MM""MMDD HH:MM""YYYYMMDD HH:MM"
const formatSmartTime = (ts) => {
if (!ts) return ''
try {
const d = new Date(Number(ts) * 1000)
const now = new Date()
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
const sameDay = d.getFullYear() === now.getFullYear() && d.getMonth() === now.getMonth() && d.getDate() === now.getDate()
if (sameDay) return `${hh}:${mm}`
const m = String(d.getMonth() + 1).padStart(2, '0')
const dd = String(d.getDate()).padStart(2, '0')
return `${m}-${dd} ${hh}:${mm}`
const timeStr = `${hh}:${mm}`
// 24
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const targetStart = new Date(d.getFullYear(), d.getMonth(), d.getDate())
const dayDiff = Math.floor((todayStart - targetStart) / (1000 * 60 * 60 * 24))
//
if (dayDiff === 0) {
return timeStr
}
//
if (dayDiff === 1) {
return `昨天 ${timeStr}`
}
// 2-6
if (dayDiff >= 2 && dayDiff <= 6) {
const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
return `${weekDays[d.getDay()]} ${timeStr}`
}
//
const month = d.getMonth() + 1
const day = d.getDate()
if (d.getFullYear() === now.getFullYear()) {
return `${month}${day}${timeStr}`
}
//
return `${d.getFullYear()}${month}${day}${timeStr}`
} catch {
return ''
}
}
const formatTimeDivider = (ts) => {
return formatSmartTime(ts)
}
const formatMessageTime = (ts) => {
if (!ts) return ''
try {
@@ -672,6 +752,12 @@ const formatTransferAmount = (amount) => {
return s.replace(/[¥¥]/g, '').trim()
}
const getRedPacketText = (message) => {
const text = String(message?.content ?? '').trim()
if (!text || text === '[Red Packet]') return '恭喜发财,大吉大利'
return text
}
//
const FileIconPdf = defineComponent({
render() {
@@ -845,9 +931,38 @@ const hasMoreMessages = computed(() => {
//
//
const selectContact = (contact) => {
const selectContact = async (contact, options = {}) => {
if (!contact) return
selectedContact.value = contact
loadMessages({ username: contact.username, reset: true })
const username = contact?.username || ''
if (!username) return
if (options.syncRoute !== false && username) {
const current = routeUsername.value || ''
if (current !== username) {
await navigateTo(buildChatPath(username), { replace: options.replaceRoute !== false })
}
}
loadMessages({ username, reset: true })
}
const applyRouteSelection = async () => {
if (!contacts.value || contacts.value.length === 0) {
selectedContact.value = null
return
}
const requested = routeUsername.value || ''
if (requested) {
const matched = contacts.value.find((c) => c.username === requested)
if (matched) {
if (selectedContact.value?.username !== matched.username) {
await selectContact(matched, { syncRoute: false })
}
return
}
}
await selectContact(contacts.value[0], { syncRoute: true, replaceRoute: true })
}
//
@@ -918,9 +1033,7 @@ const loadSessionsForSelectedAccount = async () => {
messagesError.value = ''
selectedContact.value = null
if (contacts.value.length > 0) {
selectContact(contacts.value[0])
}
await applyRouteSelection()
}
const onAccountChange = async () => {
@@ -1163,6 +1276,15 @@ const refreshSelectedMessages = async () => {
await loadMessages({ username: selectedContact.value.username, reset: true })
}
watch(
routeUsername,
async () => {
if (isLoadingContacts.value) return
await applyRouteSelection()
},
{ immediate: true }
)
const autoLoadReady = ref(true)
const onMessageScroll = async () => {
@@ -1516,9 +1638,32 @@ const LinkCard = defineComponent({
color: #1a1a1a;
}
/* 统一特殊消息尾巴(红包 / 文件等) */
.wechat-special-card {
position: relative;
overflow: visible;
}
.wechat-special-card::after {
content: '';
position: absolute;
top: 16px;
left: -4px;
width: 10px;
height: 10px;
background-color: inherit;
transform: rotate(45deg);
border-radius: 2px;
}
.wechat-special-sent-side::after {
left: auto;
right: -4px;
}
/* 转账消息样式 - 微信风格 */
.wechat-transfer-card {
width: 240px;
width: 210px;
background: #f79c46;
border-radius: var(--message-radius);
overflow: visible;
@@ -1545,8 +1690,8 @@ const LinkCard = defineComponent({
.wechat-transfer-content {
display: flex;
align-items: center;
padding: 12px 14px;
min-height: 56px;
padding: 10px 12px;
min-height: 58px;
}
.wechat-transfer-icon {
@@ -1583,11 +1728,22 @@ const LinkCard = defineComponent({
}
.wechat-transfer-bottom {
height: 24px;
height: 27px;
display: flex;
align-items: center;
padding: 0 14px;
border-top: 1px solid rgba(255,255,255,0.2);
padding: 0 12px;
border-top: none;
position: relative;
}
.wechat-transfer-bottom::before {
content: '';
position: absolute;
top: 0;
left: 13px;
right: 13px;
height: 1px;
background: rgba(255,255,255,0.2);
}
.wechat-transfer-bottom span {
@@ -1633,30 +1789,18 @@ const LinkCard = defineComponent({
/* 红包消息样式 - 微信风格 */
.wechat-redpacket-card {
width: 240px;
width: 210px;
background: #fa9d3b;
border-radius: var(--message-radius);
overflow: hidden;
overflow: visible;
position: relative;
}
.wechat-redpacket-card::after {
content: '';
position: absolute;
top: 16px;
left: -4px;
width: 10px;
height: 10px;
background: #fa9d3b;
transform: rotate(45deg);
border-radius: 2px;
}
.wechat-redpacket-content {
display: flex;
align-items: center;
padding: 12px 14px;
min-height: 56px;
padding: 10px 12px;
min-height: 58px;
}
.wechat-redpacket-icon {
@@ -1689,11 +1833,22 @@ const LinkCard = defineComponent({
}
.wechat-redpacket-bottom {
height: 24px;
height: 27px;
display: flex;
align-items: center;
padding: 0 14px;
border-top: 1px solid rgba(255,255,255,0.2);
padding: 0 12px;
border-top: none;
position: relative;
}
.wechat-redpacket-bottom::before {
content: '';
position: absolute;
top: 0;
left: 13px;
right: 13px;
height: 1px;
background: rgba(255,255,255,0.2);
}
.wechat-redpacket-bottom span {
@@ -1706,10 +1861,6 @@ const LinkCard = defineComponent({
background: #f8e2c6;
}
.wechat-redpacket-received::after {
background: #f8e2c6;
}
.wechat-redpacket-received .wechat-redpacket-text,
.wechat-redpacket-received .wechat-redpacket-status {
color: #b88550;
@@ -1719,6 +1870,85 @@ const LinkCard = defineComponent({
color: #c9a67a;
}
/* 文件消息样式 - 基于红包样式覆盖 */
.wechat-file-card {
width: 210px;
background: #fff;
cursor: pointer;
transition: background-color 0.15s ease;
}
.wechat-file-card .wechat-redpacket-content {
padding: 10px 12px;
min-height: 58px;
}
.wechat-file-card .wechat-redpacket-bottom {
height: 27px;
padding: 0 12px;
border-top: none;
position: relative;
}
.wechat-file-card .wechat-redpacket-bottom::before {
content: '';
position: absolute;
top: 0;
left: 13px;
right: 13px;
height: 1.5px;
background: #e8e8e8;
}
.wechat-file-card:hover {
background: #f5f5f5;
}
.wechat-file-card .wechat-file-info {
margin-left: 0;
margin-right: 10px;
}
.wechat-file-name {
font-size: 14px;
color: #1a1a1a;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
word-break: break-all;
line-height: 1.4;
}
.wechat-file-size {
font-size: 12px;
color: #b2b2b2;
margin-top: 4px;
}
.wechat-file-icon {
width: 40px;
height: 40px;
flex-shrink: 0;
object-fit: contain;
}
.wechat-file-bottom {
border-top: 1px solid #e8e8e8;
}
.wechat-file-bottom span {
font-size: 12px;
color: #b2b2b2;
}
.wechat-file-logo {
width: 18px;
height: 18px;
object-fit: contain;
margin-right: 4px;
}
/* 隐私模式模糊效果 */
.privacy-blur {
filter: blur(9px);
@@ -1728,4 +1958,4 @@ const LinkCard = defineComponent({
.privacy-blur:hover {
filter: blur(4px);
}
</style>
</style>