Skip to content

Commit d8d2139

Browse files
committed
improvement(chat): 完善会话置顶与消息卡片解析展示
- 后端:会话列表支持置顶识别(isTop)并按置顶优先排序 - 后端:修正群聊 XML 发送者提取,避免 refermsg 嵌套误识别 - 后端:完善转账状态后处理与视频缩略图 MD5 回填(packed_info_data) - 后端:补充 quoteThumbUrl/linkType/linkStyle 字段链路 - 前端:新增置顶会话背景态、引用链接缩略图预览与 LinkCard cover 样式 - 测试:新增转账、置顶、引用解析与视频缩略图相关回归用例
1 parent 250c125 commit d8d2139

11 files changed

Lines changed: 1365 additions & 198 deletions
92 Bytes
Loading

frontend/pages/chat/[[username]].vue

Lines changed: 258 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,13 @@
236236
<template v-else>
237237
<div v-for="contact in filteredContacts" :key="contact.id"
238238
class="px-3 cursor-pointer transition-colors duration-150 border-b border-gray-100 h-[calc(80px/var(--dpr))] flex items-center"
239-
:class="selectedContact?.id === contact.id ? 'bg-[#DEDEDE] hover:bg-[#d3d3d3]' : 'hover:bg-[#eaeaea]'"
239+
:class="contact.isTop
240+
? (selectedContact?.id === contact.id
241+
? 'bg-[#D2D2D2] hover:bg-[#C7C7C7]'
242+
: 'bg-[#EAEAEA] hover:bg-[#DEDEDE]')
243+
: (selectedContact?.id === contact.id
244+
? 'bg-[#DEDEDE] hover:bg-[#d3d3d3]'
245+
: 'hover:bg-[#eaeaea]')"
240246
@click="selectContact(contact)">
241247
<div class="flex items-center space-x-3 w-full">
242248
<!-- 联系人头像 -->
@@ -501,6 +507,7 @@
501507
:fromAvatar="message.fromAvatar"
502508
:from="message.from"
503509
:isSent="message.isSent"
510+
:variant="message.linkCardVariant || 'default'"
504511
/>
505512
<div v-else-if="message.renderType === 'file'"
506513
class="wechat-redpacket-card wechat-special-card wechat-file-card msg-radius"
@@ -651,25 +658,55 @@
651658
class="hidden"
652659
></audio>
653660
</div>
654-
<div v-else class="line-clamp-2">
655-
<span v-if="message.quoteTitle">{{ message.quoteTitle }}:</span>
656-
<span
657-
v-if="message.quoteContent && !(isQuotedImage(message) && message.quoteTitle && message.quoteImageUrl && !message._quoteImageError)"
658-
:class="message.quoteTitle ? 'ml-1' : ''"
659-
>
660-
{{ message.quoteContent }}
661-
</span>
661+
<div v-else class="min-w-0 flex items-start">
662+
<template v-if="isQuotedLink(message)">
663+
<div class="line-clamp-2 min-w-0 flex-1">
664+
<span v-if="message.quoteTitle">{{ message.quoteTitle }}:</span>
665+
<span
666+
v-if="getQuotedLinkText(message)"
667+
:class="message.quoteTitle ? 'ml-1' : ''"
668+
>
669+
🔗 {{ getQuotedLinkText(message) }}
670+
</span>
671+
</div>
672+
</template>
673+
<template v-else>
674+
<div class="line-clamp-2 min-w-0 flex-1">
675+
<span v-if="message.quoteTitle">{{ message.quoteTitle }}:</span>
676+
<span
677+
v-if="message.quoteContent && !(isQuotedImage(message) && message.quoteTitle && message.quoteImageUrl && !message._quoteImageError)"
678+
:class="message.quoteTitle ? 'ml-1' : ''"
679+
>
680+
{{ message.quoteContent }}
681+
</span>
682+
</div>
683+
</template>
662684
</div>
663685
</div>
664686
<div
665-
v-if="isQuotedImage(message) && message.quoteImageUrl && !message._quoteImageError"
666-
class="ml-2 my-2 flex-shrink-0 w-[45px] h-[45px] rounded overflow-hidden cursor-pointer"
687+
v-if="isQuotedLink(message) && message.quoteThumbUrl && !message._quoteThumbError"
688+
class="ml-2 my-2 flex-shrink-0 max-w-[98px] max-h-[49px] overflow-hidden flex items-center justify-center cursor-pointer"
689+
@click.stop="openImagePreview(message.quoteThumbUrl)"
690+
>
691+
<img
692+
:src="message.quoteThumbUrl"
693+
alt="引用链接缩略图"
694+
class="max-h-[49px] w-auto max-w-[98px] object-contain"
695+
loading="lazy"
696+
decoding="async"
697+
referrerpolicy="no-referrer"
698+
@error="onQuoteThumbError(message)"
699+
/>
700+
</div>
701+
<div
702+
v-if="!isQuotedLink(message) && isQuotedImage(message) && message.quoteImageUrl && !message._quoteImageError"
703+
class="ml-2 my-2 flex-shrink-0 max-w-[98px] max-h-[49px] overflow-hidden flex items-center justify-center cursor-pointer"
667704
@click.stop="openImagePreview(message.quoteImageUrl)"
668705
>
669706
<img
670707
:src="message.quoteImageUrl"
671708
alt="引用图片"
672-
class="w-full h-full object-contain"
709+
class="max-h-[49px] w-auto max-w-[98px] object-contain"
673710
loading="lazy"
674711
decoding="async"
675712
@error="onQuoteImageError(message)"
@@ -3226,12 +3263,31 @@ const isQuotedImage = (message) => {
32263263
return false
32273264
}
32283265

