抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

React 即时通信 UI 实战第六章。React 即时通信 UI 实战为峰华前端工程师推出的 React 实战课程,以聊天(即时通信)为原型,构建了一整套的 UI 组件库,课程重点在于 UI 组件的分析和实现,力求打造自用组件库。本章包括内容区中的标题栏、会话气泡、会话窗口、语音消息等组件。以下为我在学习和实战练习过程中所做的笔记,可供参考。

一、标题栏组件开发

使用 Hygen 创建一个标题栏组件:

1
hygen component new TitleBar

index.js 文件中编辑标题栏组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from "react";
import PropTypes from "prop-types";
import StyledTitleBar, { Actions, Title } from "./style";
import Avatar from "components/Avatar";
import Paragraph from "components/Paragraph";
import Text from "components/Text";
import Icon from "components/Icon";

import face from "assets/images/face-male-3.jpg";
import { ReactComponent as Call } from "assets/icon/call.svg";
import { ReactComponent as Camera } from "assets/icon/camera.svg";
import { ReactComponent as Options } from "assets/icon/options.svg";

function TitleBar({ children, ...rest }) {
return (
<StyledTitleBar {...rest}>
<Avatar status="offline" src={face} />
<Title>
<Paragraph size="large">慕容天宇</Paragraph>
<Paragraph type="secondary">
<Text>离线</Text>
<Text>· 最后阅读:3小时前</Text>
</Paragraph>
</Title>
<Actions>
<Icon opacity={0.3} icon={Call} />
<Icon opacity={0.3} icon={Camera} />
<Icon opacity={0.3} icon={Options} />
</Actions>
</StyledTitleBar>
);
}

TitleBar.propTypes = {
children: PropTypes.any
};

export default TitleBar;

再在 style.js 中修改样式,使用 Grid 布局:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import StyledIcon from "components/Icon/style";
import styled from "styled-components";

const Title = styled.div`
display: grid;
`;

const Actions = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
${StyledIcon} {
cursor: pointer;
}
`;

const StyledTitleBar = styled.div`
display: grid;
grid-template-columns: 62px 1fr 112px;
padding: 30px;
max-height: 110px;
border-bottom: 1px solid ${({ theme }) => theme.gray4};
`;

export default StyledTitleBar;
export { Actions, Title };

最后编辑 titleBar.stories.js 文件:

1
2
3
4
5
6
7
8
9
import React from "react";
import TitleBar from ".";

export default {
title: "UI 组件/TitleBar",
component: TitleBar,
};

export const Default = () => <TitleBar />;

二、会话气泡组件开发

会话气泡用于突出显示聊天的内容,使用 Hygen 创建一个标题栏组件:

1
hygen component new ChatBubble

index.js 文件中编辑会话气泡组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import React from "react";
import PropTypes from "prop-types";
import StyledChatBubble, { Bubble, BubbleTip, Time, MessageText, } from "./style";

import { ReactComponent as BubbleTipIcon } from "assets/icon/bubbleTip.svg";

function ChatBubble({ children, type, time, ...rest }) {
return (
<StyledChatBubble type={type} {...rest}>
<Bubble>
<BubbleTip icon={BubbleTipIcon} width={40} height={28} color="white" />
<MessageText>{children}</MessageText>
</Bubble>
<Time>{time}</Time>
</StyledChatBubble>
);
}

ChatBubble.propTypes = {
children: PropTypes.any,
type: PropTypes.oneOf(["mine"]),
time: PropTypes.string,
};

export default ChatBubble;

再在 style.js 中修改样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import styled, { css } from "styled-components";
import Paragraph from "components/Paragraph";
import Icon from "components/Icon";
import Text from "components/Text";

const Time = styled(Paragraph).attrs({ type: "secondary", size: "small" })`
margin: 6px;
margin-left: 24px;
word-spacing: 1rem;
`;

const BubbleTip = styled(Icon)`
position: absolute;
bottom: -15%;
left: 0;
z-index: 5;
`;

const Bubble = styled.div`
padding: 15px 22px;
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.1);
border-radius: 100px;
position: relative;
z-index: 10;
`;

const MessageText = styled(Text)``;

const typeVariants = {
mine: css`
${Bubble} {
background-color: ${({ theme }) => theme.primaryColor};
}

${BubbleTip} {
transform: rotateY(180deg);
left: unset;
right: 0;

path {
fill: ${({ theme }) => theme.primaryColor};
}
}

${Time} {
text-align: right;
margin-left: 0;
margin-right: 24px;
}

