├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .eslintignore ├── src ├── App.scss ├── statics │ └── images │ │ ├── logo.png │ │ ├── cover_bg.png │ │ ├── slide_bg.png │ │ └── title_bg.png ├── setupTests.js ├── App.test.js ├── index.js ├── App.js ├── reportWebVitals.js ├── utils │ ├── tokenManager.js │ ├── index.js │ └── request.js ├── index.css ├── components │ ├── web-pptx │ │ ├── index.scss │ │ └── index.js │ └── editor-tree │ │ └── index.js ├── pages │ └── home │ │ ├── index.scss │ │ └── index.js └── mocks │ └── markdown.js ├── .env.test ├── .env.development ├── .env.production ├── .gitignore ├── .prettierrc.js ├── README.md ├── package.json ├── README.en.md ├── craco.config.js └── .eslintrc.js /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumengniu/markdown-to-pptx/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumengniu/markdown-to-pptx/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumengniu/markdown-to-pptx/HEAD/public/logo512.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | src/statics 4 | public 5 | README.md 6 | package.json 7 | yarn.lock -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | /*text-align: center;*/ 3 | font-size: 16px; 4 | color: #333; 5 | } 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/statics/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumengniu/markdown-to-pptx/HEAD/src/statics/images/logo.png -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 环境 2 | REACT_APP_ENV= 'test' 3 | # api服务 4 | REACT_APP_BASE_API='' 5 | # 文件服务 6 | REACT_APP_FILE_ACCESS_API= '' 7 | -------------------------------------------------------------------------------- /src/statics/images/cover_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumengniu/markdown-to-pptx/HEAD/src/statics/images/cover_bg.png -------------------------------------------------------------------------------- /src/statics/images/slide_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumengniu/markdown-to-pptx/HEAD/src/statics/images/slide_bg.png -------------------------------------------------------------------------------- /src/statics/images/title_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liumengniu/markdown-to-pptx/HEAD/src/statics/images/title_bg.png -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 环境 2 | REACT_APP_ENV= 'development' 3 | # api服务 4 | REACT_APP_BASE_API='http://139.159.139.184:5000/generate_markdown' 5 | # 文件服务 6 | REACT_APP_FILE_ACCESS_API= '' 7 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 环境 2 | REACT_APP_ENV= 'production' 3 | # api服务 4 | REACT_APP_BASE_API='http://139.159.139.184:5000/generate_markdown' 5 | # 文件服务 6 | REACT_APP_FILE_ACCESS_API= '' 7 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById('root')); 8 | root.render(); 9 | reportWebVitals(); 10 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.scss" 2 | import { HashRouter, Route, Routes } from "react-router-dom" 3 | import Home from "@/pages/home" 4 | 5 | function App() { 6 | 7 | return ( 8 |
9 | 10 | 11 | } /> 12 | 13 | 14 |
15 | ) 16 | } 17 | 18 | export default App 19 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/utils/tokenManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * token处理公共方法 3 | * @type {string} 4 | */ 5 | const tokenKey = "md2pptx-token" 6 | 7 | const tokenManager = { 8 | setToken: function (token) { 9 | localStorage.setItem(tokenKey, token) 10 | }, 11 | getToken: function () { 12 | return localStorage.getItem(tokenKey) 13 | }, 14 | removeToken: function () { 15 | localStorage.removeItem(tokenKey) 16 | } 17 | } 18 | 19 | export default tokenManager 20 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: "Microsoft YaHei UI", -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | 15 | ul{ 16 | margin: 0; 17 | padding: 0; 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | /.jsconfig.json 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /build.zip 15 | /release 16 | /release.zip 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | .umi 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | /.idea 31 | webstorm.config.js* 32 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Markdown-Editor-Compared 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: prettierrc 3 | * @Author: Kevin 4 | * @Date: 2022-04-25 13:41:50 5 | * @LastEditors: Kevin 6 | * @LastEditTime: 2022-04-27 09:52:17 7 | */ 8 | module.exports = { 9 | printWidth: 175, // 每行代码长度(默认175) 10 | trailingComma: "none", // 在对象或数组最后一个元素后面是否加逗号 11 | tabWidth: 2, // 每个tab相当于多少个空格(默认2) 12 | useTabs: true, // 使用tab(制表符)缩进而非空格 13 | semi: false, // 是否在行尾加分号 14 | singleQuote: false, // 使用单引号代替双引号 15 | arrowParens: "avoid", // (x) => {} 箭头函数参数只有一个时是否要有小括号。avoid:省略括号 16 | bracketSpacing: true, // 在对象,数组括号与文字之间加空格 "{ foo: bar }" 17 | proseWrap: "preserve", // 默认值。因为使用了一些折行敏感型的渲染器(如GitHub comment)而按照markdown文本样式进行折行 18 | htmlWhitespaceSensitivity: "ignore", // HTML 文件空格敏感度 19 | jsxSingleQuote: false, // jsx中是否使用单引号 20 | endOfLine: "auto", // 结尾是 \n \r \n\r auto 21 | jsxBracketSameLine: true // 将>多行JSX元素放在最后一行的末尾,而不是单独放在下一行 22 | }; 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #

markdown-to-pptx 在线可编辑的markdown生成pptx

2 | 3 | [//]: # (https://github.com/ikatyang/emoji-cheat-sheet 表情仓库) 4 | 5 | ## 🔥 [English Guide](./README.en.md) 6 | 7 | > please visit [English Guide](./README.en.md) 8 | 9 | ## 🎞️ 项目介绍 10 | 11 | > 使用 gpt-3.5-turbo 生成markdown。[项目地址](https://github.com/limaoyi1/Auto-PPT). \ 12 | > 通过在线版markdown树结构优化和编辑markdown,生成最终需要的pptx版式。 \ 13 | > 以下是项目原始示例,未经任何修改: 14 | > ![image](https://github.com/liumengniu/markdown-to-pptx/assets/24428623/fa096602-d0ad-4312-b4c2-d85eb0b9bb39) 15 | > 以下是生成的pptx,未经任何修改: 16 | > ![aigc-pptx](https://github.com/liumengniu/markdown-to-pptx/assets/24428623/3beccf39-0cd9-4ab9-9201-d42737a2bc97) 17 | 18 | 19 | ## 🧲 项目优势 20 | 21 | > 🌟 AIGC生成的markdown很难完全满足你的要求,我们提供了在线可编辑的markdown! 22 | 23 | > 🎩 在线将markdown转化为可视化的树形结构,让你在制作ppt时可以更加结构化的思考,演说ppt时可以更有脉络。 24 | 25 | > 💡 通过web相关技术实时渲染生成在线版ppt,供您在线预览调整,所见即所得,当您调试至满意程度时再导出响应的ppt! 26 | 27 | > 🖼️ 风景图插图:我们与Unsplash合作,提供最精美的插图,让你的PPT瞬间焕发生机与美感。 28 | 29 | 30 | ## 🎨 运行指南 31 | 32 | > 项目运行需要node环境 ,推荐node14+,作者使用的是node 16.13.1 33 | 34 | > 1. 安装依赖 35 | 36 | ```bash 37 | npm install 38 | ``` 39 | 40 | > 2. 运行项目 41 | 42 | ```bash 43 | npm run start 44 | ``` 45 | 46 | > 3. 编译项目 47 | 48 | ```bash 49 | npm run build 50 | ``` 51 | 52 | ## 💡 优化清单 53 | > 需要优化的内容 2023/8/23 54 | > 55 | | 蓝图 | 完成情况 | 存在问题 | 56 | |--------------------------|------------|-------------| 57 | | 多套pptx样式模板切换 | pending | ui只有一套固定版式 | 58 | | 编译成绿色下载可执行软件-分发 | pending | | 59 | | 优化体验 | pending | 看产品逻辑能否更加流畅 | 60 | 61 | ## 🌟 Star History 62 |
63 | 64 | [![Star History Chart](https://api.star-history.com/svg?repos=liumengniu/markdown-to-pptx&type=Timeline)](https://star-history.com/#liumengniu/markdown-to-pptx&Timeline) 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "markdown-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^7.1.0", 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "ahooks": "^3.7.8", 11 | "hast-util-to-html": "^9.0.0", 12 | "lodash": "^4.17.21", 13 | "mdast-util-to-hast": "^13.0.1", 14 | "pptxgenjs": "^3.12.0", 15 | "qs": "^6.11.2", 16 | "react": "^18.2.0", 17 | "react-dom": "^18.2.0", 18 | "react-router-dom": "^6.14.2", 19 | "react-scripts": "5.0.1", 20 | "react-spinners": "^0.13.8", 21 | "sass": "^1.65.1", 22 | "short-uuid": "^4.2.2", 23 | "swiper": "^8.4.2", 24 | "web-vitals": "^2.1.4" 25 | }, 26 | "scripts": { 27 | "start": "cross-env REACT_APP_ENVIRONMENT=development craco start", 28 | "build:test": "env-cmd -f .env.test craco build", 29 | "build": "cross-env REACT_APP_ENVIRONMENT=production GENERATE_SOURCEMAP=false craco build", 30 | "test": "craco test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | }, 51 | "devDependencies": { 52 | "buffer": "^6.0.3", 53 | "cross-env": "^7.0.3", 54 | "env-cmd": "^10.1.0", 55 | "eslint-config-prettier": "^9.0.0", 56 | "eslint-plugin-prettier": "^5.0.0", 57 | "prettier": "^3.0.1", 58 | "process": "^0.11.10", 59 | "terser-webpack-plugin": "^5.3.9" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/web-pptx/index.scss: -------------------------------------------------------------------------------- 1 | .web-pptx{ 2 | .swiper { 3 | width: 640px; 4 | height: 360px; 5 | position: relative; 6 | .swiper-slide { 7 | background: url("./../../statics/images/cover_bg.png"); 8 | background-size: 100% 100%; 9 | position: relative; 10 | .cover-slide{ 11 | width: 80%; 12 | margin: 0 auto; 13 | position: absolute; 14 | top: 40%; 15 | left: 10%; 16 | } 17 | 18 | .common-slide { 19 | .common{ 20 | position: relative; 21 | padding: 5%; 22 | box-sizing: border-box; 23 | h2 { 24 | margin-top: 0 25 | } 26 | &-content{ 27 | width: 60%; 28 | } 29 | &-list{ 30 | margin-top: 5px; 31 | width: 60%; 32 | padding-left: 20px; 33 | box-sizing: border-box; 34 | font-size: 4px; 35 | } 36 | &-img{ 37 | position: absolute; 38 | left: 60%; 39 | top: 0; 40 | width: 40%; 41 | height: 360px; 42 | object-fit: fill; 43 | } 44 | } 45 | } 46 | } 47 | 48 | .slide-navigator{ 49 | z-index: 2; 50 | width: 100%; 51 | position: absolute; 52 | bottom: 10px; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | >div.navigator-arrow{ 57 | text-align: center; 58 | width: 40px; 59 | height: 40px; 60 | line-height: 40px; 61 | cursor: pointer; 62 | border-radius: 5px; 63 | background-color: gray; 64 | color: #fff; 65 | opacity: .7; 66 | } 67 | &-left{ 68 | } 69 | &-pagination{ 70 | text-align: center; 71 | margin: 0 40px; 72 | width: 70px; 73 | height: 20px; 74 | border-radius: 5px; 75 | line-height: 20px; 76 | font-size: 14px; 77 | font-weight: 700; 78 | background-color: #383838; 79 | color: white; 80 | opacity: .7; 81 | } 82 | 83 | &-right{ 84 | 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/components/editor-tree/index.js: -------------------------------------------------------------------------------- 1 | import _ from "lodash"; 2 | import {forwardRef, useImperativeHandle} from "react"; 3 | 4 | /** 5 | * description: markdown可编辑目录树 6 | * @author Kevin 7 | * @date 2023/8/24 8 | */ 9 | 10 | const EditorTree = forwardRef(function(props, refP) { 11 | const {leftData, showOptions, addItem, addChildItem, removeItem, handleEditMd} = props; 12 | 13 | useImperativeHandle(refP, ()=>({ 14 | 15 | })) 16 | /** 17 | * 渲染左侧目录树 18 | * contentEditable={true}会有光标闪现最前端的BUG,所以可编辑的div做成非受控组件(PS:递归渲染的div似乎很难解决此问题) 19 | * input有换行bug,textarea有高度bug 20 | * 经过取舍最后还是采用 contentEditable={true} 21 | */ 22 | const renderTree = tree => { 23 | if (_.isEmpty(tree)) { 24 | return 25 | } 26 | let level = 0; 27 | return _.map(tree, (o, idx) => { 28 | if (!_.isNil(o.level)) level = o.level; 29 | return
31 | { 32 | !_.isNil(o.level) && o.type !== "list" &&
33 | } 34 |
35 | { 36 | ((o.type === "image" || o.type === "paragraph" || o.type === "listItem") || o?.text) && 37 |
38 |
39 |
40 |
41 | showOptions(o.id)}>+ 42 |
43 |
44 |
    45 |
  • addItem(o, idx)}>添加节点
  • 46 | { 47 | _.isNumber(o.level) &&
  • addChildItem(o, idx)}>添加子节点
  • 48 | } 49 |
  • removeItem(o, idx)}>删除节点
  • 50 |
  • 添加图片
  • 51 |
  • 子节点添加图片
  • 52 |
53 |
54 |
55 |
56 | 57 | } 58 |
59 | { 60 | o.type === "image" && 61 | } 62 | { 63 | o?.text &&
handleEditMd(e, idx, o.id)}>{o?.text}
65 | } 66 |
67 |
68 | { 69 | renderTree(o.children) 70 | } 71 |
72 | }) 73 | } 74 | 75 | return ( 76 |
77 | {renderTree(leftData)} 78 |
79 | ) 80 | }) 81 | 82 | export default EditorTree; 83 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 | #