3266+
const isQuotedLink = (message) => {
3267+
const t = String(message?.quoteType || '').trim()
3268+
if (t === '49') return true
3269+
return /^\[链接\]\s*/.test(String(message?.quoteContent || '').trim())
3270+
}
3271+
3272+
const getQuotedLinkText = (message) => {
3273+
const raw = String(message?.quoteContent || '').trim()
3274+
if (!raw) return ''
3275+
const stripped = raw.replace(/^\[链接\]\s*/u, '').trim()
3276+
return stripped || raw
3277+
}
3278+
32293279
const onQuoteImageError = (message) => {
32303280
try {
32313281
if (message) message._quoteImageError = true
32323282
} catch {}
32333283
}
32343284

3285+
const onQuoteThumbError = (message) => {
3286+
try {
3287+
if (message) message._quoteThumbError = true
3288+
} catch {}
3289+
}
3290+
32353291
const playQuoteVoice = (message) => {
32363292
playVoice({ id: getQuoteVoiceId(message) })
32373293
}
@@ -3969,7 +4025,7 @@ const getTransferTitle = (message) => {
39694025
if (message.transferStatus) return message.transferStatus
39704026
switch (paySubType) {
39714027
case '1': return '转账'
3972-
case '3': return message.isSent ? '已收款' : '已被接收'
4028+
case '3': return message.isSent ? '已被接收' : '已收款'
39734029
case '8': return '发起转账'
39744030
case '4': return '已退还'
39754031
case '9': return '已被退还'
@@ -4136,6 +4192,7 @@ const loadSessionsForSelectedAccount = async () => {
41364192
lastMessageTime: s.lastMessageTime || '',
41374193
unreadCount: s.unreadCount || 0,
41384194
isGroup: !!s.isGroup,
4195+
isTop: !!s.isTop,
41394196
username: s.username
41404197
}))
41414198

@@ -4209,6 +4266,7 @@ const refreshSessionsForSelectedAccount = async ({ sourceOverride } = {}) => {
42094266
lastMessageTime: s.lastMessageTime || '',
42104267
unreadCount: s.unreadCount || 0,
42114268
isGroup: !!s.isGroup,
4269+
isTop: !!s.isTop,
42124270
username: s.username
42134271
}))
42144272