${MessageText} {
color: white;
}
`,
};

const StyledChatBubble = styled.div`
display: flex;
flex-direction: column;
${({ type }) => type && typeVariants[type]}
`;

export default StyledChatBubble;
export { Bubble, BubbleTip, Time, MessageText };

最后编辑 chatBubble.stories.js 文件,使用 decorators 设置 stories 中默认边距:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import ChatBubble from ".";

export default {
title: "UI 组件/ChatBubble",
component: ChatBubble,
decorators: [(storyFn) => <div style={{ padding: "24px" }}>{storyFn()}</div>]
};

export const FromOthers = () => (
<ChatBubble time="昨天 下午14:26">这是一条其它人发送的聊天消息</ChatBubble>
);

export const Mine = () => (
<ChatBubble type="mine" time="昨天 下午16:30">
这是一条我自己发送的聊天消息
</ChatBubble>
);

三、语音消息组件开发

使用 Hygen 创建一个语音消息组件:

1
hygen component new VoiceMessage

index.js 文件中编辑语音消息组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import React from "react";
import PropTypes from "prop-types";
import StyledVoiceMessage from "./style";

import { ReactComponent as Play } from "assets/icon/play.svg";
import { ReactComponent as Wave } from "assets/icon/wave.svg";
import { useTheme } from "styled-components";
import Button from "components/Button";
import Icon from "components/Icon";
import Text from "components/Text";

function VoiceMessage({ children, time, type, ...rest }) {
const theme = useTheme();
return (
<StyledVoiceMessage type={type} {...rest}>
<Button size="40px">
<Icon icon={Play} color="white" width="14" height="16" style={{ transform: "translateX(1px)" }} />
</Button>
<Icon icon={Wave} width="100%" height="100%" color={theme.primaryColor} />
<Text bold>{time}</Text>
</StyledVoiceMessage>
);
}

VoiceMessage.propTypes = {
children: PropTypes.any,
type: PropTypes.oneOf(["mine"]),
time: PropTypes.string,
};

export default VoiceMessage;

再在 style.js 中修改样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import styled, { css } from "styled-components";
import StyledButton from "components/Button/style";
import StyledIcon from "components/Icon/style";
import StyledText from "components/Text/style";

const typeVariants = {
mine: css`
${StyledButton} {
background-color: white;

${StyledIcon} path {
fill: ${({ theme }) => theme.primaryColor};
}
}
& > ${StyledIcon} path {
fill: white;
}

& > ${StyledText} {
color: white;
}
`,
};

const StyledVoiceMessage = styled.div`
display: flex;
align-items: center;

& > *:first-child {
flex-shrink: 0;
}

& > *:not(:first-child) {
margin-left: 16px;
}

