feat(markdown): 引入 react-markdown 及相关依赖支持 markdown 渲染
- 添加 react-markdown 库以支持 markdown 内容渲染 - 引入 @types 相关类型定义包,包括 debug、estree-jsx、hast 等 - 添加解析 markdown 所需的工具库,如 micromark、mdast-util 系列 - 集成 hast-util-to-jsx-runtime 实现 hast 到 jsx 的转换 - 添加字符实体处理和样式解析相关依赖 - 引入支持 mdx 语法的相关工具包 - 添加统一的类型检查和开发依赖支持 - 更新 package-lock.json 锁定新增依赖版本
This commit is contained in:
+8
-2
@@ -1,6 +1,7 @@
|
||||
import {HashRouter as Router, Routes, Route, NavLink} from 'react-router-dom';
|
||||
import TimestampPage from './pages/TimestampPage';
|
||||
import ElectronicWoodenFishPage from "./pages/ElectronicWoodenFishPage";
|
||||
import TodoListPage from "./pages/TodoListPage";
|
||||
import './App.css';
|
||||
|
||||
function App() {
|
||||
@@ -9,8 +10,12 @@ function App() {
|
||||
<div className="app">
|
||||
<nav className="nav">
|
||||
<ul className="nav-list">
|
||||
<li><NavLink to="/" className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>Timestamp</NavLink></li>
|
||||
<li><NavLink to="/dzmy" className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>电子木鱼</NavLink></li>
|
||||
<li><NavLink to="/" className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>Timestamp</NavLink>
|
||||
</li>
|
||||
<li><NavLink to="/dzmy"
|
||||
className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>电子木鱼</NavLink></li>
|
||||
<li><NavLink to="/todo" className={({isActive}) => isActive ? "nav-link active" : "nav-link"}>待办</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -18,6 +23,7 @@ function App() {
|
||||
<Route path="/" element={<TimestampPage/>}/>
|
||||
<Route path="/timestamp" element={<TimestampPage/>}/>
|
||||
<Route path="/dzmy" element={<ElectronicWoodenFishPage/>}/>
|
||||
<Route path="/todo" element={<TodoListPage/>}/>
|
||||
</Routes>
|
||||
</div>
|
||||
</Router>);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
const TodoList = ({todoContentList = []}) => {
|
||||
|
||||
const todoLi = todoContentList?.map((todo, index) => {
|
||||
return (
|
||||
<li key={index}>{todo}</li>
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="todo-list">
|
||||
<ul>
|
||||
{todoLi}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TodoList;
|
||||
@@ -0,0 +1,309 @@
|
||||
/* Markdown Editor Styles */
|
||||
.markdown-editor {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.toolbar-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.toolbar-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
font-size: 0.95rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.format-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.format-button {
|
||||
padding: 6px 12px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #ced4da;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.format-button:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.format-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.action-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.clear-button:hover {
|
||||
background-color: #c82333;
|
||||
}
|
||||
|
||||
.reset-button {
|
||||
background-color: #6c757d;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.reset-button:hover {
|
||||
background-color: #5a6268;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.toolbar-section {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.format-buttons {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toolbar-label {
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.editor-container {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.input-section,
|
||||
.preview-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.input-section label,
|
||||
.preview-section label {
|
||||
margin-bottom: 10px;
|
||||
font-weight: 600;
|
||||
color: #444;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.markdown-input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
padding: 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
transition: border-color 0.2s ease;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.markdown-input:focus {
|
||||
outline: none;
|
||||
border-color: #4a90e2;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
|
||||
}
|
||||
|
||||
.markdown-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.markdown-preview {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-height: 400px;
|
||||
max-height: 600px;
|
||||
padding: 15px;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
overflow-y: auto;
|
||||
background-color: #fff;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.markdown-preview h1,
|
||||
.markdown-preview h2,
|
||||
.markdown-preview h3,
|
||||
.markdown-preview h4,
|
||||
.markdown-preview h5,
|
||||
.markdown-preview h6 {
|
||||
margin-top: 1.5em;
|
||||
margin-bottom: 0.5em;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.markdown-preview h1 {
|
||||
font-size: 2em;
|
||||
border-bottom: 2px solid #eaeaea;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-preview h2 {
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eaeaea;
|
||||
padding-bottom: 0.3em;
|
||||
}
|
||||
|
||||
.markdown-preview p {
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.markdown-preview ul,
|
||||
.markdown-preview ol {
|
||||
padding-left: 2em;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.markdown-preview li {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
.markdown-preview code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(27, 31, 35, 0.05);
|
||||
border-radius: 3px;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
}
|
||||
|
||||
.markdown-preview pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: #f6f8fa;
|
||||
border-radius: 6px;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.markdown-preview pre code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.markdown-preview blockquote {
|
||||
padding: 0 1em;
|
||||
color: #6a737d;
|
||||
border-left: 0.25em solid #dfe2e5;
|
||||
margin: 0 0 1em 0;
|
||||
}
|
||||
|
||||
.markdown-preview table {
|
||||
border-collapse: collapse;
|
||||
margin: 0 0 1em 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.markdown-preview table th,
|
||||
.markdown-preview table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #dfe2e5;
|
||||
}
|
||||
|
||||
.markdown-preview table th {
|
||||
font-weight: 600;
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.markdown-preview table tr:nth-child(2n) {
|
||||
background-color: #f6f8fa;
|
||||
}
|
||||
|
||||
.markdown-preview a {
|
||||
color: #0366d6;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.markdown-preview a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.markdown-preview img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.markdown-preview hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: #e1e4e8;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.markdown-preview::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.markdown-preview::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.markdown-preview::-webkit-scrollbar-thumb {
|
||||
background: #c1c1c1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.markdown-preview::-webkit-scrollbar-thumb:hover {
|
||||
background: #a8a8a8;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import './TodoMarkdownEditor.css';
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import {useState, useRef} from "react";
|
||||
|
||||
const TodoMarkdownEditor = () => {
|
||||
const [markdown, setMarkdown] = useState('# Hello, world!\n\nThis is a simple paragraph with some **bold** text.');
|
||||
const textareaRef = useRef(null);
|
||||
const handleClear = () => {
|
||||
setMarkdown('');
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setMarkdown('# Hello, world!\n\nThis is a simple paragraph with some **bold** text.');
|
||||
};
|
||||
|
||||
return (<div className="markdown-editor">
|
||||
<div className="editor-container">
|
||||
<div className="preview-section">
|
||||
<label>预览区</label>
|
||||
<div className="markdown-preview">
|
||||
<ReactMarkdown>{markdown}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="input-section">
|
||||
<label htmlFor="markdown-input">编辑区</label>
|
||||
<textarea
|
||||
id="markdown-input"
|
||||
ref={textareaRef}
|
||||
placeholder="输入Markdown内容..."
|
||||
className="markdown-input"
|
||||
value={markdown}
|
||||
onChange={(e) => setMarkdown(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="toolbar">
|
||||
<div className="toolbar-section">
|
||||
<button className="action-button clear-button" onClick={handleClear}>
|
||||
清空
|
||||
</button>
|
||||
<button className="action-button reset-button" onClick={handleReset}>
|
||||
重置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
};
|
||||
|
||||
export default TodoMarkdownEditor;
|
||||
@@ -0,0 +1,12 @@
|
||||
import TodoMarkdownEditor from "../components/TodoMarkdownEditor";
|
||||
import TodoList from "../components/TodoList";
|
||||
|
||||
|
||||
function TodoListPage() {
|
||||
return (<div>
|
||||
<TodoMarkdownEditor/>
|
||||
<TodoList todoContentList={['1. 创建待办事项列表', '2. 添加新的待办事项', '3. 删除已完成的待办事项']}/>
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default TodoListPage;
|
||||
Reference in New Issue
Block a user