优化了接龙的消息样式

This commit is contained in:
xuncha
2026-04-12 08:11:20 +08:00
parent f2f78bb4e2
commit 6359123323
4 changed files with 255 additions and 0 deletions

View File

@@ -4670,6 +4670,8 @@ class ChatService {
case '57':
// 引用消息title 就是回复的内容
return title
case '53':
return `[接龙] ${title.split(/\r?\n/).map(line => line.trim()).find(Boolean) || title}`
case '2000':
return `[转账] ${title}`
case '2001':
@@ -4699,6 +4701,8 @@ class ChatService {
return '[链接]'
case '87':
return '[群公告]'
case '53':
return '[接龙]'
default:
return '[消息]'
}
@@ -5298,6 +5302,8 @@ class ChatService {
const quoteInfo = this.parseQuoteMessage(content)
result.quotedContent = quoteInfo.content
result.quotedSender = quoteInfo.sender
} else if (xmlType === '53') {
result.appMsgKind = 'solitaire'
} else if ((xmlType === '5' || xmlType === '49') && (sourceUsername?.startsWith('gh_') || appName?.includes('公众号') || sourceName)) {
result.appMsgKind = 'official-link'
} else if (url) {

View File

@@ -2119,6 +2119,7 @@ class ExportService {
}
return title || '[引用消息]'
}
if (xmlType === '53') return title ? `[接龙] ${title.split(/\r?\n/).map(line => line.trim()).find(Boolean) || title}` : '[接龙]'
if (xmlType === '5' || xmlType === '49') return title ? `[链接] ${title}` : '[链接]'
// 有 title 就返回 title
@@ -3220,6 +3221,8 @@ class ExportService {
appMsgKind = 'announcement'
} else if (xmlType === '57' || hasReferMsg || localType === 244813135921) {
appMsgKind = 'quote'
} else if (xmlType === '53') {
appMsgKind = 'solitaire'
} else if (xmlType === '5' || xmlType === '49') {
appMsgKind = 'link'
} else if (looksLikeAppMsg) {

View File

@@ -2064,6 +2064,7 @@
.message-bubble .bubble-content:has(> .link-message),
.message-bubble .bubble-content:has(> .card-message),
.message-bubble .bubble-content:has(> .chat-record-message),
.message-bubble .bubble-content:has(> .solitaire-message),
.message-bubble .bubble-content:has(> .official-message),
.message-bubble .bubble-content:has(> .channel-video-card),
.message-bubble .bubble-content:has(> .location-message) {
@@ -3604,6 +3605,140 @@
}
}
// 接龙消息
.solitaire-message {
width: min(360px, 72vw);
max-width: 360px;
background: var(--card-inner-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
overflow: hidden;
cursor: pointer;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
&:hover {
background: var(--bg-hover);
border-color: var(--primary);
}
.solitaire-header {
display: flex;
gap: 10px;
padding: 12px 14px 10px;
border-bottom: 1px solid var(--border-color);
}
.solitaire-icon {
width: 30px;
height: 30px;
border-radius: 8px;
background: color-mix(in srgb, var(--primary) 12%, transparent);
color: var(--primary);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.solitaire-heading {
min-width: 0;
flex: 1;
}
.solitaire-title {
color: var(--text-primary);
font-size: 14px;
font-weight: 600;
line-height: 1.45;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.solitaire-meta {
margin-top: 2px;
color: var(--text-tertiary);
font-size: 12px;
line-height: 1.4;
}
.solitaire-intro,
.solitaire-entry-list {
padding: 10px 14px;
border-bottom: 1px solid var(--border-color);
}
.solitaire-intro {
color: var(--text-secondary);
font-size: 12px;
line-height: 1.55;
}
.solitaire-intro-line {
white-space: pre-wrap;
word-break: break-word;
}
.solitaire-entry-list {
display: flex;
flex-direction: column;
gap: 7px;
}
.solitaire-entry {
display: flex;
gap: 8px;
align-items: flex-start;
color: var(--text-secondary);
font-size: 12px;
line-height: 1.45;
}
.solitaire-entry-index {
width: 22px;
height: 22px;
border-radius: 8px;
background: var(--bg-tertiary);
color: var(--text-tertiary);
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
font-size: 11px;
}
.solitaire-entry-text {
min-width: 0;
flex: 1;
word-break: break-word;
}
.solitaire-muted-line {
color: var(--text-tertiary);
font-size: 12px;
line-height: 1.45;
}
.solitaire-footer {
padding: 8px 14px 10px;
color: var(--text-tertiary);
font-size: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.solitaire-chevron {
transition: transform 0.2s ease;
}
&.expanded .solitaire-chevron {
transform: rotate(180deg);
}
}
// 通话消息
.call-message {
display: flex;

View File

@@ -181,6 +181,51 @@ function buildChatRecordPreviewItems(recordList: ChatRecordItem[], maxVisible =
]
}
interface SolitaireEntry {
index: string
text: string
}
interface SolitaireContent {
title: string
introLines: string[]
entries: SolitaireEntry[]
}
function parseSolitaireContent(rawTitle: string): SolitaireContent {
const lines = String(rawTitle || '')
.replace(/\r\n/g, '\n')
.split('\n')
.map(line => line.trim())
.filter(Boolean)
const title = lines[0] || '接龙'
const introLines: string[] = []
const entries: SolitaireEntry[] = []
let hasStartedEntries = false
for (const line of lines.slice(1)) {
const entryMatch = /^(\d+)[..、]\s*(.+)$/.exec(line)
if (entryMatch) {
hasStartedEntries = true
entries.push({
index: entryMatch[1],
text: entryMatch[2].trim()
})
continue
}
if (hasStartedEntries && entries.length > 0) {
const previous = entries[entries.length - 1]
previous.text = `${previous.text} ${line}`.trim()
} else {
introLines.push(line)
}
}
return { title, introLines, entries }
}
function composeGlobalMsgSearchResults(
seedMap: Map<string, GlobalMsgSearchResult[]>,
authoritativeMap: Map<string, GlobalMsgSearchResult[]>
@@ -7825,6 +7870,7 @@ function MessageBubble({
const [senderName, setSenderName] = useState<string | undefined>(undefined)
const [quotedSenderName, setQuotedSenderName] = useState<string | undefined>(undefined)
const [quoteLayout, setQuoteLayout] = useState<QuoteLayout>('quote-top')
const [solitaireExpanded, setSolitaireExpanded] = useState(false)
const senderProfileRequestSeqRef = useRef(0)
const [emojiError, setEmojiError] = useState(false)
const [emojiLoading, setEmojiLoading] = useState(false)
@@ -9433,6 +9479,71 @@ function MessageBubble({
)
}
if (xmlType === '53' || message.appMsgKind === 'solitaire') {
const solitaireText = message.linkTitle || q('appmsg > title') || q('title') || cleanedParsedContent || '接龙'
const solitaire = parseSolitaireContent(solitaireText)
const previewEntries = solitaireExpanded ? solitaire.entries : solitaire.entries.slice(0, 3)
const hiddenEntryCount = Math.max(0, solitaire.entries.length - previewEntries.length)
const introLines = solitaireExpanded ? solitaire.introLines : solitaire.introLines.slice(0, 4)
const hasMoreIntro = !solitaireExpanded && solitaire.introLines.length > introLines.length
const countText = solitaire.entries.length > 0 ? `${solitaire.entries.length} 人参与` : '接龙消息'
return (
<div
className={`solitaire-message${solitaireExpanded ? ' expanded' : ''}`}
role="button"
tabIndex={0}
aria-expanded={solitaireExpanded}
onClick={isSelectionMode ? undefined : (e) => {
e.stopPropagation()
setSolitaireExpanded(value => !value)
}}
onKeyDown={isSelectionMode ? undefined : (e) => {
if (e.key !== 'Enter' && e.key !== ' ') return
e.preventDefault()
e.stopPropagation()
setSolitaireExpanded(value => !value)
}}
title={solitaireExpanded ? '点击收起接龙' : '点击展开接龙'}
>
<div className="solitaire-header">
<div className="solitaire-icon" aria-hidden="true">
<Hash size={18} />
</div>
<div className="solitaire-heading">
<div className="solitaire-title">{solitaire.title}</div>
<div className="solitaire-meta">{countText}</div>
</div>
</div>
{introLines.length > 0 && (
<div className="solitaire-intro">
{introLines.map((line, index) => (
<div key={`${line}-${index}`} className="solitaire-intro-line">{line}</div>
))}
{hasMoreIntro && <div className="solitaire-muted-line">...</div>}
</div>
)}
{previewEntries.length > 0 ? (
<div className="solitaire-entry-list">
{previewEntries.map(entry => (
<div key={`${entry.index}-${entry.text}`} className="solitaire-entry">
<span className="solitaire-entry-index">{entry.index}</span>
<span className="solitaire-entry-text">{entry.text}</span>
</div>
))}
{hiddenEntryCount > 0 && (
<div className="solitaire-muted-line"> {hiddenEntryCount} ...</div>
)}
</div>
) : null}
<div className="solitaire-footer">
<span>{solitaireExpanded ? '收起接龙' : '展开接龙'}</span>
<ChevronDown size={14} className="solitaire-chevron" />
</div>
</div>
)
}
const title = message.linkTitle || q('title') || cleanedParsedContent || 'Card'
const desc = message.appMsgDesc || q('des')
const url = message.linkUrl || q('url')