${({ type }) => type && typeVariants[type]}
`;

export default StyledVoiceMessage;

编辑 voiceMessage.stories.js 文件:

1
2
3
4
5
6
7
8
9
import React from "react";
import VoiceMessage from ".";

export default {
title: "UI 组件/VoiceMessage",
component: VoiceMessage,
};

export const Default = () => <VoiceMessage time="01:25" />;

最后将语音条添加到会话气泡中,编辑 chatBubble.stories.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import VoiceMessage from "components/VoiceMessage";
import React from "react";
import ChatBubble from ".";

export default {
title: "UI 组件/ChatBubble",
component: ChatBubble,
decorators: [(storyFn) => <div style={{ padding: "24px" }}>{storyFn()}</div>]
};

export const FromOthers = () => (
<ChatBubble time="昨天 下午14:26">这是一条其它人发送的聊天消息</ChatBubble>
);

export const Mine = () => (
<ChatBubble type="mine" time="昨天 下午16:30">
这是一条我自己发送的聊天消息
</ChatBubble>
);

export const VoiceMessageType = () => (
<ChatBubble time="昨天 下午18:30">
<VoiceMessage time="01:24" />
</ChatBubble>
);

export const VoiceMessageTypeMine = () => (
<ChatBubble type="mine" time="昨天 下午18:30">
<VoiceMessage type="mine" time="01:24" />
</ChatBubble>
);

四、Emoji 组件开发

Emoji 组件用来显示表情,可以直接使用操作系统自带表情,再用 React 封装,根据可访问原则,Emoji 应用 span 标签包装,并设置 rolearia-label 属性:

1
<span role="img" aria-label="smirk">😜</span>

使用 Hygen 创建一个 Emoji 组件:

1
hygen component new Emoji

index.js 文件中编辑标题栏组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from "react";
import PropTypes from "prop-types";
import StyledEmoji from "./style";

function Emoji({ children, label, ...rest }) {
return (
<StyledEmoji role="img" aria-label={label} {...rest}>
{children}
</StyledEmoji>
);
}

Emoji.propTypes = {
children: PropTypes.any,
label: PropTypes.string,
};

export default Emoji;

再在 style.js 中修改样式,使用 span 渲染:

1
2
3
4
5
import styled from "styled-components";

const StyledEmoji = styled.span``;

export default StyledEmoji;

编辑 emoji.stories.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from "react";
import Emoji from ".";

export default {
title: "UI 组件/Emoji",
component: Emoji,
};

/* eslint-disable jsx-a11y/accessible-emoji */
export const Default = () => (
<div>
<Emoji label="smile">😄</Emoji>
<Emoji label="todo"></Emoji>
<Emoji label="clock">🕔</Emoji>
</div>
);

最后编辑 chatBubble.stories.js 文件,给 Mine 输入添加笑脸 Emoji:

1
2
3
4
5
6
7
8
import Emoji from "components/Emoji";

/* eslint-disable jsx-a11y/accessible-emoji */
export const Mine = () => (
<ChatBubble type="mine" time="昨天 下午16:30">
这是一条我自己发送的聊天消息<Emoji label="smile">😄</Emoji>
</ChatBubble>
);

五、Popover 组件开发

弹出层组件 Popover 用于显示额外的弹出层,使用 Hygen 创建一个弹出层组件:

1
hygen component new Popover

index.js 文件中编辑标题栏组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React, { useState } from "react";
import PropTypes from "prop-types";
import StyledPopover, { Content, Triangle, Target } from "./style";

function Popover({ children, content, offset = {}, onVisible, onHide, ...rest }) {
const [visible, setVisible] = useState(false);

const handleClick = () => {
if (visible) {
setVisible(false);
onHide && onHide();
} else {
setVisible(true);
onVisible && onVisible();
}
};

return (
<StyledPopover onClick={handleClick} {...rest}>
<Content visible={visible} offset={offset}>
{content}
</Content>
<Triangle visible={visible} offset={offset} />
<Target>{children}</Target>
</StyledPopover>
);
}

Popover.propTypes = {
children: PropTypes.any,
content: PropTypes.any,
offset: PropTypes.shape({ x: PropTypes.string, y: PropTypes.string }),
onVisible: PropTypes.func,
onHide: PropTypes.func,
};

export default Popover;

再在 style.js 中修改样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import styled from "styled-components";

const Content = styled.div`
background: ${({ theme }) => theme.background};
border-radius: 21px;
box-shadow: 0px 8px 40px rgba(0, 0, 0, 0.12);
padding: 12px 30px;
position: absolute;

bottom: calc(100% + 12px);

${({ offset }) =>
offset && `transform: translate(${offset.x || 0}, ${offset.y || 0})`};
${({ visible }) => !visible && `display: none`};
`;

const Triangle = styled.div`
position: absolute;
width: 0;
height: 0;
border-style: solid;
border-width: 6px 6px 0 6px;
border-color: ${({ theme }) => theme.background} transparent transparent
transparent;
left: calc(50% - 6px);
bottom: calc(100% + 12px - 5px);