@@ -4401,6 +4459,19 @@ const normalizeMessage = (msg) => {
44014459
].filter(Boolean)
44024460
return parts.length ? `${mediaBase}/api/chat/media/image?${parts.join('&')}` : ''
44034461
})()
4462+
const quoteThumbUrl = (() => {
4463+
const raw = isUsableMediaUrl(msg.quoteThumbUrl) ? normalizeMaybeUrl(msg.quoteThumbUrl) : ''
4464+
if (!raw) return ''
4465+
if (/^\/api\/chat\/media\//i.test(raw) || /^blob:/i.test(raw) || /^data:/i.test(raw)) return raw
4466+
if (!/^https?:\/\//i.test(raw)) return raw
4467+
try {
4468+
const host = new URL(raw).hostname.toLowerCase()
4469+
if (host.endsWith('.qpic.cn') || host.endsWith('.qlogo.cn')) {
4470+
return `${mediaBase}/api/chat/media/proxy_image?url=${encodeURIComponent(raw)}`
4471+
}
4472+
} catch {}
4473+
return raw
4474+
})()
44044475

44054476
return {
44064477
id: msg.id,
@@ -4443,17 +4514,22 @@ const normalizeMessage = (msg) => {
44434514
quoteVoiceLength: msg.quoteVoiceLength || '',
44444515
quoteVoiceUrl,
44454516
quoteImageUrl: quoteImageUrl || '',
4517+
quoteThumbUrl: quoteThumbUrl || '',
44464518
_quoteImageError: false,
4519+
_quoteThumbError: false,
44474520
amount: msg.amount || '',
44484521
coverUrl: msg.coverUrl || '',
44494522
fileSize: msg.fileSize || '',
44504523
fileMd5: msg.fileMd5 || '',
44514524
paySubType: msg.paySubType || '',
44524525
transferStatus: msg.transferStatus || '',
4453-
transferReceived: msg.paySubType === '3' || msg.transferStatus === '已收款',
4526+
transferReceived: msg.paySubType === '3' || msg.transferStatus === '已收款' || msg.transferStatus === '已被接收',
44544527
voiceUrl: normalizedVoiceUrl || '',
44554528
voiceDuration: msg.voiceLength || msg.voiceDuration || '',
44564529
preview: normalizedLinkPreviewUrl || '',
4530+
linkType: String(msg.linkType || '').trim(),
4531+
linkStyle: String(msg.linkStyle || '').trim(),
4532+
linkCardVariant: String(msg.linkStyle || '').trim() === 'cover' ? 'cover' : 'default',
44574533
from: String(msg.from || '').trim(),
44584534
fromUsername,
44594535
fromAvatar,
@@ -5331,7 +5407,8 @@ const LinkCard = defineComponent({
53315407
preview: { type: String, default: '' },
53325408
fromAvatar: { type: String, default: '' },
53335409
from: { type: String, default: '' },
5334-
isSent: { type: Boolean, default: false }
5410+
isSent: { type: Boolean, default: false },
5411+
variant: { type: String, default: 'default' }
53355412
},
53365413
setup(props) {
53375414
const getFromText = () => {
@@ -5356,6 +5433,65 @@ const LinkCard = defineComponent({
53565433
return t ? (Array.from(t)[0] || '') : ''
53575434
})()
53585435
const fromAvatarUrl = String(props.fromAvatar || '').trim()
5436+
const isCoverVariant = String(props.variant || '').trim() === 'cover'
5437+
5438+
if (isCoverVariant) {
5439+
const fromRow = h('div', { class: 'wechat-link-cover-from' }, [
5440+
h('div', { class: 'wechat-link-cover-from-avatar', 'aria-hidden': 'true' }, [
5441+
fromAvatarText || '\u200B',
5442+
fromAvatarUrl ? h('img', {
5443+
src: fromAvatarUrl,
5444+
alt: '',
5445+
class: 'wechat-link-cover-from-avatar-img',
5446+
referrerpolicy: 'no-referrer',
5447+
onError: (e) => { try { e?.target && (e.target.style.display = 'none') } catch {} }
5448+
}) : null
5449+
].filter(Boolean)),
5450+
h('div', { class: 'wechat-link-cover-from-name' }, fromText || '\u200B')
5451+
])
5452+
5453+
return h(
5454+
'a',
5455+
{
5456+
href: props.href,
5457+
target: '_blank',
5458+
rel: 'noreferrer',
5459+
class: [
5460+
'wechat-link-card-cover',
5461+
'wechat-special-card',
5462+
'msg-radius',
5463+
props.isSent ? 'wechat-special-sent-side' : ''
5464+
].filter(Boolean).join(' '),
5465+
style: {
5466+
width: '137px',
5467+
minWidth: '137px',
5468+
maxWidth: '137px',
5469+
display: 'flex',
5470+
flexDirection: 'column',
5471+
boxSizing: 'border-box',
5472+
flex: '0 0 auto',
5473+
background: '#fff',
5474+
border: 'none',
5475+
boxShadow: 'none',
5476+
textDecoration: 'none',
5477+
outline: 'none'
5478+
}
5479+
},
5480+
[
5481+
props.preview ? h('div', { class: 'wechat-link-cover-image-wrap' }, [
5482+
h('img', {
5483+
src: props.preview,
5484+
alt: props.heading || '链接封面',
5485+
class: 'wechat-link-cover-image',
5486+
referrerpolicy: 'no-referrer'
5487+
}),
5488+
fromRow,
5489+
]) : fromRow,
5490+
h('div', { class: 'wechat-link-cover-title' }, props.heading || props.href)
5491+
].filter(Boolean)
5492+
)
5493+
}
5494+
53595495
return h(
53605496
'a',
53615497
{
@@ -5930,11 +6066,11 @@ const LinkCard = defineComponent({
59306066

59316067
/* 已领取的转账样式 */
59326068
.wechat-transfer-received {
5933-
background: #f8e2c6;
6069+
background: #FDCE9D;
59346070
}
59356071

59366072
.wechat-transfer-received::after {
5937-
background: #f8e2c6;
6073+
background: #FDCE9D;
59386074
}
59396075

59406076
.wechat-transfer-received .wechat-transfer-amount,
@@ -6258,6 +6394,111 @@ const LinkCard = defineComponent({
62586394
white-space: nowrap;
62596395
}
62606396

6397+
/* 链接封面卡片(170x230 图 + 60 底栏) */
6398+
:deep(.wechat-link-card-cover) {
6399+
width: 137px;
6400+
min-width: 137px;
6401+
max-width: 137px;
6402+
background: #fff;
6403+
display: flex;
6404+
flex-direction: column;
6405+
box-sizing: border-box;
6406+
border: none;
6407+
box-shadow: none;
6408+
outline: none;
6409+
cursor: pointer;
6410+
text-decoration: none;
6411+
transition: background-color 0.15s ease;
6412+
}
6413+
6414+
:deep(.wechat-link-card-cover:hover) {
6415+
background: #f5f5f5;
6416+
}
6417+
6418+
:deep(.wechat-link-cover-image-wrap) {
6419+
width: 137px;
6420+
height: 180px;
6421+
position: relative;
6422+
overflow: hidden;
6423+
border-radius: 4px 4px 0 0;
6424+
background: #f2f2f2;
6425+
flex-shrink: 0;
6426+
}
6427+
6428+
:deep(.wechat-link-cover-image) {
6429+
width: 100%;
6430+
height: 100%;
6431+
object-fit: cover;
6432+
object-position: center;
6433+
display: block;
6434+
}
6435+
6436+
/* 仅公众号封面卡片去掉菱形尖角,其它消息保持原样 */
6437+
:deep(.wechat-link-card-cover.wechat-special-card)::after {
6438+
content: none !important;
6439+
}
6440+
6441+
:deep(.wechat-link-cover-from) {
6442+
height: 30px;
6443+
display: flex;
6444+
align-items: center;
6445+
gap: 6px;
6446+
padding: 0 10px;
6447+
box-sizing: border-box;
6448+
position: absolute;
6449+
left: 0;
6450+
right: 0;
6451+
bottom: 0;
6452+
background: transparent;
6453+
flex-shrink: 0;
6454+
}
6455+
6456+
:deep(.wechat-link-cover-from-avatar) {
6457+
width: 18px;
6458+
height: 18px;
6459+
border-radius: 50%;
6460+
background: #111;
6461+
color: #fff;
6462+
font-size: 11px;
6463+
line-height: 18px;
6464+
text-align: center;
6465+
flex-shrink: 0;
6466+
position: relative;
6467+
overflow: hidden;
6468+
}
6469+
6470+
:deep(.wechat-link-cover-from-avatar-img) {
6471+
position: absolute;
6472+
inset: 0;
6473+
width: 100%;
6474+
height: 100%;
6475+
object-fit: cover;
6476+
display: block;
6477+
}
6478+
6479+
:deep(.wechat-link-cover-from-name) {
6480+
font-size: 12px;
6481+
color: #f3f3f3;
6482+
overflow: hidden;
6483+
text-overflow: ellipsis;
6484+
white-space: nowrap;
6485+
}
6486+
6487+
:deep(.wechat-link-cover-title) {
6488+
height: 50px;
6489+
padding: 7px 10px 0;
6490+
box-sizing: border-box;
6491+
font-size: 12px;
6492+
line-height: 1.24;
6493+
color: #1a1a1a;
6494+
display: -webkit-box;
6495+
-webkit-line-clamp: 2;
6496+
-webkit-box-orient: vertical;
6497+
overflow: hidden;
6498+
word-break: break-word;
6499+
flex-shrink: 0;
6500+
}
6501+
62616502
/* 隐私模式模糊效果 */
62626503
.privacy-blur {
62636504
filter: blur(9px);
92 Bytes
Loading

0 commit comments

Comments
 (0)