feat(消息记录): 增加小程序类型2和文章链接消息样式及处理逻辑
添加对小程序类型2(垂直布局)的支持,包括样式和渲染逻辑 新增文章类型消息的样式和解析处理 增加链接类型消息的样式和解析处理 优化现有小程序消息的样式和布局
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user