mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-18 03:57:02 +08:00
feat: 支持微信内置表情渲染
This commit is contained in:
@@ -10,3 +10,4 @@ out
|
|||||||
docs/design/
|
docs/design/
|
||||||
docs/ui-redesign-plan.md
|
docs/ui-redesign-plan.md
|
||||||
docs/ui-redesign-spec.md
|
docs/ui-redesign-spec.md
|
||||||
|
AGENTS.md
|
||||||
|
|||||||
+2
-1
@@ -39,7 +39,8 @@
|
|||||||
"fzstd": "^0.1.1",
|
"fzstd": "^0.1.1",
|
||||||
"koffi": "^3.1.0",
|
"koffi": "^3.1.0",
|
||||||
"openai": "^6.10.0",
|
"openai": "^6.10.0",
|
||||||
"silk-wasm": "^3.7.1"
|
"silk-wasm": "^3.7.1",
|
||||||
|
"wechat-emojis": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||||
|
|||||||
Generated
+7
@@ -33,6 +33,7 @@ specifiers:
|
|||||||
silk-wasm: ^3.7.1
|
silk-wasm: ^3.7.1
|
||||||
typescript: ^5.9.3
|
typescript: ^5.9.3
|
||||||
vite: ^7.2.6
|
vite: ^7.2.6
|
||||||
|
wechat-emojis: ^1.0.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
'@electron-toolkit/preload': 3.0.2_electron@43.1.0
|
'@electron-toolkit/preload': 3.0.2_electron@43.1.0
|
||||||
@@ -42,6 +43,7 @@ dependencies:
|
|||||||
koffi: 3.1.0
|
koffi: 3.1.0
|
||||||
openai: 6.10.0
|
openai: 6.10.0
|
||||||
silk-wasm: 3.7.1
|
silk-wasm: 3.7.1
|
||||||
|
wechat-emojis: 1.0.2
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@electron-toolkit/eslint-config-prettier': 3.0.0_fiitszekoa4sqtbwyewxi6kyy4
|
'@electron-toolkit/eslint-config-prettier': 3.0.0_fiitszekoa4sqtbwyewxi6kyy4
|
||||||
@@ -4817,6 +4819,11 @@ packages:
|
|||||||
defaults: 1.0.4
|
defaults: 1.0.4
|
||||||
dev: true
|
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:
|
/which-boxed-primitive/1.1.1:
|
||||||
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|||||||
@@ -364,6 +364,13 @@ body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-wechat-emoji {
|
||||||
|
display: inline-block;
|
||||||
|
margin: 0 1px;
|
||||||
|
vertical-align: -5px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
.voice-bubble {
|
.voice-bubble {
|
||||||
min-width: 138px;
|
min-width: 138px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Message, Contact } from '../../../shared/types'
|
|||||||
import { VoicePlayer } from './VoicePlayer'
|
import { VoicePlayer } from './VoicePlayer'
|
||||||
import { RichMessageBubble } from './RichMessageBubble'
|
import { RichMessageBubble } from './RichMessageBubble'
|
||||||
import { ImageBubble } from './ImageBubble'
|
import { ImageBubble } from './ImageBubble'
|
||||||
|
import { renderWechatEmojiText } from '../utils/wechatEmojiText'
|
||||||
import {
|
import {
|
||||||
buildGroupReportInput,
|
buildGroupReportInput,
|
||||||
GROUP_REPORT_SYSTEM_PROMPT,
|
GROUP_REPORT_SYSTEM_PROMPT,
|
||||||
@@ -416,7 +417,7 @@ const ChatWindow: React.FC<ChatWindowProps> = ({
|
|||||||
) : isRichMedia && msg.contentData ? (
|
) : isRichMedia && msg.contentData ? (
|
||||||
<RichMessageBubble contentData={msg.contentData} />
|
<RichMessageBubble contentData={msg.contentData} />
|
||||||
) : (
|
) : (
|
||||||
<div className="message-text">{msg.content}</div>
|
<div className="message-text">{renderWechatEmojiText(msg.content)}</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="message-meta">
|
<div className="message-meta">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ParsedContent } from '../../../shared/types'
|
import { ParsedContent } from '../../../shared/types'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import type { JSX, MouseEvent } from 'react'
|
import type { JSX, MouseEvent } from 'react'
|
||||||
|
import { renderWechatEmojiText } from '../utils/wechatEmojiText'
|
||||||
|
|
||||||
const stickerDataUrlCache = new Map<string, string>()
|
const stickerDataUrlCache = new Map<string, string>()
|
||||||
|
|
||||||
@@ -26,7 +27,9 @@ export function RichMessageBubble({ contentData }: RichMessageBubbleProps): JSX.
|
|||||||
return <SystemBubble data={contentData} />
|
return <SystemBubble data={contentData} />
|
||||||
case 'unknown':
|
case 'unknown':
|
||||||
return (
|
return (
|
||||||
<div className="message-text">{(contentData as { raw?: string }).raw || '[未知消息]'}</div>
|
<div className="message-text">
|
||||||
|
{renderWechatEmojiText((contentData as { raw?: string }).raw || '[未知消息]')}
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
default:
|
default:
|
||||||
return <div className="message-text">[不支持的消息类型]</div>
|
return <div className="message-text">[不支持的消息类型]</div>
|
||||||
@@ -207,15 +210,15 @@ function QuoteBubble({ data }: { data: Extract<ParsedContent, { type: 'quote' }>
|
|||||||
<div className="quote-message">
|
<div className="quote-message">
|
||||||
<div className="quoted-message">
|
<div className="quoted-message">
|
||||||
{quotedSender && <span className="quoted-sender">{quotedSender}</span>}
|
{quotedSender && <span className="quoted-sender">{quotedSender}</span>}
|
||||||
<span className="quoted-text">{quotedText}</span>
|
<span className="quoted-text">{renderWechatEmojiText(quotedText, 18)}</span>
|
||||||
</div>
|
</div>
|
||||||
{replyText && <div className="quote-reply">{replyText}</div>}
|
{replyText && <div className="quote-reply">{renderWechatEmojiText(replyText)}</div>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SystemBubble({ data }: { data: Extract<ParsedContent, { type: 'system' }> }): JSX.Element {
|
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 {
|
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