mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
feat: 支持微信内置表情渲染
This commit is contained in:
@@ -10,3 +10,4 @@ out
|
||||
docs/design/
|
||||
docs/ui-redesign-plan.md
|
||||
docs/ui-redesign-spec.md
|
||||
AGENTS.md
|
||||
|
||||
+2
-1
@@ -39,7 +39,8 @@
|
||||
"fzstd": "^0.1.1",
|
||||
"koffi": "^3.1.0",
|
||||
"openai": "^6.10.0",
|
||||
"silk-wasm": "^3.7.1"
|
||||
"silk-wasm": "^3.7.1",
|
||||
"wechat-emojis": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
|
||||
Generated
+7
@@ -33,6 +33,7 @@ specifiers:
|
||||
silk-wasm: ^3.7.1
|
||||
typescript: ^5.9.3
|
||||
vite: ^7.2.6
|
||||
wechat-emojis: ^1.0.2
|
||||
|
||||
dependencies:
|
||||
'@electron-toolkit/preload': 3.0.2_electron@43.1.0
|
||||
@@ -42,6 +43,7 @@ dependencies:
|
||||
koffi: 3.1.0
|
||||
openai: 6.10.0
|
||||
silk-wasm: 3.7.1
|
||||
wechat-emojis: 1.0.2
|
||||
|
||||
devDependencies:
|
||||
'@electron-toolkit/eslint-config-prettier': 3.0.0_fiitszekoa4sqtbwyewxi6kyy4
|
||||
@@ -4817,6 +4819,11 @@ packages:
|
||||
defaults: 1.0.4
|
||||
dev: true
|
||||
|
||||
/wechat-emojis/1.0.2:
|
||||
resolution: {integrity: sha512-T1drHGy92rKm/Vo7LRkU4D4wdREpVTjAMEa4gR1NB9IAyck3qmmewFSrnEEIyZfsv3SXTA7X1kt6Smt0UKVCyw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
dev: false
|
||||
|
||||
/which-boxed-primitive/1.1.1:
|
||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
@@ -364,6 +364,13 @@ body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.inline-wechat-emoji {
|
||||
display: inline-block;
|
||||
margin: 0 1px;
|
||||
vertical-align: -5px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.voice-bubble {
|
||||
min-width: 138px;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Message, Contact } from '../../../shared/types'
|
||||
import { VoicePlayer } from './VoicePlayer'
|
||||
import { RichMessageBubble } from './RichMessageBubble'
|
||||
import { ImageBubble } from './ImageBubble'
|
||||
import { renderWechatEmojiText } from '../utils/wechatEmojiText'
|
||||
import {
|
||||
buildGroupReportInput,
|
||||
GROUP_REPORT_SYSTEM_PROMPT,
|
||||
@@ -416,7 +417,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
||||
) : isRichMedia && msg.contentData ? (
|
||||
<RichMessageBubble contentData={msg.contentData} />
|
||||
) : (
|
||||
<div className="message-text">{msg.content}</div>
|
||||
<div className="message-text">{renderWechatEmojiText(msg.content)}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="message-meta">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ParsedContent } from '../../../shared/types'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { JSX, MouseEvent } from 'react'
|
||||
import { renderWechatEmojiText } from '../utils/wechatEmojiText'
|
||||
|
||||
const stickerDataUrlCache = new Map<string, string>()
|
||||
|
||||
@@ -26,7 +27,9 @@ export function RichMessageBubble({ contentData }: RichMessageBubbleProps): JSX.
|
||||
return <SystemBubble data={contentData} />
|
||||
case 'unknown':
|
||||
return (
|
||||
<div className="message-text">{(contentData as { raw?: string }).raw || '[未知消息]'}</div>
|
||||
<div className="message-text">
|
||||
{renderWechatEmojiText((contentData as { raw?: string }).raw || '[未知消息]')}
|
||||
</div>
|
||||
)
|
||||
default:
|
||||
return <div className="message-text">[不支持的消息类型]</div>
|
||||
@@ -207,15 +210,15 @@ function QuoteBubble({ data }: { data: Extract<ParsedContent, { type: 'quote' }>
|
||||
<div className="quote-message">
|
||||
<div className="quoted-message">
|
||||
{quotedSender && <span className="quoted-sender">{quotedSender}</span>}
|
||||
<span className="quoted-text">{quotedText}</span>
|
||||
<span className="quoted-text">{renderWechatEmojiText(quotedText, 18)}</span>
|
||||
</div>
|
||||
{replyText && <div className="quote-reply">{replyText}</div>}
|
||||
{replyText && <div className="quote-reply">{renderWechatEmojiText(replyText)}</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SystemBubble({ data }: { data: Extract<ParsedContent, { type: 'system' }> }): JSX.Element {
|
||||
return <div className="message-text">{data.content || '[系统消息]'}</div>
|
||||
return <div className="message-text">{renderWechatEmojiText(data.content || '[系统消息]')}</div>
|
||||
}
|
||||
|
||||
function getUrlHost(url?: string): string {
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
declare module 'wechat-emojis' {
|
||||
export function getEmojiPath(name: string): string | undefined
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { getEmojiPath } from 'wechat-emojis'
|
||||
|
||||
const emojiAssetModules = import.meta.glob(
|
||||
'../../../../node_modules/wechat-emojis/assets/**/*.png',
|
||||
{
|
||||
eager: true,
|
||||
query: '?url',
|
||||
import: 'default'
|
||||
}
|
||||
) as Record<string, string>
|
||||
|
||||
const emojiUrlByPath = new Map<string, string>()
|
||||
|
||||
for (const [modulePath, url] of Object.entries(emojiAssetModules)) {
|
||||
const normalized = modulePath.replace(/\\/g, '/')
|
||||
const match = normalized.match(/wechat-emojis\/(.+)$/)
|
||||
if (match) emojiUrlByPath.set(match[1], url)
|
||||
}
|
||||
|
||||
const unicodeFallback: Record<string, string> = {
|
||||
微笑: '🙂',
|
||||
撇嘴: '😒',
|
||||
色: '😍',
|
||||
发呆: '😳',
|
||||
得意: '😎',
|
||||
流泪: '😭',
|
||||
害羞: '😊',
|
||||
闭嘴: '🤐',
|
||||
睡: '😴',
|
||||
大哭: '😭',
|
||||
尴尬: '😅',
|
||||
发怒: '😡',
|
||||
调皮: '😜',
|
||||
呲牙: '😁',
|
||||
惊讶: '😮',
|
||||
难过: '😞',
|
||||
抓狂: '😫',
|
||||
吐: '🤮',
|
||||
偷笑: '🤭',
|
||||
愉快: '😄',
|
||||
白眼: '🙄',
|
||||
傲慢: '😤',
|
||||
困: '😪',
|
||||
惊恐: '😱',
|
||||
憨笑: '😄',
|
||||
悠闲: '😌',
|
||||
咒骂: '😤',
|
||||
疑问: '❓',
|
||||
嘘: '🤫',
|
||||
晕: '😵',
|
||||
衰: '😞',
|
||||
骷髅: '💀',
|
||||
敲打: '🔨',
|
||||
再见: '👋',
|
||||
擦汗: '😓',
|
||||
抠鼻: '🤧',
|
||||
鼓掌: '👏',
|
||||
坏笑: '😏',
|
||||
鄙视: '😒',
|
||||
委屈: '🥺',
|
||||
快哭了: '😢',
|
||||
阴险: '😈',
|
||||
亲亲: '😘',
|
||||
可怜: '🥺',
|
||||
笑脸: '😄',
|
||||
生病: '🤒',
|
||||
脸红: '😊',
|
||||
破涕为笑: '😂',
|
||||
恐惧: '😨',
|
||||
失望: '😞',
|
||||
无语: '😑',
|
||||
嘿哈: '😄',
|
||||
捂脸: '😭',
|
||||
奸笑: '😏',
|
||||
机智: '😏',
|
||||
皱眉: '😟',
|
||||
耶: '✌️',
|
||||
666: '👍',
|
||||
Emm: '😐'
|
||||
}
|
||||
|
||||
function resolveEmojiUrl(name: string): string {
|
||||
const emojiPath = getEmojiPath(name)
|
||||
return emojiPath ? emojiUrlByPath.get(emojiPath) || '' : ''
|
||||
}
|
||||
|
||||
export function renderWechatEmojiText(text: string, size = 22): ReactNode {
|
||||
if (!text || !text.includes('[')) return text
|
||||
|
||||
return text.split(/\[([^\[\]\r\n]{1,16})\]/g).map((part, index) => {
|
||||
if (index % 2 === 0) return part
|
||||
|
||||
const url = resolveEmojiUrl(part)
|
||||
if (url) {
|
||||
return (
|
||||
<img
|
||||
key={`${part}-${index}`}
|
||||
className="inline-wechat-emoji"
|
||||
src={url}
|
||||
alt={`[${part}]`}
|
||||
title={`[${part}]`}
|
||||
style={{ width: size, height: size }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return unicodeFallback[part] || `[${part}]`
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user