React博客项目系列1 编写markdown文章,代码高亮,显示文章与目录
要实现的功能:
yarn add copy-to-clipboard
// 封装组件
import { CopyOutlined, LoadingOutlined } from '@ant-design/icons'
import React, { useEffect, useState } from 'react'
import './index.scss'
import copy from 'copy-to-clipboard'
import { Button } from 'antd'
export default function CodeCopyBtn ({ children }) {
// 控制按钮显示
const [copyOk, setCopyOk] = useState(false)
const handleClick = async () => {
const text = children[0].props.children[0] // 获取文本
if (window.isSecureContext) {// 在安全域下
await navigator.clipboard.writeText(text) // 使用浏览器原生剪贴板
} else {
copy(text) // 使用copy-to-clipboard
}
setCopyOk(true)
}
// 复制成功后将按钮变回"复制代码"供下次使用
useEffect(() => {
if (copyOk) {
setTimeout(() => {
setCopyOk(false)
}, 1000)
}
}, [copyOk])
return (
<div className='code-copy-btn'>
{copyOk ? (
<Button className='copy-btn'>
<LoadingOutlined />
复制成功
</Button>
) : (
<Button className='copy-btn' onClick={handleClick}>
<CopyOutlined />
复制代码
</Button>
)}
</div>
)
}
.code-copy-btn {
position: absolute;
right: 10px;
top: 10px;
display: none;
.copy-btn {
padding: 5px;
line-height: 1;
border-radius: 5px;
color: #fff;
background-color: #d9534f;
border-color: #d43f3a;
}
.ant-btn-default:not(:disabled):hover {
color: #000 !important;
border-color: #000 !important;
}
.ant-btn {
height: auto;
}
pre.blog-pre {
position: relative !important;
}
// 鼠标hover代码块控制按钮出现
pre.blog-pre:hover & {
display: block;
}
// 兼容移动端的触摸事件
pre.blog-pre.active & {
display: block;
}
}
// 组件使用
import './index.scss'
import ReactMarkdown from 'react-markdown'
import { useState, useEffect } from 'react'
import CodeCopyBtn from '@/components/CodeCopyBtn'
const ArticleDetail = () => {
const [articleMsg, setArticleMsg] = useState({})
let ref = ''
const Pre = (preProps) => {
return (
<pre
className='blog-pre'
// 兼容移动端的触摸事件
onTouchStart={({ currentTarget }) => {
if (ref) ref.className = 'blog-pre'
currentTarget.className = 'blog-pre active'
ref = currentTarget
}}
>
<CodeCopyBtn>{preProps.children}</CodeCopyBtn>
{preProps.children}
</pre>
)
}
return (
<div className='article-body'>
<ReactMarkdown
className='markdown-body'
remarkPlugins={[remarkGfm, { singleTilde: false }]}
rehypePlugins={[rehypeRaw]}
components={{
pre: Pre, // 修改pre标签
code ({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline && match ? (
<SyntaxHighlighter
children={String(children).replace(/\n$/, '')}
style={coldarkDark}
language={match[1]}
PreTag="div"
showLineNumbers={true}
// showInlineLineNumbers={true}
{...props}
/>
) : (
<code className={className} {...props} children={children} />
)
}
}}
>{articleMsg.body}</ReactMarkdown>
</div>
)
}
export default ArticleDetail
拿到文本:preProps.children[0].props.children[0]
效果:
MySQL 的 utf8 编码只支持3字节的数据,而 emoji 数据是4个字节的字符,所以如果后端用的是 MySql 的话字符集记得要用utf8mb4。
要实现的功能:
// 封装组件
import './index.scss'
import { useState } from "react"
import { SmileOutlined } from '@ant-design/icons'
import { Col, Row } from 'antd'
// inputValue是当前输入的评论内容,setInputValue是设置评论内容
const Emoji = ({ inputValue, setInputValue }) => {
const [showEmoji, setShowEmoji] = useState(false)
// emoji自行在https://www.emojiall.com/zh-hans/all-emojis中选择
const emojiList = [
{ id: 1, emoji: '' },
{ id: 2, emoji: '' },
{ id: 3, emoji: '' },
{ id: 4, emoji: '' },
{ id: 5, emoji: '' },
{ id: 6, emoji: '' },
{ id: 7, emoji: '' },
{ id: 8, emoji: '' },
{ id: 9, emoji: '' },
{ id: 10, emoji: '' },
{ id: 11, emoji: '' },
{ id: 12, emoji: '' },
{ id: 13, emoji: '' },
{ id: 14, emoji: '' },
{ id: 15, emoji: '' },
{ id: 16, emoji: '' },
{ id: 17, emoji: '' },
{ id: 18, emoji: '' },
{ id: 19, emoji: '✋' },
{ id: 20, emoji: '' },
{ id: 21, emoji: '' },
{ id: 22, emoji: '' },
{ id: 23, emoji: '' },
{ id: 24, emoji: '️' },
]
// 表情追加到评论内容后
const handleOk = (emoji) => {
setInputValue(inputValue.concat(emoji))
}
return (
<div className='emoji'>
// 点击一次出现表情框,再点一次表情框关闭
<div className={showEmoji ? "active smile" : "smile"} onClick={() => setShowEmoji(!showEmoji)}>
<SmileOutlined />
<span>表情</span>
</div>
// 表情框部分
{showEmoji && <div className="emoji-box" >
<Row gutter={[16, 8]}>
{emojiList.map((item) => (
<Col span={4} key={item.id} onClick={() => handleOk(item.emoji)}>
<span>{item.emoji}</span>
</Col>
))}
</Row>
</div >}
</div>
)
}
export default Emoji
$blue: #409eff;
.emoji {
position: relative;
.smile {
font-size: 14px;
color: #4e5969;
span {
margin-left: 4px;
}
&:hover { // 悬浮字体变蓝
cursor: pointer;
color: $blue;
}
}
.active { // 点击字体变蓝
color: $blue;
}
.emoji-box {
position: absolute;
top: 100%;
left: -75px;
z-index: 6666;
width: 336px;
padding: 24px;
color: #909090;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
margin-top: 14px;
&::before {
content: '';
position: absolute;
margin-left: -0.5rem;
top: -0.6rem;
right: 70%;
width: 1rem;
height: 1rem;
background-color: #fff;
border-right: none;
border-bottom: none;
transform: rotate(45deg);
}
span {
font-size: 20px;
cursor: pointer;
}
}
}
因为"表情"那部分就是span,所以有时候点快了会出现蓝底:
解决方法是设置user-select: none;
约束用户不能选择文本。
效果: