From ad5093900511bb07be910975f07f3939bb2322c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B5=E6=91=87=E5=B0=8F=E5=AD=90?= <969409112@qq.com> Date: Sat, 11 Jul 2026 01:13:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=86=85=E7=BD=AE=E8=A1=A8=E6=83=85=E6=B8=B2=E6=9F=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + package.json | 3 +- pnpm-lock.yaml | 7 ++ src/renderer/src/assets/main.css | 7 ++ src/renderer/src/components/ChatWindow.tsx | 3 +- .../src/components/RichMessageBubble.tsx | 11 +- src/renderer/src/types/wechat-emojis.d.ts | 3 + src/renderer/src/utils/wechatEmojiText.tsx | 110 ++++++++++++++++++ 8 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 src/renderer/src/types/wechat-emojis.d.ts create mode 100644 src/renderer/src/utils/wechatEmojiText.tsx diff --git a/.gitignore b/.gitignore index a8558d8..5312024 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ out docs/design/ docs/ui-redesign-plan.md docs/ui-redesign-spec.md +AGENTS.md diff --git a/package.json b/package.json index 867c70c..dffa416 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 106d8a7..040b4e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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'} diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index 1e863c0..c3e06de 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -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; } diff --git a/src/renderer/src/components/ChatWindow.tsx b/src/renderer/src/components/ChatWindow.tsx index 7c928a1..eb99bd3 100644 --- a/src/renderer/src/components/ChatWindow.tsx +++ b/src/renderer/src/components/ChatWindow.tsx @@ -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 = ({ ) : isRichMedia && msg.contentData ? ( ) : ( -
{msg.content}
+
{renderWechatEmojiText(msg.content)}
)}
diff --git a/src/renderer/src/components/RichMessageBubble.tsx b/src/renderer/src/components/RichMessageBubble.tsx index 1739ebc..2249834 100644 --- a/src/renderer/src/components/RichMessageBubble.tsx +++ b/src/renderer/src/components/RichMessageBubble.tsx @@ -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() @@ -26,7 +27,9 @@ export function RichMessageBubble({ contentData }: RichMessageBubbleProps): JSX. return case 'unknown': return ( -
{(contentData as { raw?: string }).raw || '[未知消息]'}
+
+ {renderWechatEmojiText((contentData as { raw?: string }).raw || '[未知消息]')} +
) default: return
[不支持的消息类型]
@@ -207,15 +210,15 @@ function QuoteBubble({ data }: { data: Extract
{quotedSender && {quotedSender}} - {quotedText} + {renderWechatEmojiText(quotedText, 18)}
- {replyText &&
{replyText}
} + {replyText &&
{renderWechatEmojiText(replyText)}
}
) } function SystemBubble({ data }: { data: Extract }): JSX.Element { - return
{data.content || '[系统消息]'}
+ return
{renderWechatEmojiText(data.content || '[系统消息]')}
} function getUrlHost(url?: string): string { diff --git a/src/renderer/src/types/wechat-emojis.d.ts b/src/renderer/src/types/wechat-emojis.d.ts new file mode 100644 index 0000000..5182eb9 --- /dev/null +++ b/src/renderer/src/types/wechat-emojis.d.ts @@ -0,0 +1,3 @@ +declare module 'wechat-emojis' { + export function getEmojiPath(name: string): string | undefined +} diff --git a/src/renderer/src/utils/wechatEmojiText.tsx b/src/renderer/src/utils/wechatEmojiText.tsx new file mode 100644 index 0000000..293d78a --- /dev/null +++ b/src/renderer/src/utils/wechatEmojiText.tsx @@ -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 + +const emojiUrlByPath = new Map() + +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 = { + 微笑: '🙂', + 撇嘴: '😒', + 色: '😍', + 发呆: '😳', + 得意: '😎', + 流泪: '😭', + 害羞: '😊', + 闭嘴: '🤐', + 睡: '😴', + 大哭: '😭', + 尴尬: '😅', + 发怒: '😡', + 调皮: '😜', + 呲牙: '😁', + 惊讶: '😮', + 难过: '😞', + 抓狂: '😫', + 吐: '🤮', + 偷笑: '🤭', + 愉快: '😄', + 白眼: '🙄', + 傲慢: '😤', + 困: '😪', + 惊恐: '😱', + 憨笑: '😄', + 悠闲: '😌', + 咒骂: '😤', + 疑问: '❓', + 嘘: '🤫', + 晕: '😵', + 衰: '😞', + 骷髅: '💀', + 敲打: '🔨', + 再见: '👋', + 擦汗: '😓', + 抠鼻: '🤧', + 鼓掌: '👏', + 坏笑: '😏', + 鄙视: '😒', + 委屈: '🥺', + 快哭了: '😢', + 阴险: '😈', + 亲亲: '😘', + 可怜: '🥺', + 笑脸: '😄', + 生病: '🤒', + 脸红: '😊', + 破涕为笑: '😂', + 恐惧: '😨', + 失望: '😞', + 无语: '😑', + 嘿哈: '😄', + 捂脸: '😭', + 奸笑: '😏', + 机智: '😏', + 皱眉: '😟', + 耶: '✌️', + 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 ( + {`[${part}]`} + ) + } + + return unicodeFallback[part] || `[${part}]` + }) +}