优化了接龙的消息样式

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

@@ -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')