markdown-to-pptx Online editable markdown generates pptx

2 | 3 | [//]: # (https://github.com/ikatyang/emoji-cheat-sheet 表情仓库) 4 | 5 | ## 🔥 [Chinese Guide](./README.md) 6 | 7 | > please visit [Chinese Guide](./README.md) 8 | 9 | ## 🎞️ Project Introduction 10 | 11 | > Use gpt-3.5-turbo to generate markdown。[project address](https://github.com/limaoyi1/Auto-PPT). \ 12 | > Through the online version of markdown tree structure optimization and editing markdown, generate the final required pptx format。 \ 13 | > Below is the original example of the project without any modification: 14 | > ![image](https://github.com/liumengniu/markdown-to-pptx/assets/24428623/fa096602-d0ad-4312-b4c2-d85eb0b9bb39) 15 | > Below is the generated pptx without any modification: 16 | > ![aigc-pptx](https://github.com/liumengniu/markdown-to-pptx/assets/24428623/3beccf39-0cd9-4ab9-9201-d42737a2bc97) 17 | 18 | 19 | ## 🧲 Project Benefits 20 | 21 | > 🌟 It is difficult for the markdown generated by AIGC to fully meet your requirements. We provide online editable markdown! 22 | 23 | > 🎩 Convert markdown into a visual tree structure online, so that you can think more structured when making ppt, and have more context when making ppt presentations。 24 | 25 | > 💡 Real-time rendering and generation of online version of ppt through web-related technologies for you to preview and adjust online, what you see is what you get, and then export the corresponding ppt when you debug to your satisfaction! 26 | 27 | > 🖼️ Scenery illustrations: We cooperate with Unsplash to provide the most exquisite illustrations, so that your PPT will instantly revive with vitality and beauty。 28 | 29 | 30 | ## 🎨 run guide 31 | 32 | > The project operation requires a node environment, node14+ is recommended, and the author uses node 16.13.1 33 | 34 | > 1. install dependencies 35 | 36 | ```bash 37 | npm install 38 | ``` 39 | 40 | > 2. run project 41 | 42 | ```bash 43 | npm run start 44 | ``` 45 | 46 | > 3. compile project 47 | 48 | ```bash 49 | npm run build 50 | ``` 51 | 52 | ## 💡 Optimization checklist 53 | > Content that needs to be optimized 2023/8/23 54 | > 55 | | blueprint | Completion | problem | 56 | |---------------------------------------------------|-------------|------------------------------------------| 57 | | Multiple sets of pptx style template switching | pending | ui has only one set of fixed layouts | 58 | | Compile into a green download executable software | pending | | 59 | | Optimize experience | pending | See if the product logic can be smoother | 60 | 61 | ## 🌟 Star History 62 |
63 | 64 | [![Star History Chart](https://api.star-history.com/svg?repos=liumengniu/markdown-to-pptx&type=Timeline)](https://star-history.com/#liumengniu/markdown-to-pptx&Timeline) 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author liumengniu 3 | * @Date: 2021-7-3 4 | */ 5 | 6 | const path = require("path"); 7 | const webpack = require("webpack"); 8 | const { whenProd } = require("@craco/craco") 9 | const TerserPlugin = require("terser-webpack-plugin") 10 | 11 | module.exports = { 12 | devServer: { 13 | port: 5050 14 | }, 15 | webpack: { 16 | publicPath: "./", 17 | configure: (webpackConfig, {env, paths}) => { 18 | webpackConfig.resolve.fallback = { 19 | "buffer": require.resolve("buffer") 20 | }; 21 | webpackConfig.module.rules.push( 22 | { 23 | test: /\.m?js$/, 24 | resolve: { 25 | fullySpecified: false 26 | }, 27 | }), 28 | webpackConfig.ignoreWarnings = [/Failed to parse source map/]; 29 | whenProd(() => { 30 | webpackConfig.optimization.minimize = true 31 | webpackConfig.optimization.minimizer.map(plugin => { 32 | if (plugin instanceof TerserPlugin) { 33 | Object.assign(plugin.options.minimizer.options.compress, { 34 | drop_debugger: true, // 删除 debugger 35 | drop_console: true, // 删除 console 36 | pure_funcs: ["console.log"] 37 | }) 38 | } 39 | return plugin 40 | }) 41 | webpackConfig.optimization.runtimeChunk = "single" 42 | webpackConfig.optimization.splitChunks = { 43 | ...webpackConfig.optimization.splitChunks, 44 | chunks: "all", 45 | minSize: 30000, 46 | maxAsyncRequests: 30, 47 | maxInitialRequests: 30, 48 | cacheGroups: { 49 | defaultVendors: { 50 | test: /[\\/]node_modules[\\/]/, 51 | name: "vendors" 52 | }, 53 | antd: { 54 | test: /lodash/, 55 | name: "lodash", 56 | priority: 90 57 | }, 58 | echarts: { 59 | test: /ahooks/, 60 | name: "ahooks", 61 | priority: 90 62 | }, 63 | base: { 64 | chunks: "all", 65 | test: /(react|react-dom|react-dom-router)/, 66 | name: "base", 67 | priority: 100 68 | }, 69 | commons: { 70 | chunks: "all", 71 | minChunks: 2, 72 | name: "commons", 73 | priority: 110 74 | } 75 | } 76 | } 77 | }) 78 | return webpackConfig; 79 | }, 80 | alias: { 81 | "@": path.resolve("src"), 82 | "@statics": path.resolve(__dirname, "src/statics"), 83 | "@views": path.resolve(__dirname, "src/views"), 84 | "@comp": path.resolve(__dirname, "src/components"), 85 | "@services": path.resolve(__dirname, "src/services"), 86 | "@utils": path.resolve(__dirname, "src/utils"), 87 | "@redux": path.resolve(__dirname, "src/redux"), 88 | "@styles": path.resolve(__dirname, "src/styles") 89 | }, 90 | plugins: [ 91 | new webpack.ProvidePlugin({ 92 | process: "process/browser", 93 | Buffer: ["buffer", "Buffer"], 94 | }), 95 | ] 96 | }, 97 | eslint: { 98 | enable: false, 99 | }, 100 | }; 101 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * description: 工具函数 3 | * @author Kevin 4 | * @date 2023/8/10 5 | */ 6 | import _ from "lodash" 7 | const short = require('short-uuid'); 8 | 9 | const utils = { 10 | md2json: function (){ 11 | 12 | }, 13 | json2md: function (){ 14 | 15 | }, 16 | /** 17 | * 展平数组转目录树 18 | * @param data 19 | */ 20 | flattenToTree: function (data){ 21 | let map = {}; 22 | for(let item of data){ 23 | map[item.id] = item 24 | } 25 | let result = []; //存放数组 26 | for(let item of data){ 27 | item.children = []; //给每个数组添加一个空children 28 | if(item.parentId === null){ 29 | result.push(item)//最上级的标签 30 | }else{ 31 | //相当于用这个 parentId 当做父Id去查找对比查找数据 32 | let parent = map[item.parentId] 33 | //添加到刚刚定义children数组中去 34 | parent.children.push(item) 35 | } 36 | } 37 | return result 38 | }, 39 | /** 40 | * markdown的str转 目录树形结构 41 | */ 42 | parseMarkdownToTree:function (markdown){ 43 | const lines = markdown.split('\n'); 44 | const directoryTree = []; 45 | let currentSection = { type: 'root', children: directoryTree }; 46 | const listStack = []; 47 | 48 | for (const line of lines) { 49 | const trimmedLine = line.trim(); 50 | const matchHeading = trimmedLine.match(/^(#{1,6})\s+(.*)/); 51 | const matchListItem = trimmedLine.match(/^(\s*[-+*])\s+(.*)/); 52 | const matchImage = trimmedLine.match(/^!\[([^\]]+)\]\(([^\)]+)\)/); 53 | 54 | if (matchHeading) { 55 | const level = matchHeading[1].length; 56 | const text = matchHeading[2]; 57 | const newSection = { type: 'section', level, text, children: [], id: short.generate() }; 58 | while (listStack.length > 0 && listStack[listStack.length - 1].level >= newSection.level) { 59 | listStack.pop(); 60 | } 61 | if (listStack.length === 0) { 62 | directoryTree?.push(newSection); 63 | } else { 64 | listStack[listStack.length - 1].children?.push(newSection); 65 | } 66 | listStack?.push(newSection); 67 | currentSection = newSection; 68 | } else if (matchListItem) { 69 | const text = matchListItem[2]; 70 | if (currentSection.type !== 'list') { 71 | const newList = { type: 'list', level: matchListItem[1].length, children: [], id: short.generate() }; 72 | currentSection.children?.push(newList); 73 | listStack?.push(currentSection); 74 | currentSection = newList; 75 | } 76 | const newItem = { type: 'listItem', text, children: [], id: short.generate() }; 77 | currentSection.children?.push(newItem); 78 | } else if (matchImage) { 79 | const altText = matchImage[1]; 80 | const src = matchImage[2]; 81 | currentSection?.type === "list" ? listStack[listStack.length - 1].children?.push({ 82 | type: 'image', 83 | altText, 84 | src, 85 | id: short.generate() 86 | }) : currentSection.children?.push({type: 'image', altText, src, id: short.generate()}); 87 | } else if (trimmedLine !== '') { 88 | if (currentSection.type === 'list') { 89 | const newParagraph = { type: 'paragraph', text: trimmedLine, id: short.generate() }; 90 | listStack[listStack.length - 1].children?.push(newParagraph); 91 | } else { 92 | currentSection.children?.push({ type: 'paragraph', text: trimmedLine, id: short.generate() }); 93 | } 94 | } 95 | } 96 | 97 | return directoryTree; 98 | }, 99 | } 100 | 101 | export default utils 102 | 103 | -------------------------------------------------------------------------------- /src/pages/home/index.scss: -------------------------------------------------------------------------------- 1 | .md{ 2 | display: flex; 3 | background-color: #f6f8fa; 4 | min-height: 100vh; 5 | overflow-y: hidden; 6 | 7 | &-left{ 8 | width: 60%; 9 | white-space: pre-wrap; 10 | word-wrap: break-word; 11 | border-right: 1px solid #f6f8fa; 12 | box-shadow: 2px 0px 0px 0px #eee; 13 | position: relative; 14 | max-height: 100vh; 15 | padding: 20px; 16 | box-sizing: border-box; 17 | overflow-y: auto; 18 | 19 | .btn{ 20 | position: absolute; 21 | top: 20px; 22 | right: 50px; 23 | cursor: pointer; 24 | color: #fff; 25 | background-color: #1677ff; 26 | box-shadow: 0 2px 0 rgba(5,145,255,.1); 27 | font-size: 14px; 28 | padding: 4px 15px; 29 | border-radius: 6px; 30 | z-index: 2; 31 | &:hover{ 32 | color: white; 33 | } 34 | } 35 | 36 | .tree{ 37 | &-item{ 38 | display: flex; 39 | align-items: center; 40 | margin-bottom: 5px; 41 | &:hover{ 42 | .tree-item-add { 43 | display: block; 44 | } 45 | } 46 | 47 | &-line{ 48 | position: absolute; 49 | top: 16px; 50 | left: 4px; 51 | width: 1px; 52 | height: calc(100% - 20px); 53 | background-color: #e2e2e2; 54 | } 55 | .tree-item-line:last-of-type { 56 | height: 0; 57 | } 58 | &-position{ 59 | position: relative; 60 | } 61 | &-box{ 62 | position: static; 63 | display: flex; 64 | align-items: center; 65 | border-left: 1px solid #000; 66 | .tree-item-add{ 67 | cursor: pointer; 68 | position: absolute; 69 | left: -30px; 70 | color: #D0CFCD; 71 | font-size: 24px; 72 | >span{ 73 | writing-mode: vertical-rl; 74 | display: block; 75 | } 76 | display: none; 77 | 78 | } 79 | .tree-item-options{ 80 | z-index: 1100; 81 | display: none; 82 | position: absolute; 83 | left: -20px; 84 | top: 20px; 85 | padding: 4px 0; 86 | text-align: left; 87 | list-style-type: none; 88 | background-color: rgba(255, 255, 255, 1) !important; 89 | background-clip: padding-box; 90 | border-radius: 2px; 91 | outline: none; 92 | box-shadow: 0 2px 8px #00000026; 93 | &.active{ 94 | display: block; 95 | } 96 | 97 | >ul { 98 | >li{ 99 | clear: both; 100 | margin: 0; 101 | padding: 5px 12px; 102 | color: #000000a6; 103 | font-weight: 400; 104 | font-size: 14px; 105 | line-height: 22px; 106 | white-space: nowrap; 107 | cursor: pointer; 108 | transition: all .3s; 109 | list-style: none; 110 | } 111 | } 112 | } 113 | } 114 | 115 | &-point{ 116 | width: 8px; 117 | height: 8px; 118 | border-radius: 4px; 119 | background-color: #D0CFCD; 120 | margin-right: 8px; 121 | 122 | } 123 | &-content{ 124 | flex: 1; 125 | min-height: 24px; 126 | border: none; 127 | outline: none; 128 | } 129 | &-img{ 130 | width: 320px; 131 | height: 180px; 132 | } 133 | } 134 | &-box{ 135 | position: relative; 136 | } 137 | } 138 | } 139 | &-right{ 140 | width: 40%; 141 | padding: 20px; 142 | box-sizing: border-box; 143 | overflow-x: hidden; 144 | } 145 | } 146 | 147 | .sweet-loading{ 148 | width: 100%; 149 | height: 100%; 150 | margin: 0 auto; 151 | position: absolute; 152 | top: 0; 153 | display: flex; 154 | justify-content: center; 155 | align-items: center; 156 | } 157 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | /** 2 | * description:服务请求公共方法 3 | * @author Kevin 4 | * @date 2023/8/25 5 | */ 6 | import _ from "lodash" 7 | import qs from "qs" 8 | import tokenManager from "@utils/tokenManager" 9 | 10 | const baseUrl = process.env.REACT_APP_BASE_API + "" 11 | 12 | const request = { 13 | /** 14 | * fetch兼容 SSE的方法 15 | * @param url 16 | * @param options 17 | * @returns {Promise} 18 | */ 19 | requestSSE: function (url, options = {}) { 20 | const _this = this 21 | url = baseUrl + url 22 | if (!options.method) options.method = "get" 23 | return new Promise((resolve, reject) => { 24 | fetch(url, { ...options, headers: { ...options.headers, Accept: "text/event-stream" } }) 25 | .then(async response => { 26 | if (response.status !== 200) { 27 | // 报错 28 | throw new Error(`Server responded with ${response.status}`) 29 | } 30 | if (!response.ok) { 31 | throw new Error(`Network response was not ok: ${response.status} - ${response.statusText}`) 32 | } 33 | if (!response.body) { 34 | throw new Error("No body included in POST response object") 35 | } 36 | const reader = response.body.getReader() 37 | let done = false 38 | let decodedData = "" 39 | 40 | while (!done) { 41 | const { value, done: readerDone } = await reader.read() 42 | if (!response.ok) { 43 | throw new Error(`Network response was not ok: ${response.status} - ${response.statusText}`) 44 | } 45 | done = readerDone 46 | if (value) { 47 | const chunk = new TextDecoder().decode(value) 48 | decodedData += chunk 49 | _.isFunction(options?.cb) && options?.cb(decodedData) 50 | } 51 | } 52 | resolve() 53 | }) 54 | .catch(error => { 55 | console.log(error) 56 | message.error("服务器异常,请联系管理员!") 57 | reject(error) 58 | }) 59 | }) 60 | }, 61 | /** 62 | * 基本请求方法 63 | * @param url 64 | * @param options 65 | * @returns {Promise} 66 | */ 67 | request: function (url, options = {}) { 68 | url = baseUrl + url 69 | if (!options.method) options.method = "get" 70 | return new Promise((resolve, reject) => { 71 | fetch(url, options) 72 | .then(res => res.json()) 73 | .then(data => { 74 | if (data?.code === 20000) { 75 | resolve({ ...data }) 76 | } else { 77 | const msg = { 78 | 401: "服务认证失效,请稍后再试", 79 | 429: "服务荷载达到上限,请稍后再试", 80 | 524: "服务响应超时,请稍后再试" 81 | }[data?.data?.error?.code] 82 | message.error(msg || "服务器异常,请联系管理员!") 83 | reject(data) 84 | } 85 | }) 86 | .catch(error => { 87 | console.log(error) 88 | message.error("服务器异常,请联系管理员!") 89 | reject(error) 90 | }) 91 | }) 92 | }, 93 | /** 94 | * get请求 95 | * @param url 96 | * @param options 97 | * @returns {Promise} 98 | */ 99 | get: async function (url, options = {}) { 100 | if (!options.method) { 101 | options.method = "get" 102 | } 103 | url = url + "?" + qs.stringify(options.data, { indices: true }) + "&" + new Date().getTime() 104 | if (!options.headers) { 105 | options.headers = { "Content-Type": "application/json" } 106 | } 107 | 108 | if (tokenManager.getToken()) { 109 | if (!options.headers) { 110 | options.headers = {} 111 | } 112 | options.headers["Authorization"] = tokenManager.getToken() 113 | } 114 | 115 | return this.request(url, options) 116 | }, 117 | /** 118 | * post请求 119 | * @param url 120 | * @param options 121 | * @param responseType 122 | * @returns {Promise<*>} 123 | */ 124 | post: async function (url, options = { method: "post" }, responseType) { 125 | if (!options.method) { 126 | options.method = "post" 127 | } 128 | if (options.data instanceof FormData) { 129 | options.body = options.data 130 | } else { 131 | if (_.isEmpty(options.headers)) { 132 | options.headers = {} 133 | } 134 | options.headers["Content-Type"] = "application/json" 135 | options.body = JSON.stringify(options.data) 136 | options.cb = _.get(options, "data.cb") 137 | } 138 | 139 | if (tokenManager.getToken()) { 140 | if (!options.headers) { 141 | options.headers = {} 142 | } 143 | options.headers["Authorization"] = tokenManager.getToken() 144 | } 145 | 146 | if (responseType && responseType === "sse") { 147 | return this.requestSSE(url, options) 148 | } else { 149 | return this.request(url, options) 150 | } 151 | } 152 | } 153 | 154 | export default request 155 | -------------------------------------------------------------------------------- /src/components/web-pptx/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * description: 在线渲染pptx组件 3 | * @author Kevin 4 | * @date 2023/8/17 5 | */ 6 | import "./index.scss" 7 | // Import Swiper React components 8 | import {Swiper, SwiperSlide} from 'swiper/react'; 9 | import {Navigation, Pagination} from "swiper"; 10 | // Import Swiper styles 11 | import "swiper/css/navigation"; 12 | // Import Swiper styles 13 | import 'swiper/css'; 14 | import { useRef, useState} from "react"; 15 | import utils from "@utils"; 16 | import _ from "lodash"; 17 | 18 | function WebPptx(props) { 19 | let list = []; 20 | const {rightData} = props 21 | const [activeIndex, setActiveIndex] = useState(0) 22 | const ref = useRef(); 23 | 24 | 25 | /** 26 | * 展平树结构 -> pptx所需数组 27 | * @param data 28 | */ 29 | const flattenTree = data =>{ 30 | _.map(data, o=>{ 31 | if(o.type==="section"){ 32 | list.push(o) 33 | flattenTree(o?.children) 34 | } 35 | }) 36 | return list 37 | } 38 | const [pptxData, setData] = useState(flattenTree(rightData)) 39 | 40 | /** 41 | * 获取单页幻灯片字数统计 42 | */ 43 | const getCount = tree => { 44 | let count = 0; 45 | _.map(tree, o => { 46 | count += _.size(o?.text); 47 | o?.type === "list" && _.map(o.children, p => { 48 | count += _.size(p?.text); 49 | }) 50 | }) 51 | return count 52 | } 53 | /** 54 | * 绘制全部幻灯片 55 | */ 56 | const renderAllSlide = () => { 57 | return ( 58 | <> 59 | 60 |
61 | {_.get(pptxData, `0.text`)} 62 |
63 |
64 | { 65 | _.map(pptxData, o => { 66 | return ( 67 |
68 | 69 |
70 |
71 |

{o?.level === 1 && o.type !== "list" ? "目录" : o?.text}

72 |
73 | {_.map(_.get(o, `children`), p => { 74 | let hasImg = _.findIndex(_.get(o, `children`), a=> a.type === "image") > -1; 75 | let textCount = getCount(o?.children); 76 | return ( 77 |
78 | { 79 | p?.text &&
160 ? "8px" : textCount > 120 ? "10px" : "16px", 81 | width: hasImg ? "60%" : "100%"}}> 82 | {p?.text} 83 |
84 | } 85 | { 86 | p?.type === "list" && 87 |
    88 | { 89 | _.map(p?.children, q => { 90 | return
  • {q?.text}
  • 91 | }) 92 | } 93 |
94 | } 95 | { 96 | p?.type === "image" && 97 | } 98 |
99 | ) 100 | })} 101 |
102 |
103 |
104 |
105 |
106 | ) 107 | }) 108 | } 109 | 110 | ) 111 | } 112 | /** 113 | * 上一张 114 | */ 115 | const handleNavigatePrev = () => { 116 | let swiperDom = ref.current.swiper; 117 | swiperDom && swiperDom.slidePrev(); 118 | } 119 | /** 120 | * 下一张 121 | */ 122 | const handleNavigateNext = () => { 123 | if(activeIndex >= _.size(pptxData)){ 124 | return 125 | } 126 | let swiperDom = ref.current.swiper; 127 | swiperDom && swiperDom.slideNext(); 128 | } 129 | 130 | return ( 131 |
132 | setActiveIndex(e.activeIndex)} 141 | > 142 | {renderAllSlide()} 143 |
144 |
{"←"}
145 |
146 | {`${activeIndex + 1}/${_.size(pptxData) + 1}`} 147 |
148 |
{"→"}
149 |
150 |
151 |
152 | ) 153 | } 154 | 155 | export default WebPptx; 156 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * @Description: eslint 3 | * @Author: Kevin 4 | * @Date: 2022-04-25 13:40:42 5 | * @LastEditors: Kevin 6 | * @LastEditTime: 2022-07-08 08:56:49 7 | */ 8 | module.exports = { 9 | root: true, // 当前配置为根配置,将不再从上级文件夹查找配置 10 | parser: "@babel/eslint-parser", // 采用 @babel/eslint-parser 作为语法解析器 11 | parserOptions: { 12 | requireConfigFile: false, // 禁用未检测到babel配置文件 13 | ecmaFeatures: { 14 | // 指定要使用其他那些语言对象 15 | jsx: true // 启用jsx语法 16 | }, 17 | ecmaVersion: 9, // 指定使用的ECMAScript版本(2015-6,、2016-7、2017-8、2018-9、2019-10) 18 | sourceType: "module", // 指定来源的类型,有两种script或module 19 | babelOptions: { 20 | presets: ["@babel/preset-react"] // 对react语法的转换 21 | } 22 | }, 23 | env: { 24 | browser: true, // 设置为所需检查的代码是在浏览器环境运行的 25 | es6: true, // 设置为所需检查的代码是nodejs环境运行的 26 | node: true // 设置所需检查代码为es6语法书写 27 | }, 28 | extends: ["plugin:prettier/recommended", "plugin:react/recommended", "prettier"], 29 | plugins: ["react", "prettier"], 30 | rules: { 31 | "react-hooks/exhaustive-deps": 0, 32 | "prettier/prettier": 2, 33 | "no-console": "off", // 只有开发环境可以使用console 34 | "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off", // 只有开发环境可以使用debugger 35 | "accessor-pairs": 2, // 应同时设置setter和getter 36 | "arrow-spacing": [2, { before: true, after: true }], // 箭头间距 37 | "block-spacing": [2, "always"], // 块间距 38 | "brace-style": [2, "1tbs", { allowSingleLine: true }], // 大括号样式允许单行 39 | camelcase: [0, { properties: "always" }], // 为属性强制执行驼峰命名 40 | "comma-dangle": [2, "never"], // 逗号不使用悬挂 41 | "comma-spacing": [2, { before: false, after: true }], // 逗号间距 42 | "comma-style": [2, "last"], // (默认)与数组元素,对象属性或变量声明在同一行之后和同一行需要逗号 43 | "constructor-super": 2, 44 | "consistent-this": [2, "that"], // 强制this别名为that 45 | curly: [2, "multi-line"], // 当一个块只包含一条语句时,不允许省略花括号。 46 | "dot-location": [2, "property"], // 成员表达式中的点应与属性部分位于同一行 47 | "eol-last": 2, // 强制文件以换行符结束(LF) 48 | eqeqeq: "off", // 强制使用全等 49 | "generator-star-spacing": [2, { before: true, after: true }], // 生成器中'*'两侧都要有间距 50 | "global-require": 1, // 所有调用require()都位于模块的顶层 51 | indent: ["off", 2, { SwitchCase: 2 }], // 缩进风格 52 | "key-spacing": [2, { beforeColon: false, afterColon: true }], // 强制在对象字面量属性中的键和值之间保持一致的间距 53 | "keyword-spacing": [2, { before: true, after: true }], // 关键字如if/function等的间距 54 | "new-cap": [2, { newIsCap: true, capIsNew: false }], // 允许在没有new操作符的情况下调用大写启动的函数;(默认)要求new使用大写启动函数调用所有操作符 55 | "new-parens": 2, // JavaScript通过new关键字调用函数时允许省略括号 56 | "no-array-constructor": 1, // 不允许使用Array构造函数。除非要指定生成数组的长度 57 | "no-class-assign": 2, // 不允许修改类声明的变量 58 | "no-const-assign": 2, // 不能修改使用const关键字声明的变量 59 | "no-control-regex": 0, // 不允许正则表达式中的控制字符 60 | "no-delete-var": 2, // 不允许在变量上使用delete操作符 61 | "no-dupe-args": 2, // 不允许在函数声明或表达式中使用重复的参数名称 62 | "no-dupe-class-members": 2, // 不允许在类成员中使用重复的参数名称 63 | "no-dupe-keys": 2, // 不允许在对象文字中使用重复键 64 | "no-duplicate-case": 2, // 不允许在switch语句的case子句中使用重复的测试表达式 65 | "no-empty-character-class": 2, // 不允许在正则表达式中使用空字符类 66 | "no-empty-pattern": 2, // 不允许空块语句 67 | "no-eval": 2, // 不允许使用eval 68 | "no-ex-assign": 2, // 不允许在catch子句中重新分配例外 69 | "no-extend-native": 2, // 不允许直接修改内建对象的原型 70 | "no-extra-boolean-cast": 2, // 禁止不必要的布尔转换 71 | "no-extra-parens": [2, "functions"], // 仅在函数表达式附近禁止不必要的括号 72 | "no-fallthrough": 2, // 消除一个案件无意中掉到另一个案件 73 | "no-floating-decimal": 2, // 消除浮点小数点,并在数值有小数点但在其之前或之后缺少数字时发出警告 74 | "no-func-assign": 2, // 允许重新分配function声明 75 | "no-implied-eval": 2, // 消除隐含eval() 76 | "no-inner-declarations": [2, "functions"], // 不允许function嵌套块中的声明 77 | "no-invalid-regexp": 2, // 不允许RegExp构造函数中的无效正则表达式字符串 78 | "no-irregular-whitespace": 2, // 捕获无效的空格 79 | "no-iterator": 2, // 消除阴影变量声明 80 | "no-label-var": 2, // 禁止创建与范围内的变量共享名称的标签 81 | "no-labels": [2, { allowLoop: false, allowSwitch: false }], // 消除 JavaScript 中使用带标签的语句 82 | "no-lone-blocks": 2, // 消除脚本顶层或其他块中不必要的和可能混淆的块 83 | "no-mixed-spaces-and-tabs": 0, // 不允许使用混合空格和制表符进行缩进 84 | "no-multi-spaces": 2, // 禁止在逻辑表达式,条件表达式,声明,数组元素,对象属性,序列和函数参数周围使用多个空格 85 | "no-multi-str": 2, // 防止使用多行字符串 86 | "no-multiple-empty-lines": [2, { max: 1 }], // 最多一个空行 87 | "no-native-reassign": 2, // 不允许修改只读全局变量 88 | "no-new-object": 2, // 不允许使用Object构造函数 89 | "no-new-require": 2, // 消除new require表达的使用 90 | "no-new-symbol": 2, // 防止Symbol与new 同时使用的意外错误 91 | "no-new-wrappers": 2, // 杜绝使用String,Number以及Boolean与new操作 92 | "no-obj-calls": 2, // 不允许调用Math,JSON和Reflect对象作为功能 93 | "no-octal": 2, // 不允许使用八进制文字 94 | "no-octal-escape": 2, // 不允许字符串文字中的八进制转义序列 95 | "no-path-concat": 2, // 防止 Node.js 中的目录路径字符串连接无效 96 | "no-redeclare": 2, // 消除在同一范围内具有多个声明的变量 97 | "no-regex-spaces": 2, // 在正则表达式文字中不允许有多个空格 98 | "no-return-assign": [2, "except-parens"], // 消除return陈述中的任务,除非用括号括起来 99 | "no-self-assign": 2, // 消除自我分配 100 | "no-self-compare": 2, // 禁止比较变量与自身 101 | "no-sequences": 2, // 禁止使用逗号运算符 102 | "no-sparse-arrays": 2, // 不允许稀疏数组文字 103 | "no-this-before-super": 2, // 在呼call前标记this/super关键字super() 104 | "no-throw-literal": 2, // 不允许抛出不可能是Error对象的文字和其他表达式 105 | "no-trailing-spaces": 2, // 不允许在行尾添加尾随空白 106 | "no-undef": 2, // 任何对未声明的变量的引用都会导致错误 107 | "no-undef-init": 2, // 消除初始化为undefined的变量声明 108 | "no-underscore-dangle": 2, // 标识符不能以_开头或结尾 109 | "no-unexpected-multiline": 2, // 不允许混淆多行表达式 110 | "no-unmodified-loop-condition": 2, // 查找循环条件内的引用,然后检查这些引用的变量是否在循环中被修改 111 | "no-unneeded-ternary": [2, { defaultAssignment: false }], // 不允许将条件表达式作为默认的分配模式 112 | "no-unreachable": 2, // 不允许可达代码return,throw,continue,和break语句后面还有语句。 113 | "no-unsafe-finally": 2, // 不允许return,throw,break,和continue里面的语句finally块 114 | "no-unused-vars": 0, // [1, { vars: "all", args: "after-used" }], 115 | // 消除未使用的变量,函数和函数的参数 116 | // vars: 'all' 检查所有变量的使用情况,包括全局范围内的变量。这是默认设置。 args: 'after-used' 只有最后一个参数必须使用。例如,这允许您为函数使用两个命名参数,并且只要您使用第二个参数,ESLint 就不会警告您第一个参数。这是默认设置。 117 | "no-useless-call": 2, // 标记使用情况,Function.prototype.call()并且Function.prototype.apply()可以用正常的函数调用来替代 118 | "no-useless-computed-key": 2, // 禁止不必要地使用计算属性键 119 | "no-useless-constructor": 2, // 在不改变类的工作方式的情况下安全地移除的类构造函数 120 | "no-useless-escape": 0, // 禁用不必要的转义字符 121 | "no-whitespace-before-property": 2, // 如果对象的属性位于同一行上,不允许围绕点或在开头括号之前留出空白 122 | "no-with": 2, // 禁用with 123 | "no-var": 2, // 禁用var 124 | "one-var": [2, { initialized: "never" }], // 强制将变量声明为每个函数(对于var)或块(对于let和const)范围一起声明或单独声明。 initialized: 'never' 每个作用域要求多个变量声明用于初始化变量 125 | "operator-linebreak": [2, "after", { overrides: { "?": "before", ":": "before" } }], // 实施一致的换行 126 | "padded-blocks": [2, "never"], // 在块内强制执行一致的空行填充 127 | "prefer-destructuring": ["error", { object: false, array: false }], // 此规则强制使用解构而不是通过成员表达式访问属性。 128 | quotes: [2, "double", { avoidEscape: true, allowTemplateLiterals: true }], // avoidEscape: true 允许字符串使用单引号或双引号,只要字符串包含必须以其他方式转义的引号 ;"allowTemplateLiterals": true 允许字符串使用反引号 129 | radix: 2, // parseInt必须指定第二个参数 130 | semi: [0, "never"], // 不使用分号 131 | "semi-spacing": [2, { before: false, after: true }], // 强制分号间隔 132 | "space-before-blocks": [2, "always"], // 块必须至少有一个先前的空间 133 | "space-before-function-paren": [ 134 | 2, 135 | { 136 | anonymous: "always", // 用于匿名函数表达式(例如function () {}) 137 | named: "never", // 用于命名函数表达式(例如function foo () {}) 138 | asyncArrow: "always" // 用于异步箭头函数表达式(例如async () => {}) 139 | } 140 | ], 141 | "space-in-parens": [2, "never"], // 禁止或要求(或)左边的一个或多个空格 142 | "space-infix-ops": 2, // 强制二元运算符左右各有一个空格 143 | "space-unary-ops": [2, { words: true, nonwords: false }], // words: true 如:new,delete,typeof,void,yield 左右必须有空格 // nonwords: false 一元运算符,如:-,+,--,++,!,!!左右不能有空格 144 | "spaced-comment": [2, "always", { markers: ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], // 注释开始后,此规则将强制间距的一致性//或/* 145 | "template-curly-spacing": [2, "never"], // 不允许大括号内的空格 146 | "use-isnan": 2, // 禁止比较时使用NaN,只能用isNaN() 147 | "valid-typeof": 2, // 必须使用合法的typeof的值 148 | "wrap-iife": [2, "any"], // 立即执行函数表达式的小括号风格 149 | "yield-star-spacing": [2, "both"], // 强制执行*周围 yield*表达式的间距,两侧都必须有空格 150 | yoda: [2, "never"], 151 | "prefer-const": 0, // 使用let关键字声明的变量,但在初始分配后从未重新分配变量,应改为const声明 152 | "object-curly-spacing": [2, "always", { objectsInObjects: true }], // 不允许以对象元素开始和/或以对象元素结尾的对象的大括号内的间距 153 | "array-bracket-spacing": [2, "never"], // 不允许数组括号内的空格 154 | "react/jsx-uses-react": 1, // 防止React被错误地标记为未使用 155 | "react/jsx-uses-vars": 2, // 防止在JSX中使用的变量被错误地标记为未使用 156 | "react/react-in-jsx-scope": 0, // 关闭使用JSX时防止丢失React 157 | "react/prop-types": 0 // 防止在React组件定义中丢失props验证 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/pages/home/index.js: -------------------------------------------------------------------------------- 1 | import mdStr from "@/mocks/markdown" 2 | import "./index.scss" 3 | import {useEffect, useRef, useState} from "react"; 4 | import {toHtml} from 'hast-util-to-html' 5 | import {toHast} from 'mdast-util-to-hast' 6 | import _ from "lodash" 7 | import {useClickAway} from 'ahooks'; 8 | import pptxgen from "pptxgenjs"; 9 | import utils from "../../utils"; 10 | import WebPptx from "@comp/web-pptx"; 11 | import EditorTree from "@comp/editor-tree"; 12 | 13 | //-------------静态资源------------------------- 14 | import cover_bg from "@/statics/images/cover_bg.png" 15 | import logo from "@/statics/images/logo.png" 16 | import title_bg from "@/statics/images/title_bg.png" 17 | import slide_bg from "@/statics/images/slide_bg.png" 18 | import {PacmanLoader} from "react-spinners"; 19 | 20 | const override = { 21 | display: "block", 22 | margin: "0 auto", 23 | borderColor: "red", 24 | }; 25 | 26 | const short = require('short-uuid'); 27 | let pres; 28 | 29 | /** 30 | * description: 首页 31 | * @author Kevin 32 | * @date 2023/8/10 33 | */ 34 | function Home() { 35 | const tree = utils.parseMarkdownToTree(mdStr) || [] 36 | const [leftData, setLeftData] = useState(tree) 37 | const [rightData, setRightData] = useState(tree) 38 | const [loading, setLoading] = useState(true) 39 | const [html, setHtml] = useState(null) 40 | const ref = useRef(null) 41 | 42 | /** 43 | * 初始化数据 44 | */ 45 | useEffect(()=>{ 46 | getAIGCData() 47 | },[]) 48 | 49 | /** 50 | * 通过AIGC获取markdown数据 51 | */ 52 | const getAIGCData = () => { 53 | setLoading(true) 54 | let params = { 55 | profession: "行业专家", 56 | topic: "新人如何直播", 57 | model_name: "gpt-3.5-turbo", 58 | language: "chinese" 59 | } 60 | /** 61 | * todo 简单测试,后期删除 62 | */ 63 | fetch(process.env.REACT_APP_BASE_API, { 64 | method: 'POST', headers: { 65 | 'Content-Type': 'application/json' 66 | }, body: JSON.stringify(params) 67 | }).then(res => { 68 | return res.text() 69 | }).then(res => { 70 | setLoading(false) 71 | let newData = utils.parseMarkdownToTree(res) 72 | setLeftData(newData) 73 | setRightData(newData) 74 | }).catch(e => { 75 | setLoading(false) 76 | console.log("请求服务异常") 77 | }) 78 | } 79 | /** 80 | * 初始化 pptxgen 81 | */ 82 | useEffect(() => { 83 | initPres(); 84 | }, []) 85 | 86 | /** 87 | * useClickAway 点击 88 | */ 89 | useClickAway(() => { 90 | hideOptions() 91 | }, ref); 92 | 93 | /** 94 | * 实例化pres 95 | */ 96 | const initPres = () => { 97 | pres = new pptxgen(); 98 | return pres 99 | } 100 | 101 | /** 102 | * 定义母版 103 | */ 104 | 105 | const defineSlideMaster = () =>{ 106 | pres.defineSlideMaster({ 107 | title: "MASTER_COVER", 108 | background: { color: "FFFFFF" }, 109 | objects: [ 110 | { image: { x: 0, y: 0, w: 10, h: 5.625, path: cover_bg } }, 111 | { image: { x: 9.0, y: 0.3,w: 0.65, h: 0.65, path: logo } }, 112 | { image: { x: 0.6, y: 0.6,w: 0.65, h: 0.55, path: title_bg } }, 113 | ], 114 | }); 115 | pres.defineSlideMaster({ 116 | title: "MASTER_SLIDE", 117 | background: { color: "FFFFFF" }, 118 | objects: [ 119 | { image: { x: 0, y: 0, w: 10, h: 5.625, path: slide_bg } }, 120 | { image: { x: 9.0, y: 0.3,w: 0.65, h: 0.65, path: logo } }, 121 | { image: { x: 0.6, y: 0.6,w: 0.65, h: 0.55, path: title_bg } }, 122 | ], 123 | }); 124 | } 125 | 126 | /** 127 | * 生成全部幻灯片 128 | */ 129 | const renderAllSlide = () => { 130 | defineSlideMaster() 131 | !_.isEmpty(rightData) && renderSlide(rightData) 132 | 133 | // let slide = pres.addSlide({ masterName: "MASTER_SLIDE" }); 134 | // slide.addText("How To Create PowerPoint Presentations with JavaScript", { x: 0.5, y: 0.7, fontSize: 18 }); 135 | } 136 | /** 137 | * 递归绘制幻灯片 138 | * 1、封面和目录要单独绘制 139 | * 2、递归渲染到倒数第二级,最后一级和父标题组成一张幻灯片 140 | * 3、gpt生成的每小节字数是不确定的,按照 0-80字符/80以上字符 字体大小分为2档,防止字体太多出幻灯片边界,字体太少显得空泛 141 | * 4、图片暂时放在左侧50%或者右侧50%,gpt生成内容不确定,很难做成极其通用的,后期按风格或分类做几套模板或者布局(纯体力活) 142 | */ 143 | const renderSlide = tree => { 144 | _.map(tree, o => { 145 | if (o.level && o.type==="section" && o.level === 1) { //渲染封面和目录 146 | renderCover(o) 147 | renderDirectory(o.children) 148 | } else { //渲染除封面/目录外的幻灯片(PS:只渲染至倒数第二级) 149 | (!_.isEmpty(o.children) && o.type !== "list") && renderChildSlide(o) 150 | } 151 | if(!_.isEmpty(o.children) && o.type !== "list"){ 152 | return renderSlide(o.children) 153 | } 154 | }) 155 | } 156 | /** 157 | * 绘制pptx封面 158 | */ 159 | const renderCover = item => { 160 | let slide = pres.addSlide({ masterName: "MASTER_COVER" }); 161 | slide.addText(_.get(item, 'text'), {x: 0, y: '40%', w: "100%", color: "#666", fontSize: 64, align: "center"}); 162 | } 163 | /** 164 | * 绘制目录界面 165 | */ 166 | const renderDirectory = directoryData => { 167 | let slide = pres.addSlide({ masterName: "MASTER_SLIDE" }); 168 | let texts = _.map(directoryData || [], (o, idx) => ({text: o.text + " ", options: { 169 | w: 100, 170 | breakLine: _.size(directoryData) < 8 || (_.size(directoryData) >= 8 && idx % 2 === 0), 171 | autoFit: true 172 | } 173 | })); 174 | let idx = _.findIndex(directoryData, o=> _.includes(o?.text, "目录")); 175 | idx <0 && slide && slide.addText("目录", { 176 | x: "9%", y: '10%', w: "80%", h: "80%", color: "#666", fontSize: 30, valign: "top" 177 | }); 178 | _.size(directoryData) > 8 ? slide.addText(texts, { 179 | x: "10%", 180 | y: "24%", 181 | w: "80%", 182 | h: "60%", 183 | margin: 10, 184 | autoFit: true 185 | }) : 186 | slide.addText(texts, {x: "10%", y: "24%", w: 8.5, h: 2.0, margin: 0.1}); 187 | } 188 | /** 189 | * 绘制底层幻灯 190 | * @param item 191 | */ 192 | const renderChildSlide = item => { 193 | let slide = pres.addSlide({ masterName: "MASTER_SLIDE" }); 194 | slide && slide.addText(_.get(item, 'text'), { 195 | x: "9%", y: '10%', w: "80%", h: "80%", color: "#666", fontSize: 30, valign: "top" 196 | }); 197 | let itemChild = item?.children || [] 198 | if (!_.isEmpty(itemChild)) { 199 | let textCount = 0; 200 | let idx = _.findIndex(itemChild, o => o.type === "list"); 201 | let children = [] 202 | if(_.isNumber(idx)){ 203 | children = flattenDepthOne(itemChild); 204 | } 205 | let textList = _.map(_.filter(children, o => o.text), o => { 206 | textCount += _.size(o.text); 207 | return ({text: o.text, options: {breakLine: true, bullet: o.type === "listItem"}}) 208 | }) || []; 209 | let imgUrl = _.get(_.find(children, o => o.type === 'image'), 'src'); 210 | slide && slide.addText(textList, { 211 | x: "10%", 212 | y: "24%", 213 | w: imgUrl ? 4.8 : 8.5, 214 | h: textCount > 160 ? 3.0 : textCount > 120 ? 2.5 : 2.0, 215 | margin: 0.1, 216 | fontSize: textCount > 160 ? 10 : textCount > 80 ? 14 : 20, 217 | paraSpaceBefore: 2, 218 | paraSpaceAfter: 4, 219 | }); 220 | imgUrl && slide.addImage({path: imgUrl, x: "60%", w: "40%", h: "100%", type: "cover"}) 221 | } 222 | } 223 | /*** 224 | * 展平数组 225 | */ 226 | const flattenDepthOne = list => { 227 | let newList = []; 228 | _.map(list, o => { 229 | if (o.type === "list" && !_.isEmpty(o?.children)) { 230 | newList = _.concat([], newList, o?.children) 231 | } 232 | newList.push(o); 233 | }) 234 | return newList 235 | } 236 | /** 237 | * 导出pptx至本地 238 | */ 239 | const exportPptx = () => { 240 | setLoading(true) 241 | renderAllSlide() 242 | pres.writeFile({fileName: "AIGC-PPTX.pptx"}).then(fileName => { 243 | setLoading(false) 244 | }); 245 | } 246 | /** 247 | * 根据左侧的编辑 - 渲染最新的html 248 | */ 249 | const renderHtml = mdast => { 250 | const hast = toHast(mdast) 251 | const lastHtml = toHtml(hast) 252 | setHtml(lastHtml) 253 | } 254 | /** 255 | * 编辑左侧目录树 256 | */ 257 | const handleEditMd = (e, idx, id) => { 258 | const value = e.target.textContent; 259 | if (value === "") { 260 | removeItem(e, idx) 261 | return 262 | } 263 | let oldData = _.cloneDeep(rightData) 264 | let newData = setTreeData(oldData, value, id,"edit") 265 | setRightData(newData) 266 | } 267 | /** 268 | * 编辑左侧目录树 269 | * @param treeData 270 | * @param value 271 | * @param id 272 | * @param type 273 | * @returns {unknown[]} 274 | */ 275 | const setTreeData = (treeData, value, id, type) => { 276 | return _.map(treeData, o=>{ 277 | if(o.showOptions){ 278 | o.showOptions = false 279 | } 280 | if(o.id === id){ 281 | if (type === "edit") { //编辑 282 | o.text = value 283 | } else if (type === "show") { //显示操作模块 284 | o.showOptions = !o.showOptions 285 | } 286 | } 287 | o.children = setTreeData(o?.children, value, id, type) 288 | return o; 289 | }) 290 | } 291 | /** 292 | * 操作目录树节点 293 | * @param treeData 294 | * @param item 295 | * @param idx 296 | * @param type 297 | * @returns {*} 298 | */ 299 | const operateTreeData = (treeData, item, idx, type) =>{ 300 | let isMatch = false 301 | _.map(treeData, o=>{ 302 | if(!o){ 303 | return 304 | } 305 | if(o?.id === item?.id){ 306 | isMatch = true; 307 | if(type === "add"){ 308 | treeData?.splice(idx + 1, 0, _.cloneDeep({...item, text: " ", children: [],showOptions: false, id: short.generate()})); 309 | setTreeData(treeData, item?.text, item?.id, "show") 310 | } else if(type === "addChild"){ 311 | if (_.isEmpty(o.children)) o.children = [] 312 | let neeItem = o.children[o.children?.length -1] 313 | o.children.push({...neeItem, text: " ", children: [], id: short.generate()}) 314 | setTreeData(treeData, item?.text, item?.id, "show") 315 | } else if(type === "remove"){ 316 | setTreeData(treeData, item?.text, item?.id, "show") 317 | treeData?.splice(idx, 1); 318 | } 319 | } else { 320 | o.children = !_.isEmpty(o?.children) ? operateTreeData(o?.children, item, idx, type) : [] 321 | return o; 322 | } 323 | }) 324 | return treeData 325 | } 326 | /** 327 | * 显示options 328 | * @param id 329 | */ 330 | const showOptions = id => { 331 | let oldData = _.cloneDeep(rightData) 332 | let newData = setTreeData(oldData, null, id, "show") 333 | setLeftData(newData) 334 | setRightData(newData) 335 | } 336 | /** 337 | * 隐藏全部options 338 | * @param id 339 | */ 340 | const hideOptions = () => { 341 | let oldData = _.cloneDeep(rightData) 342 | let newData = hideTreeOptions(oldData) 343 | setLeftData(newData) 344 | setRightData(newData) 345 | } 346 | /** 347 | * 递归隐藏全部options 348 | * @param treeData 349 | */ 350 | const hideTreeOptions = treeData => { 351 | return _.map(treeData, o => { 352 | if(o.showOptions){ 353 | o.showOptions = false 354 | } 355 | o.children = hideTreeOptions(o.children) 356 | return o; 357 | }) 358 | } 359 | /** 360 | * 添加节点 361 | * @param item 362 | * @param idx 363 | */ 364 | const addItem = (item, idx) => { 365 | operateTree(item, idx, "add") 366 | } 367 | /** 368 | * 添加子节点 369 | * @param item 370 | * @param idx 371 | */ 372 | const addChildItem = (item, idx) => { 373 | operateTree(item, idx, "addChild") 374 | } 375 | /** 376 | * 删除节点 377 | * @param item 378 | * @param idx 379 | */ 380 | const removeItem = (item, idx) => { 381 | operateTree(item, idx, "remove") 382 | } 383 | /** 384 | * 操作树节点(新增节点/新增子节点/删除节点) 385 | * @param item 386 | * @param idx 387 | * @param type 388 | */ 389 | const operateTree = (item, idx, type) =>{ 390 | let oldData = _.cloneDeep(rightData) 391 | let newData = operateTreeData(oldData, item, idx, type) 392 | setLeftData(newData) 393 | setRightData(newData) 394 | } 395 | 396 | 397 | return ( 398 | <> 399 |
400 |
401 |
输出pptx
402 | 404 |
405 |
406 | 407 |
408 |
409 | { 410 | loading &&
411 | 412 |
413 | } 414 | 415 | ) 416 | } 417 | 418 | export default Home 419 | -------------------------------------------------------------------------------- /src/mocks/markdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * description: md模拟数据 3 | * @author Kevin 4 | * @date 2023/8/10 5 | */ 6 | 7 | 8 | const mdStr = "# 新人如何直播\n" + 9 | "\n" + 10 | "## 目录\n" + 11 | "\n" + 12 | "## 1. 准备工作\n" + 13 | "### - 选取合适的直播平台\n" + 14 | "### - 确定直播内容\n" + 15 | "### - 准备直播设备\n" + 16 | "### - 测试网络连接稳定性\n" + 17 | "![准备工作](https://source.unsplash.com/1000x600/?preparation)\n" + 18 | "\n" + 19 | "## 2. 观众群体定位\n" + 20 | "### - 确定目标观众群体\n" + 21 | "### - 分析观众需求\n" + 22 | "### - 找到观众参与的方式\n" + 23 | "\n" + 24 | "![观众群体定位](https://source.unsplash.com/1000x600/?audience)\n" + 25 | "\n" + 26 | "## 3. 演讲技巧\n" + 27 | "### - 提前准备演讲稿\n" + 28 | "### - 讲述有趣的故事\n" + 29 | "### - 使用视觉辅助工具\n" + 30 | "### - 练习语言表达和肢体语言\n" + 31 | "### - 保持轻松自然的表达\n" + 32 | "\n" + 33 | "![演讲技巧](https://source.unsplash.com/1000x600/?presentation)\n" + 34 | "\n" + 35 | "## 4. 直播设备操作\n" + 36 | "### - 熟悉直播软件操作界面\n" + 37 | "### - 调整设备摄像头和麦克风\n" + 38 | "### - 设置画面和声音的质量\n" + 39 | "### - 设置直播的布局和效果\n" + 40 | "\n" + 41 | "![直播设备操作](https://source.unsplash.com/1000x600/?equipment)\n" + 42 | "\n" + 43 | "## 5. 直播互动技巧\n" + 44 | "### - 主动与观众互动\n" + 45 | "### - 回答观众提问\n" + 46 | "### - 利用弹幕进行互动\n" + 47 | "### - 发布调查问卷和抽奖活动\n" + 48 | "\n" + 49 | "![直播互动技巧](https://source.unsplash.com/1000x600/?interaction)\n" + 50 | "\n" + 51 | "## 6. 营销推广策略\n" + 52 | "### - 制定直播推广计划\n" + 53 | "### - 加入合适的社群和平台\n" + 54 | "### - 利用社交媒体扩大影响力\n" + 55 | "### - 推出直播专属福利和活动\n" + 56 | "\n" + 57 | "![营销推广策略](https://source.unsplash.com/1000x600/?promotion)\n" + 58 | "\n" + 59 | "## 7. 直播风险应对\n" + 60 | "### - 处理网络问题和不稳定情况\n" + 61 | "### - 应对技术故障和设备故障\n" + 62 | "### - 应对观众负面反馈和评论\n" + 63 | "\n" + 64 | "![直播风险应对](https://source.unsplash.com/1000x600/?risk)\n" + 65 | "\n" + 66 | "## 8. 数据分析和优化\n" + 67 | "### - 分析直播数据和观众反馈\n" + 68 | "### - 分析观众留存和观看时长\n" + 69 | "### - 优化直播内容和互动方式\n" + 70 | "\n" + 71 | "![数据分析和优化](https://source.unsplash.com/1000x600/?analysis)\n" + 72 | "\n" + 73 | "## 9. 直播经验分享\n" + 74 | "### - 邀请行业专家进行访谈\n" + 75 | "### - 参加和主持行业内的直播活动\n" + 76 | "\n" + 77 | "![直播经验分享](https://source.unsplash.com/1000x600/?experience)\n" + 78 | "\n" + 79 | "## 10. 社交媒体整合\n" + 80 | "### - 将直播内容转化为社交媒体内容\n" + 81 | "### - 利用社交媒体平台进行直播预告\n" + 82 | "### - 利用社交媒体进行直播回顾和互动\n" + 83 | "\n" + 84 | "![社交媒体整合](https://source.unsplash.com/1000x600/?integration)\n" + 85 | "\n" + 86 | "## 11. 直播盈利模式\n" + 87 | "### - 利用直播带货\n" + 88 | "### - 吸引品牌赞助和广告合作\n" + 89 | "### - 提供直播付费内容和会员权益\n" + 90 | "\n" + 91 | "![直播盈利模式](https://source.unsplash.com/1000x600/?monetization)\n" + 92 | "\n" + 93 | "## 12. 直播法律合规\n" + 94 | "### - 遵守相关直播法律法规\n" + 95 | "### - 保护观众个人信息和隐私\n" + 96 | "### - 版权保护和争议解决\n" + 97 | "\n" + 98 | "![直播法律合规](https://source.unsplash.com/1000x600/?legal)\n" + 99 | "\n" + 100 | "## 13. 直播平台推荐\n" + 101 | "### - 推荐适合新人的直播平台\n" + 102 | "### - 分析各直播平台的特点和优势\n" + 103 | "\n" + 104 | "![直播平台推荐](https://source.unsplash.com/1000x600/?platform)\n" + 105 | "\n" + 106 | "## 14. 直播案例分析\n" + 107 | "### - 分析成功的直播案例\n" + 108 | "### - 学习优秀主播的经验和技巧\n" + 109 | "\n" + 110 | "![直播案例分析](https://source.unsplash.com/1000x600/?case)\n" + 111 | "\n" + 112 | "## 15. 成功要素总结\n" + 113 | "### - 坚持练习和不断优化\n" + 114 | "### - 提升专业技能和主题研究\n" + 115 | "### - 保持与观众的良好互动关系\n" + 116 | "\n" + 117 | "![成功要素总结](https://source.unsplash.com/1000x600/?success)" 118 | 119 | const mdStr2 = "# 直播新手指南:魅力主播的秘诀\n" + 120 | "## 第一部分:准备工作\n" + 121 | "### 确定直播平台\n" + 122 | "选择适合自己的直播平台非常重要。目前市场上有很多直播平台可供选择,如抖音、快手、B站等。根据自己的内容特点和目标受众,选择一个适合自己的直播平台是第一步。\n" + 123 | "\n" + 124 | "![直播平台](https://source.unsplash.com/1000x600/?live+streaming)\n" + 125 | "\n" + 126 | "### 打造个人形象\n" + 127 | "在直播中,个人形象是非常重要的。要给观众留下好的印象,需要注意自己的仪表仪容,保持良好的形象形象。此外,还可以定制自己的直播头像和个人签名,增加个人特色。\n" + 128 | "\n" + 129 | "![个人形象](https://source.unsplash.com/1000x600/?personal+image)\n" + 130 | "\n" + 131 | "### 策划直播内容\n" + 132 | "直播内容的策划是直播成功的关键。要根据自己的专业知识和优势,选择适合自己的直播内容。可以根据观众的需求和兴趣,选择一些热门话题或实用技能进行直播。同时,还可以邀请一些行业内的专家来参与直播,增加内容的可信度和吸引力。\n" + 133 | "\n" + 134 | "![直播内容](https://source.unsplash.com/1000x600/?live+content)\n" + 135 | "\n" + 136 | "### 准备直播设备\n" + 137 | "准备好合适的直播设备也是直播成功的关键。需要选择一台高清摄像机和稳定的网络设备,确保直播的画面清晰流畅。此外,还可以准备一些专业的直播工具,如麦克风、灯光等,提高直播的质量。\n" + 138 | "\n" + 139 | "![直播设备](https://source.unsplash.com/1000x600/?live+equipment)\n" + 140 | "## 第二部分:直播前的准备\n" + 141 | "### 制定直播计划\n" + 142 | "制定直播计划是直播准备的第一步。在制定直播计划时,需要确定直播的日期、时间和主题,并确定直播的目标和内容。根据直播的目标和内容,制定详细的演讲大纲,包括要讲解的内容、要展示的案例和要使用的幻灯片。\n" + 143 | "\n" + 144 | "![制定直播计划](https://source.unsplash.com/1000x600/?planning)\n" + 145 | "\n" + 146 | "### 设计直播背景\n" + 147 | "设计直播背景是为了让观众在观看直播时有更好的视觉体验。可以使用公司logo、品牌色和相关图片作为直播背景,增加直播的专业感和品牌形象。此外,还可以在直播背景中添加文字、图标和其他元素,以增强直播的可视化效果。\n" + 148 | "\n" + 149 | "![设计直播背景](https://source.unsplash.com/1000x600/?design)\n" + 150 | "\n" + 151 | "### 宣传直播时间和主题\n" + 152 | "在直播前,需要通过一些渠道宣传直播的时间和主题,以吸引观众的关注和参与。可以在公司的社交媒体平台上发布直播的信息,并通过邮件、短信和其他渠道向潜在观众发送邀请。同时,还可以邀请行业内的专家或意见领袖参与直播,增加直播的影响力和吸引力。\n" + 153 | "\n" + 154 | "![宣传直播时间和主题](https://source.unsplash.com/1000x600/?promotion)\n" + 155 | "\n" + 156 | "### 进行预演和测试\n" + 157 | "在直播前,进行预演和测试是非常重要的。预演可以帮助主讲人熟悉演讲内容和幻灯片,检查直播设备和网络连接是否正常,并调整演讲的节奏和时间。测试可以确保直播平台和工具的正常运行,以及观众可以正常观看和参与直播。\n" + 158 | "\n" + 159 | "![进行预演和测试](https://source.unsplash.com/1000x600/?testing)\n" + 160 | "## 第三部分:直播技巧\n" + 161 | "### 搭建个人直播风格\n" + 162 | "\n" + 163 | "在直播时,搭建一个个人独特的直播风格非常重要。通过展示自己的个性和特点,可以吸引更多观众的关注和喜爱。以下是一些搭建个人直播风格的技巧:\n" + 164 | "\n" + 165 | "- 确定自己的定位:在选择直播的内容和形式时,要明确自己的定位和目标观众群体。例如,如果你是一个美食博主,可以选择直播烹饪过程或分享美食故事。\n" + 166 | "\n" + 167 | "- 增加个人元素:通过展示自己的兴趣爱好、生活经历等个人元素,可以让观众更容易产生共鸣和亲近感。例如,你可以在直播中展示你的宠物或分享你的旅行经历。\n" + 168 | "\n" + 169 | "- 保持专业形象:虽然要展示个人特点,但也要保持专业形象。注意言行举止,语言表达要清晰准确,避免出现不良言论或行为。\n" + 170 | "\n" + 171 | "![直播风格](https://source.unsplash.com/1000x600/?live)\n" + 172 | "\n" + 173 | "### 抓住观众注意力的方法\n" + 174 | "\n" + 175 | "在直播中,抓住观众的注意力非常重要。只有吸引住观众,才能保证直播的持续和成功。以下是一些抓住观众注意力的方法:\n" + 176 | "\n" + 177 | "- 精心准备开场:开场白要简洁、有趣,能够引起观众的兴趣。可以使用幽默的语言或讲述一个引人入胜的故事。\n" + 178 | "\n" + 179 | "- 使用视觉效果:在直播中加入一些视觉效果,如图片、视频片段等,可以吸引观众的眼球。但要注意不要过度使用,以免分散观众的注意力。\n" + 180 | "\n" + 181 | "- 提供有价值的内容:观众参与直播的主要目的是获取有价值的信息或娱乐。提供有趣、实用的内容,可以吸引观众的关注并留住他们。\n" + 182 | "\n" + 183 | "![抓住观众注意力](https://source.unsplash.com/1000x600/?attention)\n" + 184 | "\n" + 185 | "### 与观众互动的技巧\n" + 186 | "\n" + 187 | "与观众互动是直播的一个重要环节,可以增加观众的参与感和粘性。以下是一些与观众互动的技巧:\n" + 188 | "\n" + 189 | "- 回应观众留言:在直播中,可以回应观众的留言、提问或评论,与他们进行互动。这可以让观众感到被重视和关注。\n" + 190 | "\n" + 191 | "- 进行互动游戏或抽奖活动:在直播中可以进行一些互动游戏或抽奖活动,以吸引观众的参与。这可以增加观众的兴趣和互动性。\n" + 192 | "\n" + 193 | "- 邀请观众上台分享:如果观众有特殊的经验或知识,可以邀请他们上台分享,增加直播的多样性和互动性。\n" + 194 | "\n" + 195 | "![与观众互动](https://source.unsplash.com/1000x600/?interaction)\n" + 196 | "\n" + 197 | "### 解决直播中的问题和突发状况\n" + 198 | "\n" + 199 | "在直播过程中,可能会遇到一些问题和突发状况,如技术故障、观众投诉等。以下是一些解决直播中问题和突发状况的方法:\n" + 200 | "\n" + 201 | "- 准备备用方案:在直播前,准备一些备用方案,以应对可能出现的问题和状况。例如,准备备用设备以应对技术故障。\n" + 202 | "\n" + 203 | "- 快速反应和解决问题:如果在直播中遇到问题,要快速反应并尽快解决。可以通过与观众进行沟通和协商,找到解决问题的方法。\n" + 204 | "\n" + 205 | "- 学会妥善处理投诉:如果观众有投诉或不满意的意见,要冷静应对,并妥善处理。可以向观众道歉,并提供解决问题的方案。\n" + 206 | "\n" + 207 | "![解决问题](https://source.unsplash.com/1000x600/?livestream)\n" + 208 | "## 第四部分:直播后的总结与反思\n" + 209 | "### 收集观众反馈\n" + 210 | "在直播结束后,我们应该主动收集观众的反馈意见。通过观众的反馈,我们可以了解到观众对于直播内容的满意度、观众的需求以及对我们的建议和改进意见。这些反馈对于我们改进直播内容和提升观众体验非常有帮助。\n" + 211 | "\n" + 212 | "### 分析直播数据\n" + 213 | "除了收集观众反馈外,我们还需要对直播数据进行分析。通过分析直播的观看人数、观看时长、互动次数等数据,我们可以了解到直播的受众群体、观众的兴趣点以及直播的效果如何。这些数据可以帮助我们更好地了解观众的需求,并在下一次直播中做出相应的调整。\n" + 214 | "\n" + 215 | "### 反思改进的方向\n" + 216 | "在直播结束后,我们应该反思整个直播过程,总结出改进的方向。我们可以思考直播中存在的问题和不足之处,找出改进的空间,并制定相应的改进措施。反思是进步的源泉,只有不断地反思和改进,我们才能提升自己的直播能力和吸引更多的观众。\n" + 217 | "\n" + 218 | "### 持续学习和提升技能\n" + 219 | "直播是一个不断学习和提升的过程。在直播后,我们应该继续学习相关的知识和技能,提升自己的表达能力和沟通技巧。通过学习和不断地实践,我们可以不断地提升自己的直播能力,为观众提供更有价值的内容和更好的观看体验。\n" + 220 | "\n" + 221 | "![总结与反思](https://source.unsplash.com/1000x600/?reflection)\n" + 222 | "## 结束语\n" + 223 | "感谢各位的聆听和参与,通过本次演示文档,我们希望能够为大家提供一些有关PPT撰写的技巧和经验分享。在本次演示中,我们介绍了一些关于PPT撰写的基本原则和注意事项,希望能够对大家在日常工作和学习中的PPT制作有所帮助。\n" + 224 | "\n" + 225 | "在撰写PPT时,我们需要注意以下几点:\n" + 226 | "- 简洁明了:PPT应该是简洁明了的,避免过多的文字和复杂的图表。关键信息应该突出展示,以便观众快速理解。\n" + 227 | "- 结构清晰:PPT应该有清晰的结构和逻辑,每一页内容的排布应该合理,让观众能够顺利地跟随演示思路。\n" + 228 | "- 图文并茂:PPT中适当加入一些图表、图片和表格等内容,可以提高演示的吸引力和可读性。\n" + 229 | "- 配色搭配:选择适合的配色方案,使整个PPT看起来美观和协调。\n" + 230 | "- 字体规范:选择合适的字体和字号,保证文字清晰可读。\n" + 231 | "\n" + 232 | "通过以上的技巧,我们可以制作出一份优雅简洁的PPT,帮助我们更好地传递信息和表达观点。\n" + 233 | "\n" + 234 | "![结束语](https://source.unsplash.com/1000x600/?conclusion)" 235 | 236 | const mdStr3 = "# 开源项目管理\n" + 237 | "## 什么是开源项目\n" + 238 | "### 定义和概念\n" + 239 | "![开源项目](https://source.unsplash.com/1000x600/?opensource)\n" + 240 | "\n" + 241 | "开源项目是指源代码可以被免费使用、查看、修改和分发的项目。这种项目的代码通常是公开的,任何人都可以对其进行修改和改进。开源项目的目的是鼓励协作和共享,以推动软件的发展和创新。\n" + 242 | "\n" + 243 | "开源项目的特点包括透明度、可定制性和社区参与。透明度意味着项目的开发过程、代码和决策都是公开的,任何人都可以参与其中并对其进行审查。可定制性指的是用户可以根据自己的需求对项目进行修改和定制,而不必等待官方版本的更新。社区参与是指开源项目的发展和维护依靠一个活跃的社区,社区成员可以共同解决问题、分享知识和提供支持。\n" + 244 | "\n" + 245 | "### 开源软件许可证\n" + 246 | "![开源许可证](https://source.unsplash.com/1000x600/?opensource-license)\n" + 247 | "\n" + 248 | "开源软件许可证是规定了用户可以如何使用、修改和分发开源软件的法律文件。开源许可证确保了开源项目的自由和开放性,并为开发者和用户提供了法律保护。\n" + 249 | "\n" + 250 | "常见的开源软件许可证包括GNU通用公共许可证(GPL)、MIT许可证和Apache许可证。这些许可证在允许用户使用、修改和分发软件的同时,也有一些特定的限制和要求。开发者在选择合适的开源许可证时需要考虑项目的目标、需求和社区的反馈。\n" + 251 | "\n" + 252 | "开源软件许可证的存在使得开源项目可以在法律框架下自由地发展和共享,为创新和合作提供了更大的空间。\n" + 253 | "## 开源项目的价值和意义\n" + 254 | "开源项目的价值和意义是不可忽视的。下面将从三个方面来介绍:促进创新、降低成本和提高软件质量。\n" + 255 | "\n" + 256 | "### 促进创新\n" + 257 | "开源项目为开发人员提供了一个共享和协作的平台,使得他们能够共同参与到项目的开发中。这种开放的环境鼓励了创新的产生和分享。通过开源项目,开发人员可以学习和借鉴其他人的经验和技术,从而不断提高自己的能力。此外,开源项目还可以吸引更多的人参与进来,为项目带来更多的创新思路和解决方案。\n" + 258 | "\n" + 259 | "![Innovation](https://source.unsplash.com/1000x600/?innovation)\n" + 260 | "\n" + 261 | "### 降低成本\n" + 262 | "相比于闭源软件,开源项目可以避免使用费用的支出。开源软件的代码是公开的,任何人都可以免费使用、修改和分发。对于企业和个人用户来说,这意味着他们可以节省大量的软件购买和维护成本。此外,开源项目还可以减少软件开发的重复劳动,提高开发效率,进一步降低了开发成本。\n" + 263 | "\n" + 264 | "![Cost](https://source.unsplash.com/1000x600/?cost)\n" + 265 | "\n" + 266 | "### 提高软件质量\n" + 267 | "开源项目的代码是公开的,任何人都可以查看和审查。这种透明性使得开源项目的代码质量更容易得到监督和改进。开源项目通常有一个大型的开发社区,他们会对代码进行不断的审查和测试,从而保证了代码的质量和稳定性。此外,开源项目还可以通过众多的用户反馈来改进和修复bug,进一步提高了软件的质量。\n" + 268 | "\n" + 269 | "![Quality](https://source.unsplash.com/1000x600/?quality)\n" + 270 | "\n" + 271 | "通过以上三个方面的介绍,我们可以看到开源项目的价值和意义。它们不仅促进了创新,降低了成本,还提高了软件的质量。因此,越来越多的人选择参与到开源项目中,共同推动软件行业的发展。\n" + 272 | "## 开源项目的管理过程\n" + 273 | "### 项目规划与需求分析\n" + 274 | "![Project Planning](https://source.unsplash.com/1000x600/?project-planning)\n" + 275 | "\n" + 276 | "在开源项目的管理过程中,项目规划与需求分析是非常重要的步骤。项目规划确定了项目的目标、范围和进度安排,而需求分析则明确了项目的功能和性能要求。通过合理的规划和需求分析,可以确保项目的顺利进行,提高项目的成功率。\n" + 277 | "\n" + 278 | "### 团队组建与角色分工\n" + 279 | "![Teamwork](https://source.unsplash.com/1000x600/?teamwork)\n" + 280 | "\n" + 281 | "团队组建与角色分工是开源项目管理的关键环节。在团队组建过程中,需要根据项目的需求和规模确定团队成员,包括开发人员、测试人员、设计师等。同时,需要明确每个人员的角色和职责,确保团队协作高效有序。\n" + 282 | "\n" + 283 | "### 代码版本控制与协作开发\n" + 284 | "![Version Control](https://source.unsplash.com/1000x600/?version-control)\n" + 285 | "\n" + 286 | "代码版本控制与协作开发是开源项目管理中不可或缺的一部分。通过使用版本控制系统,如Git,可以对代码进行管理和跟踪,确保团队成员之间的协作开发顺利进行。同时,版本控制系统也能够方便地进行代码的回滚和分支管理,提高开发效率。\n" + 287 | "\n" + 288 | "### 社区参与与贡献管理\n" + 289 | "![Community Engagement](https://source.unsplash.com/1000x600/?community-engagement)\n" + 290 | "\n" + 291 | "在开源项目的管理过程中,社区参与与贡献管理起着重要的作用。通过积极参与社区活动,可以吸引更多的开发者和用户加入项目,提供反馈和贡献代码。同时,需要建立良好的贡献管理机制,对社区成员的贡献进行评估和奖励,激励更多的人参与到项目中来。\n" + 292 | "\n" + 293 | "### 问题跟踪与解决\n" + 294 | "![Issue Tracking](https://source.unsplash.com/1000x600/?issue-tracking)\n" + 295 | "\n" + 296 | "在开源项目管理过程中,问题跟踪与解决是必不可少的环节。通过建立问题跟踪系统,如GitHub的Issue,可以及时发现和解决项目中存在的问题。同时,还可以通过社区的力量,共同解决问题,提高项目的质量和稳定性。\n" + 297 | "## 开源项目管理的挑战与解决方案\n" + 298 | "### 社区治理与决策机制\n" + 299 | "![Community](https://source.unsplash.com/1000x600/?community)\n" + 300 | "\n" + 301 | "在开源项目中,社区治理和决策机制是一个重要的挑战。开源项目通常由志愿者组成的社区来推动发展,而社区成员之间的意见和决策往往会存在分歧。为了解决这个问题,可以采取以下措施:\n" + 302 | "\n" + 303 | "- 设立明确的社区治理规则和流程,确保决策的透明性和公正性。\n" + 304 | "- 建立开放的讨论平台,让社区成员能够自由表达意见,并进行有效的讨论和协商。\n" + 305 | "- 鼓励社区成员参与决策过程,通过选举或投票的方式来确定重要决策。\n" + 306 | "\n" + 307 | "### 知识共享与技术传承\n" + 308 | "![Knowledge](https://source.unsplash.com/1000x600/?knowledge)\n" + 309 | "\n" + 310 | "在开源项目中,知识共享和技术传承是保持项目可持续发展的关键。开源项目的成功往往依赖于社区成员之间的积极交流和知识分享。为了解决这个挑战,可以采取以下措施:\n" + 311 | "\n" + 312 | "- 建立开放的文档和知识库,记录项目的设计、实现和使用方法,方便新的社区成员学习和参与项目。\n" + 313 | "- 组织定期的技术分享和培训活动,促进社区成员之间的技术交流和经验传承。\n" + 314 | "- 鼓励社区成员编写和维护项目的文档和教程,提供给其他人使用和参考。\n" + 315 | "\n" + 316 | "### 版权管理与代码许可证合规\n" + 317 | "![Copyright](https://source.unsplash.com/1000x600/?copyright)\n" + 318 | "\n" + 319 | "开源项目的版权管理和代码许可证合规是一个重要的法律和道德挑战。在开源项目中,代码的开源许可证对项目的可用性和合法性有着重要影响。为了解决这个挑战,可以采取以下措施:\n" + 320 | "\n" + 321 | "- 确保项目的代码使用合适的开源许可证,并遵守许可证的要求。\n" + 322 | "- 定期审查项目中使用的第三方代码和库,确保其许可证的合规性。\n" + 323 | "- 提供清晰的版权声明和许可证信息,让使用者知晓项目的知识产权归属和使用限制。\n" + 324 | "\n" + 325 | "### 安全与漏洞管理\n" + 326 | "![Security](https://source.unsplash.com/1000x600/?security)\n" + 327 | "\n" + 328 | "开源项目的安全性和漏洞管理是一个重要的挑战。由于项目的开放性,任何人都可以查看和修改项目的代码,因此存在被恶意利用的风险。为了解决这个挑战,可以采取以下措施:\n" + 329 | "\n" + 330 | "- 建立安全审计和漏洞管理的流程,及时发现和修复项目中的安全漏洞。\n" + 331 | "- 鼓励社区成员报告安全漏洞,并及时响应和修复漏洞。\n" + 332 | "- 提供安全性指导和最佳实践,帮助社区成员提高对安全问题的意识和能力。\n" + 333 | "\n" + 334 | "通过以上措施,可以有效解决开源项目管理中的挑战,促进项目的可持续发展和社区的健康成长。\n" + 335 | "## 成功案例分析\n" + 336 | "### Linux内核开源项目\n" + 337 | "\n" + 338 | "![Linux](https://source.unsplash.com/1000x600/?linux)\n" + 339 | "\n" + 340 | "Linux内核开源项目是一个非常成功的开源项目,它由全球各地的开发者共同参与和贡献。Linux内核作为操作系统的核心,被广泛应用于各种设备和系统中,包括个人电脑、服务器、移动设备等。它具有高度可定制性和稳定性,成为了许多公司和组织的首选。\n" + 341 | "\n" + 342 | "Linux内核开源项目的成功得益于其开放的开发模式和强大的社区支持。任何人都可以参与到项目中,提交代码、报告问题、参与讨论等。这样的开放性使得项目能够吸引全球最优秀的开发者,不断推动Linux内核的发展和创新。\n" + 343 | "\n" + 344 | "同时,Linux内核项目也得到了许多大公司的支持,比如IBM、Intel、Red Hat等。这些公司投入了大量的人力和资源,为Linux内核的开发和维护做出了巨大贡献。他们认识到Linux内核的重要性,并且通过参与项目来共同推动开源软件的发展。\n" + 345 | "\n" + 346 | "Linux内核开源项目的成功经验对于其他开源项目也具有借鉴意义。它展示了开放、协作和共享的力量,证明了开源软件可以取得商业成功并且具有巨大的潜力。\n" + 347 | "\n" + 348 | "### Apache软件基金会\n" + 349 | "\n" + 350 | "![Apache](https://source.unsplash.com/1000x600/?apache)\n" + 351 | "\n" + 352 | "Apache软件基金会是一个致力于开源软件开发的非营利组织。它托管了许多著名的开源项目,比如Apache HTTP服务器、Hadoop、Tomcat等。这些项目在各个领域具有广泛的应用,成为了开源软件的重要代表。\n" + 353 | "\n" + 354 | "Apache软件基金会的成功得益于其严格的项目管理和开发规范。每个项目都有自己的开发团队和贡献者,他们按照Apache的开发流程和规则进行工作。这种规范性保证了项目的质量和稳定性,同时也有利于吸引更多的开发者参与进来。\n" + 355 | "\n" + 356 | "另外,Apache软件基金会注重社区建设和用户支持。它提供了丰富的文档、教程和讨论论坛,方便用户学习和交流。同时,基金会也定期举办开发者大会和技术交流活动,促进项目之间的合作和交流。\n" + 357 | "\n" + 358 | "Apache软件基金会的成功经验表明,良好的项目管理和开发规范是开源软件成功的关键。通过建立健全的社区和提供优质的支持,可以吸引更多的开发者和用户,推动项目的发展和壮大。\n" + 359 | "## 结论\n" + 360 | "### 开源项目管理的未来展望\n" + 361 | "\n" + 362 | "![开源项目管理](https://source.unsplash.com/1000x600/?open-source)\n" + 363 | "\n" + 364 | "随着技术的不断发展和开源文化的普及,开源项目管理在未来将扮演越来越重要的角色。开源项目管理不仅可以促进知识共享和合作,还能够提高软件产品的质量和可靠性。在未来,我们可以预见以下几个方面的发展:\n" + 365 | "\n" + 366 | "1. 社区参与度的增加:随着开源项目的数量和规模的不断增长,越来越多的开发者和组织将会加入到开源项目的开发和维护中。开源项目管理将更加注重社区的参与和贡献,通过吸引更多的开发者和用户参与进来,来推动项目的发展。\n" + 367 | "\n" + 368 | "2. 自动化和智能化的工具:随着人工智能和自动化技术的发展,开源项目管理将借助这些工具来提高效率和质量。例如,自动化测试和持续集成工具可以帮助开发者更快速地进行测试和部署,智能化的代码审查工具可以提供更准确和全面的代码质量评估。\n" + 369 | "\n" + 370 | "3. 开源项目的商业化:随着开源项目的影响力和价值的不断增长,越来越多的组织将会将开源项目作为商业模式的一部分。开源项目管理将需要更多的商业化能力,包括项目的定位、市场营销和商业模式设计等方面的能力。\n" + 371 | "\n" + 372 | "### 总结和反思\n" + 373 | "\n" + 374 | "![总结](https://source.unsplash.com/1000x600/?summary)\n" + 375 | "\n" + 376 | "通过本次演示,我们对开源项目管理的未来展望进行了探讨和总结。开源项目管理作为一种新兴的管理方式,将在未来发挥越来越重要的作用。我们应该关注社区参与度的增加、自动化和智能化的工具的应用以及开源项目的商业化等方面的发展趋势。同时,我们也应该不断反思和总结自己在开源项目管理中的经验和教训,以提高管理效能和项目质量。" 377 | 378 | export default mdStr2 379 | --------------------------------------------------------------------------------