refactor(Skeleton): 重构骨架屏组件结构和样式

- 将原有单一骨架屏拆分为头部、侧边栏、聊天区域和右侧面板多个模块
- 优化骨架屏的样式细节,使其更接近真实UI
- 添加加载状态提示和交互效果
This commit is contained in:
超级老白兔
2025-09-02 14:20:03 +08:00
parent 97bb1c1fa9
commit 45c496313d
3 changed files with 347 additions and 182 deletions

View File

@@ -1,111 +1,185 @@
.skeletonLayout {
height: 100vh;
// 头部骨架样式
.headerSkeleton {
display: flex;
align-items: center;
justify-content: space-between;
height: 100%;
padding: 0 16px;
}
.headerLeft {
display: flex;
align-items: center;
gap: 12px;
}
.headerCenter {
flex: 1;
display: flex;
justify-content: center;
}
.headerRight {
display: flex;
align-items: center;
}
// 左侧边栏骨架样式
.siderContent {
height: 100%;
display: flex;
flex-direction: column;
}
.skeletonHeader {
height: 64px;
padding: 0 24px;
display: flex;
align-items: center;
background-color: #fff;
border-bottom: 1px solid #f0f0f0;
.searchBox {
padding: 16px;
border-bottom: 1px solid #f0f0f0;
}
.tabBar {
display: flex;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.contactList {
flex: 1;
overflow-y: auto;
}
.contactItem {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid #f5f5f5;
&:hover {
background-color: #f8f8f8;
}
}
.skeletonVerticalSider {
background-color: #fff;
border-right: 1px solid #f0f0f0;
.contactInfo {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.verticalUserList {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 0;
.contactTime {
font-size: 12px;
color: #999;
}
.verticalUserItem {
margin-bottom: 16px;
}
}
// 聊天区域骨架样式
.chatContent {
display: flex;
flex-direction: column;
background: #f5f5f5;
}
.chatHeader {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
background: #fff;
border-bottom: 1px solid #f0f0f0;
}
.chatHeaderLeft {
display: flex;
align-items: center;
gap: 12px;
}
.chatHeaderRight {
display: flex;
gap: 8px;
}
.messageArea {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.loadingTip {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.loadingText {
font-size: 14px;
color: #666;
text-align: center;
}
.inputArea {
background: #fff;
border-top: 1px solid #f0f0f0;
}
.inputToolbar {
display: flex;
gap: 8px;
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
}
.inputField {
display: flex;
gap: 12px;
padding: 12px 16px;
align-items: flex-end;
}
// 右侧面板骨架样式
.rightPanel {
background: #fff;
border-left: 1px solid #f0f0f0;
}
.profileSection {
padding: 24px;
}
.profileHeader {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding-bottom: 24px;
border-bottom: 1px solid #f0f0f0;
}
.profileDetails {
padding: 24px 0;
border-bottom: 1px solid #f0f0f0;
}
.detailItem {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
&:last-child {
margin-bottom: 0;
}
}
.skeletonSider {
background-color: #fff;
border-right: 1px solid #f0f0f0;
padding: 16px;
.tagSection {
padding-top: 24px;
}
.searchSkeleton {
margin-bottom: 16px;
}
.tabsSkeleton {
display: flex;
justify-content: space-around;
margin-bottom: 16px;
}
.contactListSkeleton {
.contactItemSkeleton {
display: flex;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f5f5f5;
.contactInfoSkeleton {
margin-left: 12px;
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
}
}
}
.skeletonMainContent {
background-color: #f5f5f5;
padding: 16px;
display: flex;
flex-direction: column;
.chatHeaderSkeleton {
background-color: #fff;
padding: 16px;
display: flex;
align-items: center;
gap: 12px;
border-radius: 8px 8px 0 0;
}
.chatContentSkeleton {
flex: 1;
background-color: #fff;
padding: 16px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
.messageSkeleton {
display: flex;
align-items: flex-start;
gap: 8px;
&.leftMessage {
align-self: flex-start;
}
&.rightMessage {
align-self: flex-end;
flex-direction: row-reverse;
}
}
}
.inputAreaSkeleton {
background-color: #fff;
padding: 16px;
border-radius: 0 0 8px 8px;
border-top: 1px solid #f0f0f0;
}
}
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}

View File

@@ -1,5 +1,6 @@
import React from "react";
import { Skeleton, Layout } from "antd";
import { Skeleton, Layout, Spin } from "antd";
import { LoadingOutlined } from "@ant-design/icons";
import styles from "./index.module.scss";
import pageStyles from "../../index.module.scss";
@@ -17,100 +18,190 @@ interface PageSkeletonProps {
const PageSkeleton: React.FC<PageSkeletonProps> = ({ loading, children }) => {
if (!loading) return <>{children}</>;
const antIcon = <LoadingOutlined style={{ fontSize: 16 }} spin />;
return (
<Layout className={pageStyles.ckboxLayout}>
{/* 顶部标题栏骨架 */}
<Header className={pageStyles.header}>
<Skeleton.Button active size="large" shape="square" block />
<div className={styles.headerSkeleton}>
<div className={styles.headerLeft}>
<Skeleton.Avatar active size="small" shape="circle" />
<Skeleton.Input active size="small" style={{ width: "80px" }} />
</div>
<div className={styles.headerCenter}>
<Skeleton.Input active size="small" style={{ width: "200px" }} />
</div>
<div className={styles.headerRight}>
<Skeleton.Button active size="small" style={{ width: "60px" }} />
</div>
</div>
</Header>
<Layout>
{/* 垂直侧边栏骨架 */}
<Sider width={60} className={pageStyles.verticalSider}>
<div className={styles.verticalUserList}>
{Array(5)
.fill(null)
.map((_, index) => (
<div
key={`vertical-${index}`}
className={styles.verticalUserItem}
>
<Skeleton.Avatar active size="large" shape="circle" />
</div>
))}
</div>
</Sider>
{/* 左侧联系人边栏骨架 */}
{/* 左侧联系人列表骨架 */}
<Sider width={280} className={pageStyles.sider}>
<div className={styles.searchSkeleton}>
<Skeleton.Input active size="small" block />
</div>
<div className={styles.tabsSkeleton}>
<Skeleton.Button
active
size="small"
shape="square"
style={{ width: "30%" }}
/>
<Skeleton.Button
active
size="small"
shape="square"
style={{ width: "30%" }}
/>
</div>
<div className={styles.contactListSkeleton}>
{Array(8)
.fill(null)
.map((_, index) => (
<div
key={`contact-${index}`}
className={styles.contactItemSkeleton}
>
<Skeleton.Avatar active size="large" shape="circle" />
<div className={styles.contactInfoSkeleton}>
<Skeleton.Input
active
size="small"
style={{ width: "60%" }}
/>
<Skeleton.Input
active
size="small"
style={{ width: "80%" }}
/>
<div className={styles.siderContent}>
{/* 搜索框 */}
<div className={styles.searchBox}>
<Skeleton.Input active size="large" block />
</div>
{/* 标签栏 */}
<div className={styles.tabBar}>
<Skeleton.Button active size="small" style={{ width: "60px" }} />
<Skeleton.Button active size="small" style={{ width: "60px" }} />
<Skeleton.Button active size="small" style={{ width: "60px" }} />
</div>
{/* 联系人列表 */}
<div className={styles.contactList}>
{Array(10)
.fill(null)
.map((_, index) => (
<div key={`contact-${index}`} className={styles.contactItem}>
<Skeleton.Avatar active size="large" shape="circle" />
<div className={styles.contactInfo}>
<Skeleton.Input
active
size="small"
style={{ width: "80px" }}
/>
<Skeleton.Input
active
size="small"
style={{ width: "120px" }}
/>
</div>
<div className={styles.contactTime}>
<Skeleton.Input
active
size="small"
style={{ width: "40px" }}
/>
</div>
</div>
</div>
))}
))}
</div>
</div>
</Sider>
{/* 主内容区骨架 */}
<Content className={styles.skeletonMainContent}>
<div className={styles.chatHeaderSkeleton}>
<Skeleton.Avatar active size="large" shape="circle" />
<Skeleton.Input active size="small" style={{ width: "30%" }} />
{/* 主聊天区域骨架 */}
<Content className={styles.chatContent}>
{/* 聊天头部 */}
<div className={styles.chatHeader}>
<div className={styles.chatHeaderLeft}>
<Skeleton.Avatar active size="large" shape="circle" />
<Skeleton.Input active size="small" style={{ width: "100px" }} />
</div>
<div className={styles.chatHeaderRight}>
<Skeleton.Button active size="small" shape="circle" />
<Skeleton.Button active size="small" shape="circle" />
<Skeleton.Button active size="small" shape="circle" />
</div>
</div>
<div className={styles.chatContentSkeleton}>
{Array(5)
.fill(null)
.map((_, index) => (
<div
key={`message-${index}`}
className={`${styles.messageSkeleton} ${index % 2 === 0 ? styles.leftMessage : styles.rightMessage}`}
>
<Skeleton.Avatar active size="small" shape="circle" />
<Skeleton.Input
active
size="small"
style={{ width: index % 2 === 0 ? "60%" : "40%" }}
/>
</div>
))}
{/* 消息区域 */}
<div className={styles.messageArea}>
<div className={styles.loadingTip}>
<Spin indicator={antIcon} />
<span className={styles.loadingText}>
...
</span>
</div>
</div>
<div className={styles.inputAreaSkeleton}>
<Skeleton.Input active size="large" block />
{/* 输入区域 */}
<div className={styles.inputArea}>
<div className={styles.inputToolbar}>
<Skeleton.Button active size="small" shape="circle" />
<Skeleton.Button active size="small" shape="circle" />
<Skeleton.Button active size="small" shape="circle" />
<Skeleton.Button active size="small" shape="circle" />
<Skeleton.Button active size="small" shape="circle" />
<Skeleton.Button active size="small" shape="circle" />
</div>
<div className={styles.inputField}>
<Skeleton.Input active size="large" block />
<Skeleton.Button active size="large" style={{ width: "60px" }} />
</div>
</div>
</Content>
{/* 右侧个人信息面板骨架 */}
<Sider width={300} className={styles.rightPanel}>
<div className={styles.profileSection}>
<div className={styles.profileHeader}>
<Skeleton.Avatar active size={80} shape="circle" />
<Skeleton.Input
active
size="small"
style={{ width: "100px", marginTop: "12px" }}
/>
<Skeleton.Input
active
size="small"
style={{ width: "60px", marginTop: "4px" }}
/>
</div>
<div className={styles.profileDetails}>
<div className={styles.detailItem}>
<Skeleton.Input active size="small" style={{ width: "60px" }} />
<Skeleton.Input
active
size="small"
style={{ width: "120px" }}
/>
</div>
<div className={styles.detailItem}>
<Skeleton.Input active size="small" style={{ width: "40px" }} />
<Skeleton.Input active size="small" style={{ width: "80px" }} />
</div>
<div className={styles.detailItem}>
<Skeleton.Input active size="small" style={{ width: "40px" }} />
<Skeleton.Input
active
size="small"
style={{ width: "100px" }}
/>
</div>
<div className={styles.detailItem}>
<Skeleton.Input active size="small" style={{ width: "40px" }} />
<Skeleton.Input
active
size="small"
style={{ width: "140px" }}
/>
</div>
</div>
<div className={styles.tagSection}>
<Skeleton.Input
active
size="small"
style={{ width: "40px", marginBottom: "12px" }}
/>
<div className={styles.tags}>
<Skeleton.Button
active
size="small"
style={{ width: "50px", marginRight: "8px" }}
/>
<Skeleton.Button
active
size="small"
style={{ width: "60px", marginRight: "8px" }}
/>
<Skeleton.Button
active
size="small"
style={{ width: "70px" }}
/>
</div>
</div>
</div>
</Sider>
</Layout>
</Layout>
);