From a94ab8366857ddf744502a066014ad7efe099530 Mon Sep 17 00:00:00 2001 From: 2977094657 <2977094657@qq.com> Date: Thu, 26 Mar 2026 12:57:50 +0800 Subject: [PATCH] feat: support finder cards in chat view --- frontend/assets/css/chat.css | 126 ++++++++++++++++++ frontend/assets/css/tailwind.css | 6 + frontend/components/chat/LinkCard.vue | 71 +++++++++- frontend/components/chat/SessionListPanel.vue | 2 +- .../assets/images/wechat/channels-logo.svg | 5 + src/wechat_decrypt_tool/chat_helpers.py | 64 +++++++++ tests/test_parse_app_message.py | 30 +++++ 7 files changed, 300 insertions(+), 4 deletions(-) create mode 100644 frontend/public/assets/images/wechat/channels-logo.svg diff --git a/frontend/assets/css/chat.css b/frontend/assets/css/chat.css index e154e99..39e2c25 100644 --- a/frontend/assets/css/chat.css +++ b/frontend/assets/css/chat.css @@ -1139,6 +1139,132 @@ flex-shrink: 0; } +.wechat-link-card-finder { + width: 135px; + min-width: 135px; + max-width: 135px; + border: none; + box-shadow: none; + outline: none; + cursor: pointer; + text-decoration: none; +} + +.wechat-link-card-finder.wechat-link-card--disabled { + cursor: default; +} + +.wechat-link-finder-cover { + width: 135px; + height: 185px; + position: relative; + overflow: hidden; + border-radius: 4px; + background: var(--app-surface-muted); +} + +.wechat-link-finder-cover--empty { + background: linear-gradient(180deg, #37cc6a 0%, #118f42 100%); +} + +.wechat-link-finder-cover-img { + width: 100%; + height: 100%; + object-fit: cover; + object-position: center; + display: block; +} + +.wechat-link-finder-cover-placeholder { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: rgba(255, 255, 255, 0.92); +} + +.wechat-link-finder-cover-placeholder svg { + width: 34px; + height: 34px; +} + +.wechat-link-finder-cover-shade { + position: absolute; + inset: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0.04) 0%, rgba(0, 0, 0, 0.12) 42%, rgba(0, 0, 0, 0.68) 100%); +} + +.wechat-link-finder-play { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -66%); + width: 40px; + height: 40px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.42); + display: flex; + align-items: center; + justify-content: center; + color: #fff; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.18); +} + +.wechat-link-finder-play svg { + width: 20px; + height: 20px; + margin-left: 2px; +} + +.wechat-link-finder-meta { + position: absolute; + left: 8px; + right: 8px; + bottom: 8px; + display: flex; + flex-direction: column; + gap: 0; +} + +.wechat-link-finder-author { + display: flex; + align-items: center; + gap: 5px; + min-width: 0; + padding: 5px 7px; + border-radius: 999px; + background: rgba(0, 0, 0, 0.28); + backdrop-filter: blur(6px); +} + +.wechat-link-finder-author-avatar { + width: 18px; + height: 18px; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.wechat-link-finder-author-avatar-img { + width: 100%; + height: 100%; + object-fit: contain; + display: block; +} + +.wechat-link-finder-author-name { + min-width: 0; + flex: 1 1 auto; + font-size: 10px; + color: rgba(255, 255, 255, 0.96); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.28); +} + /* 隐私模式模糊效果 */ .privacy-blur { filter: blur(9px); diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 70994a1..d653392 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -1437,14 +1437,20 @@ .session-list-item-name { color: var(--session-list-name); + font-weight: 400; + font-synthesis: none; } .session-list-item-time { color: var(--session-list-meta); + font-weight: 400; + font-synthesis: none; } .session-list-item-preview { color: var(--session-list-preview); + font-weight: 400; + font-synthesis: none; } .contact-search-wrapper { diff --git a/frontend/components/chat/LinkCard.vue b/frontend/components/chat/LinkCard.vue index a231804..1599be2 100644 --- a/frontend/components/chat/LinkCard.vue +++ b/frontend/components/chat/LinkCard.vue @@ -2,6 +2,8 @@ import { defineComponent, h, ref, watch } from 'vue' import miniProgramIconUrl from '~/assets/images/wechat/mini-program.svg' +const finderLogoUrl = '/assets/images/wechat/channels-logo.svg' + export default defineComponent({ name: 'LinkCard', props: { @@ -51,7 +53,11 @@ export default defineComponent({ return text ? (Array.from(text)[0] || '') : '' })() const fromAvatarUrl = String(props.fromAvatar || '').trim() + const headingText = String(props.heading || href || '').trim() + let abstractText = String(props.abstract || '').trim() + if (abstractText && headingText && abstractText === headingText) abstractText = '' const isMiniProgram = String(props.linkType || '').trim() === 'mini_program' + const isFinder = String(props.linkType || '').trim() === 'finder' const isCoverVariant = !isMiniProgram && String(props.variant || '').trim() === 'cover' const Tag = canNavigate ? 'a' : 'div' @@ -140,9 +146,68 @@ export default defineComponent({ ) } - const headingText = String(props.heading || href || '').trim() - let abstractText = String(props.abstract || '').trim() - if (abstractText && headingText && abstractText === headingText) abstractText = '' + if (isFinder) { + return h( + Tag, + { + ...(canNavigate ? { href, target: '_blank', rel: 'noreferrer' } : { role: 'group', 'aria-disabled': 'true' }), + class: [ + 'wechat-link-card-finder', + !canNavigate ? 'wechat-link-card--disabled' : '', + 'wechat-special-card', + 'msg-radius', + props.isSent ? 'wechat-special-sent-side' : '' + ].filter(Boolean).join(' '), + style: { + width: '135px', + minWidth: '135px', + maxWidth: '135px', + display: 'flex', + flexDirection: 'column', + boxSizing: 'border-box', + flex: '0 0 auto', + border: 'none', + boxShadow: 'none', + textDecoration: 'none', + outline: 'none' + } + }, + [ + h('div', { class: ['wechat-link-finder-cover', !props.preview ? 'wechat-link-finder-cover--empty' : ''].filter(Boolean).join(' ') }, [ + props.preview + ? h('img', { + src: props.preview, + alt: props.heading || '视频号封面', + class: 'wechat-link-finder-cover-img', + referrerpolicy: 'no-referrer' + }) + : h('div', { class: 'wechat-link-finder-cover-placeholder', 'aria-hidden': 'true' }, [ + h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ + h('path', { d: 'M8 5v14l11-7z' }) + ]) + ]), + h('div', { class: 'wechat-link-finder-cover-shade', 'aria-hidden': 'true' }), + h('div', { class: 'wechat-link-finder-play', 'aria-hidden': 'true' }, [ + h('svg', { viewBox: '0 0 24 24', fill: 'currentColor' }, [ + h('path', { d: 'M8 5v14l11-7z' }) + ]) + ]), + h('div', { class: 'wechat-link-finder-meta' }, [ + h('div', { class: 'wechat-link-finder-author' }, [ + h('div', { class: 'wechat-link-finder-author-avatar', 'aria-hidden': 'true' }, [ + h('img', { + src: finderLogoUrl, + alt: '', + class: 'wechat-link-finder-author-avatar-img' + }) + ]), + h('div', { class: 'wechat-link-finder-author-name' }, fromText || '视频号') + ]) + ]) + ]) + ] + ) + } if (isMiniProgram) { return h( diff --git a/frontend/components/chat/SessionListPanel.vue b/frontend/components/chat/SessionListPanel.vue index 9eec2a0..7538d5c 100644 --- a/frontend/components/chat/SessionListPanel.vue +++ b/frontend/components/chat/SessionListPanel.vue @@ -98,7 +98,7 @@