feat(消息记录): 增加小程序类型2和文章链接消息样式及处理逻辑

添加对小程序类型2(垂直布局)的支持,包括样式和渲染逻辑
新增文章类型消息的样式和解析处理
增加链接类型消息的样式和解析处理
优化现有小程序消息的样式和布局
This commit is contained in:
超级老白兔
2025-09-08 15:19:29 +08:00
parent 79c1a539bb
commit 4e8c728457
2 changed files with 362 additions and 61 deletions

View File

@@ -180,14 +180,15 @@
}
}
// 小程序消息样式
// 小程序消息基础样式
.miniProgramMessage {
background: #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
// 通用小程序卡片基础样式
.miniProgramCard {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
border-bottom: 1px solid #e1e8ed;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
@@ -198,6 +199,7 @@
overflow: hidden;
}
// 通用小程序元素样式
.miniProgramThumb {
width: 50px;
height: 50px;
@@ -222,6 +224,7 @@
}
.miniProgramTitle {
padding-left: 16px;
font-weight: 600;
color: #1a1a1a;
font-size: 14px;
@@ -244,6 +247,189 @@
}
}
// 类型1小程序样式默认横向布局
.miniProgramType1 {
// 继承基础样式,无需额外定义
}
// 类型2小程序样式垂直图片布局
.miniProgramType2 {
.miniProgramCardType2 {
flex-direction: column;
align-items: stretch;
padding: 0;
min-height: 220px;
max-width: 280px;
.miniProgramAppTop {
padding: 12px 16px 8px;
font-size: 13px;
font-weight: 500;
color: #495057;
background: #f8f9fa;
display: flex;
align-items: center;
&::before {
content: "📱";
font-size: 12px;
}
}
.miniProgramImageArea {
width: calc(100% - 32px);
height: 0;
padding-bottom: 75%; // 4:3 宽高比
margin: 0px 16px;
overflow: hidden;
position: relative;
background: #f8f9fa;
.miniProgramImage {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
}
}
}
.miniProgramContent {
padding: 12px 16px;
display: flex;
flex-direction: column;
gap: 8px;
.miniProgramTitle {
font-size: 14px;
font-weight: 600;
color: #212529;
line-height: 1.4;
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.miniProgramIdentifier {
font-size: 11px;
color: #6c757d;
border-radius: 8px;
display: inline-flex;
align-items: center;
align-self: flex-start;
gap: 3px;
&::before {
content: "🏷️";
font-size: 9px;
}
}
}
}
}
// 链接类型消息样式
.linkMessage {
.linkCard {
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.linkDescription {
font-size: 12px;
color: #666;
line-height: 1.4;
margin: 4px 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
}
// 文章类型消息样式
.articleMessage {
.articleCard {
flex-direction: column;
align-items: stretch;
padding: 16px;
min-height: auto;
max-width: 320px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.articleTitle {
font-size: 16px;
font-weight: 600;
color: #1a1a1a;
line-height: 1.4;
margin-bottom: 12px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.articleContent {
display: flex;
gap: 12px;
align-items: flex-start;
}
.articleTextArea {
flex: 1;
min-width: 0;
}
.articleDescription {
font-size: 13px;
color: #666;
line-height: 1.5;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
.articleImageArea {
flex-shrink: 0;
width: 80px;
height: 80px;
overflow: hidden;
border-radius: 8px;
background: #f8f9fa;
}
.articleImage {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
}
}
}
// 文件消息样式
.fileMessage {
.fileCard {
@@ -315,22 +501,8 @@
.miniProgramCard {
max-width: 260px;
padding: 10px 14px;
gap: 10px;
min-height: 56px;
border-radius: 10px;
&:hover {
transform: none; // 移动端禁用悬浮动画
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-color: #e1e8ed;
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.04),
0 1px 3px rgba(0, 0, 0, 0.06);
&::before {
opacity: 0;
}
}
}
.miniProgramThumb {

View File

@@ -9,7 +9,6 @@ import { useWeChatStore } from "@/store/module/weChat/weChat";
interface MessageRecordProps {
contract: ContractData | weChatGroup;
}
const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
const messagesEndRef = useRef<HTMLDivElement>(null);
const currentMessages = useWeChatStore(state => state.currentMessages);
@@ -187,29 +186,80 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
}
return renderErrorMessage("[表情包]");
case 49: // 小程序/其他:图文、文件
case 49: // 小程序/文章/其他:图文、文件
if (typeof content !== "string" || !content.trim()) {
return renderErrorMessage("[小程序/文件消息 - 无效内容]");
return renderErrorMessage("[小程序/文章/文件消息 - 无效内容]");
}
try {
const trimmedContent = content.trim();
// 尝试解析JSON格式的小程序消息
// 尝试解析JSON格式的消息
if (trimmedContent.startsWith("{") && trimmedContent.endsWith("}")) {
const miniProgramData = JSON.parse(trimmedContent);
const messageData = JSON.parse(trimmedContent);
// 处理文章类型消息
if (
messageData.type === "link" &&
messageData.title &&
messageData.url
) {
const { title, desc, thumbPath, url } = messageData;
return (
<div
className={`${styles.miniProgramMessage} ${styles.articleMessage}`}
>
<div
className={`${styles.miniProgramCard} ${styles.articleCard}`}
onClick={() => window.open(url, "_blank")}
>
{/* 标题在第一行 */}
<div className={styles.articleTitle}>{title}</div>
{/* 下方:文字在左,图片在右 */}
<div className={styles.articleContent}>
<div className={styles.articleTextArea}>
{desc && (
<div className={styles.articleDescription}>
{desc}
</div>
)}
</div>
{thumbPath && (
<div className={styles.articleImageArea}>
<img
src={thumbPath}
alt="文章缩略图"
className={styles.articleImage}
onError={e => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>
</div>
)}
</div>
</div>
<div className={styles.miniProgramApp}></div>
</div>
);
}
// 处理包含contentXml的小程序消息格式
if (
miniProgramData.contentXml &&
miniProgramData.type === "miniprogram"
) {
const xmlContent = miniProgramData.contentXml;
if (messageData.contentXml && messageData.type === "miniprogram") {
const xmlContent = messageData.contentXml;
// 从XML中提取title
const titleMatch = xmlContent.match(/<title>([^<]*)<\/title>/);
const title = titleMatch ? titleMatch[1] : "小程序消息";
// 从XML中提取type字段
const typeMatch = xmlContent.match(
/<weappinfo>[\s\S]*?<type>(\d+)<\/type>[\s\S]*?<\/weappinfo>/,
);
const miniProgramType = typeMatch ? parseInt(typeMatch[1]) : 1;
// 从XML中提取thumburl或使用previewImage
const thumbUrlMatch = xmlContent.match(
/<thumburl>\s*([^<]*?)\s*<\/thumburl>/,
@@ -218,7 +268,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
// 如果thumburl为空或无效使用previewImage
if (!thumbUrl || thumbUrl === "`" || thumbUrl.includes("`")) {
thumbUrl = miniProgramData.previewImage || "";
thumbUrl = messageData.previewImage || "";
}
// 清理URL中的特殊字符
@@ -232,41 +282,83 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
);
const appName = appNameMatch ? appNameMatch[1] : "小程序";
return (
<div className={styles.miniProgramMessage}>
<div className={styles.miniProgramCard}>
{thumbUrl && (
<img
src={thumbUrl}
alt="小程序缩略图"
className={styles.miniProgramThumb}
onError={e => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>
)}
<div className={styles.miniProgramInfo}>
// 根据type类型渲染不同布局
if (miniProgramType === 2) {
// 类型2图片区域布局小程序昵称、图片、标题、小程序标识
return (
<div
className={`${styles.miniProgramMessage} ${styles.miniProgramType2}`}
>
<div
className={`${styles.miniProgramCard} ${styles.miniProgramCardType2}`}
>
{/* 小程序昵称 */}
<div className={styles.miniProgramAppTop}>{appName}</div>
{/* 标题 */}
<div className={styles.miniProgramTitle}>{title}</div>
{/* 图片 */}
{thumbUrl && (
<div className={styles.miniProgramImageArea}>
<img
src={thumbUrl}
alt="小程序图片"
className={styles.miniProgramImage}
onError={e => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>
</div>
)}
<div className={styles.miniProgramContent}>
{/* 小程序标识 */}
<div className={styles.miniProgramIdentifier}>
</div>
</div>
</div>
</div>
<div className={styles.miniProgramApp}>{appName}</div>
</div>
);
);
} else {
// 默认类型:横向布局
return (
<div
className={`${styles.miniProgramMessage} ${styles.miniProgramType1}`}
>
<div className={styles.miniProgramCard}>
{thumbUrl && (
<img
src={thumbUrl}
alt="小程序缩略图"
className={styles.miniProgramThumb}
onError={e => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>
)}
<div className={styles.miniProgramInfo}>
<div className={styles.miniProgramTitle}>{title}</div>
</div>
</div>
<div className={styles.miniProgramApp}>{appName}</div>
</div>
);
}
}
// 验证传统JSON格式的小程序数据结构
if (
miniProgramData &&
typeof miniProgramData === "object" &&
(miniProgramData.title || miniProgramData.appName)
messageData &&
typeof messageData === "object" &&
(messageData.title || messageData.appName)
) {
return (
<div className={styles.miniProgramMessage}>
<div className={styles.miniProgramCard}>
{miniProgramData.thumb && (
{messageData.thumb && (
<img
src={miniProgramData.thumb}
src={messageData.thumb}
alt="小程序缩略图"
className={styles.miniProgramThumb}
onError={e => {
@@ -277,11 +369,11 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
)}
<div className={styles.miniProgramInfo}>
<div className={styles.miniProgramTitle}>
{miniProgramData.title || "小程序消息"}
{messageData.title || "小程序消息"}
</div>
{miniProgramData.appName && (
{messageData.appName && (
<div className={styles.miniProgramApp}>
{miniProgramData.appName}
{messageData.appName}
</div>
)}
</div>
@@ -404,17 +496,54 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
);
}
// 2. 检查是否为视频消息(兼容旧逻辑
// 2. 检查是否为JSON格式消息包括视频、链接等
if (contentStr.startsWith("{") && contentStr.endsWith("}")) {
try {
const videoData = JSON.parse(contentStr);
const jsonData = JSON.parse(contentStr);
// 检查是否为链接类型消息
if (jsonData.type === "link" && jsonData.title && jsonData.url) {
const { title, desc, thumbPath, url } = jsonData;
return (
<div
className={`${styles.miniProgramMessage} ${styles.miniProgramType1}`}
>
<div
className={`${styles.miniProgramCard} ${styles.linkCard}`}
onClick={() => window.open(url, "_blank")}
>
{thumbPath && (
<img
src={thumbPath}
alt="链接缩略图"
className={styles.miniProgramThumb}
onError={e => {
const target = e.target as HTMLImageElement;
target.style.display = "none";
}}
/>
)}
<div className={styles.miniProgramInfo}>
<div className={styles.miniProgramTitle}>{title}</div>
{desc && (
<div className={styles.linkDescription}>{desc}</div>
)}
</div>
</div>
<div className={styles.miniProgramApp}></div>
</div>
);
}
// 检查是否为视频消息(兼容旧逻辑)
if (
videoData &&
typeof videoData === "object" &&
videoData.previewImage &&
(videoData.tencentUrl || videoData.videoUrl)
jsonData &&
typeof jsonData === "object" &&
jsonData.previewImage &&
(jsonData.tencentUrl || jsonData.videoUrl)
) {
const previewImageUrl = String(videoData.previewImage).replace(
const previewImageUrl = String(jsonData.previewImage).replace(
/[`"']/g,
"",
);
@@ -427,7 +556,7 @@ const MessageRecord: React.FC<MessageRecordProps> = ({ contract }) => {
className={styles.videoPreview}
onClick={() => {
const videoUrl =
videoData.videoUrl || videoData.tencentUrl;
jsonData.videoUrl || jsonData.tencentUrl;
if (videoUrl) {
window.open(videoUrl, "_blank");
}