${({ offset }) => offset && `transform: translateY(${offset.y || 0});`}
${({ visible }) => !visible && `display: none`};
`;

const Target = styled.div``;

const StyledPopover = styled.div`
display: flex;
justify-content: center;
position: relative;
`;

export default StyledPopover;
export { Content, Target, Triangle };

最后编辑 popover.stories.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from "react";
import Popover from ".";
import Button from "components/Button";

export default {
title: "UI 组件/Popover",
component: Popover,
};

export const Default = () => (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "50vh",
}}
>
<Popover content="Hello!">
<Button shape="rect">点我</Button>
</Popover>
</div>
);

export const WithOffset = () => (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "50vh",
}}
>
<Popover offset={{ x: "-25%" }} content={"Hello!"}>
<Button shape="rect">点我</Button>
</Popover>
</div>
);

六、底部操作组件开发

使用 Hygen 创建一个底部操作组件:

1
hygen component new Footer

index.js 文件中编辑标题栏组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React, { useState } from "react";
import PropTypes from "prop-types";
import StyledFooter, { IconContainer, StyledPopoverContent } from "./style";

import { ReactComponent as ClipIcon } from "assets/icon/clip.svg";
import { ReactComponent as SmileIcon } from "assets/icon/smile.svg";
import { ReactComponent as MicrophoneIcon } from "assets/icon/microphone.svg";
import { ReactComponent as PlaneIcon } from "assets/icon/plane.svg";
import { ReactComponent as OptionsIcon } from "assets/icon/options.svg";
import Input from "components/Input";
import Icon from "components/Icon";
import Button from "components/Button";
import Emoji from "components/Emoji";
import Popover from "components/Popover";
import { useTheme } from "styled-components";

function Footer({ animeProps, style, children, ...rest }) {
const [emojiIconActive, setEmojiIconActive] = useState(false);
const theme = useTheme();
return (
<StyledFooter style={{ ...style, ...animeProps }} {...rest}>
<Input
placeholder="输入想和对方说的话"
prefix={<Icon icon={ClipIcon} />}
suffix={
<IconContainer>
<Popover
content={<PopoverContent />}
offset={{ x: "-25%" }}
onVisible={() => setEmojiIconActive(true)}
onHide={() => setEmojiIconActive(false)}
>
<Icon
icon={SmileIcon}
color={emojiIconActive ? undefined : theme.gray3}
/>
</Popover>
<Icon icon={MicrophoneIcon} />
<Button size="52px">
<Icon
icon={PlaneIcon}
color="white"
style={{ transform: "translateX(-2px)" }}
/>
</Button>
</IconContainer>
}
/>
</StyledFooter>
);
}

/* eslint-disable jsx-a11y/accessible-emoji */
function PopoverContent(props) {
return (
<StyledPopoverContent>
<Emoji label="smile">😊</Emoji>
<Emoji label="grinning">😆</Emoji>
<Emoji label="thumbup">👍</Emoji>
<Emoji label="indexfingerup">☝️</Emoji>
<Emoji label="ok">👌</Emoji>
<Emoji label="handsputtogether">🙏</Emoji>
<Emoji label="smilewithsunglasses">😎</Emoji>
<Emoji label="flexedbicep">💪</Emoji>
<Icon icon={OptionsIcon} style={{ marginLeft: "24px" }} />
</StyledPopoverContent>
);
}

Footer.propTypes = {
children: PropTypes.any,
};

export default Footer;

再在 style.js 中修改样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import styled from "styled-components";

const IconContainer = styled.div`
display: flex;
align-items: center;
margin-right: -30px;
& > * {
margin-left: 16px;
}
`;

const StyledPopoverContent = styled.div`
display: flex;
& > * {
margin: 0 8px;
font-size: 16px;
}
`;

const StyledFooter = styled.footer`
padding: 12px 30px;
width: 100%;
`;

export default StyledFooter;
export { IconContainer, StyledPopoverContent };

最后编辑 footer.stories.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react";
import Footer from ".";

export default {
title: "页面组件/Footer",
component: Footer,
};

export const Default = () => (
<div style={{ marginTop: 80 }}>
<Footer />
</div>
);

七、会话窗口组件开发

最后,将以上编写的所有组件组装起来,使用 Hygen 创建一个会话窗口组件:

1
hygen component new Conversation

index.js 文件中编辑标题栏组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import React from "react";
import PropTypes from "prop-types";
import StyledConversation, { Conversations, MyChatBubble } from "./style";
import TitleBar from "components/TitleBar";
import ChatBubble from "components/ChatBubble";
import VoiceMessage from "components/VoiceMessage";
import Emoji from "components/Emoji";
import Footer from "components/Footer";

function Conversation({ children, ...rest }) {
return (
<StyledConversation {...rest}>
<TitleBar />
<Conversations>
<ChatBubble time="昨天 下午14:26">Hi 小宇,忙什么呢?</ChatBubble>
<MyChatBubble time="昨天 下午16:30">
Hello 啊!最近就是一直在加班改 bug,然后 怼产品,怼 UI,各种怼!
</MyChatBubble>
<ChatBubble time="昨天 下午18:30">
<VoiceMessage time="01:24" />
</ChatBubble>
<MyChatBubble time="昨天 下午16:30">
明天约一把王者荣耀,不连赢5把不罢休 🤘
<Emoji label="smile">🤘</Emoji>
</MyChatBubble>
</Conversations>
<Footer />
</StyledConversation>
);
}

Conversation.propTypes = {
children: PropTypes.any
};

export default Conversation;

再在 style.js 中修改样式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import ChatBubble from "components/ChatBubble";
import styled from "styled-components";

const Conversations = styled.div`
padding: 10px 15px;
display: flex;
flex-direction: column;
align-items: flex-start;
width: 100%;
overflow-y: auto;
flex: 1;

& > * {
margin: 10px 0;
}
`;

const MyChatBubble = styled(ChatBubble).attrs({ type: "mine" })`
align-self: flex-end;
`;

const StyledConversation = styled.div`
display: flex;
flex-direction: column;
height: 100vh;
border: 1px solid ${({ theme }) => theme.gray4};

& > *:last-child {
align-self: end;
}
`;

export default StyledConversation;
export { Conversations, MyChatBubble };

最后编辑 popover.stories.js 文件:

1
2
3
4
5
6
7
8
9
import React from "react";
import Conversation from ".";

export default {
title: "页面组件/Conversation",
component: Conversation,
};

export const Default = () => <Conversation />;

评论



Copyright © 2020 - 2022 Zhihao Zhuang. All rights reserved

本站访客数: 人,
总访问量: