├── .babelrc ├── .editorconfig ├── .github └── FUNDING.yml ├── .npmignore ├── CNAME ├── LICENSE ├── README.md ├── dist ├── css │ ├── main.css │ └── main.css.map └── vue-bl-mark-down-editor.js ├── html ├── index.html └── main.22e52bb.js ├── package.json ├── src ├── MarkDownEditor.vue ├── core │ └── hljs │ │ └── lang.hljs.js ├── dev │ ├── App.vue │ ├── index.html │ ├── main.js │ ├── toolBar │ │ ├── Audio.vue │ │ ├── Example1.vue │ │ ├── Example2.vue │ │ ├── MyCanvas.vue │ │ ├── SlotExample1.vue │ │ ├── SlotExample2.vue │ │ └── 有更多好的工具栏可以直接提交请求哦 │ └── utils │ │ ├── HZRecorder.js │ │ └── axios.js ├── index.js ├── lib │ ├── Markdown.js │ └── MarkdownFunction.js └── toolBar │ ├── about.vue │ ├── align-center.vue │ ├── align-left.vue │ ├── align-right.vue │ ├── bold.vue │ ├── code.vue │ ├── emoji.vue │ ├── header.vue │ ├── italic.vue │ ├── link.vue │ ├── list-ol.vue │ ├── list-ul.vue │ ├── picture.vue │ ├── quote-left.vue │ ├── repeat.vue │ ├── separator.vue │ ├── strikethrough.vue │ ├── subscript.vue │ ├── superscript.vue │ ├── table.vue │ ├── thumb-tack.vue │ ├── trash.vue │ ├── underline.vue │ └── undo.vue ├── webpack.build.config.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { "modules": false }], 4 | "stage-2" 5 | ], 6 | "plugins": [ 7 | "transform-vue-jsx", 8 | "transform-runtime" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | 2 | custom: [ 3 | 'http://paypal.me/blowsnow', 4 | 'https://i.loli.net/2019/09/02/vrJgXxpKmnHBLyD.png' 5 | ] 6 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | build/ 3 | node_modules/ 4 | src/ 5 | test/ 6 | config/ 7 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | markdown-editor.bload.cn -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 blowsnowit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | @[toc](目录) 2 | 3 | # vue-bl-markdown-editor 4 | 5 | > 一个基于markdown-it 高度可扩展的vue编辑器组件 6 | 7 | > 允许提供插槽自定义工具栏 8 | 9 | > 允许通过动态注册组件,允许插入工具栏指定位置 10 | 11 | > 优势: 可完全自定义工具栏功能,markdown-it插件调用等 12 | 13 | > [演示站](http://markdown-editor.bload.cn/html/) 14 | 15 | ![11111](https://images.gitee.com/uploads/images/2019/0922/105532_62dec7a6_1130434.png "屏幕截图.png") 16 | 17 | ### 安装 18 | ``` 19 | $ npm install vue-bl-markdown-editor --save 20 | ``` 21 | ### Use (如何引入) 22 | 23 | `main.js`: 24 | ```javascript 25 | // 全局注册 26 | // import with ES6 27 | import Vue from 'vue' 28 | import MarkDownEditor from 'vue-bl-markdown-editor' 29 | import 'vue-bl-markdown-editor/dist/css/main.css' 30 | // use 31 | Vue.use(MarkDownEditor); 32 | new Vue({ 33 | 'el': '#main', 34 | data() { 35 | return { value: '' } 36 | } 37 | }) 38 | ``` 39 | ### 配置要求 40 | ```html 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ``` 49 | 50 | 51 | ### 配置 52 | |名称|类型|默认值|描述| 53 | |---|---|---|---| 54 | |placeholder|String|请输入内容|提示文本| 55 | |height|Number|500|编辑器高度| 56 | |isShowToolBar|Boolean|true|是否显示工具栏| 57 | |isShowToolBarRight|Boolean|true|是否显示右侧工具栏| 58 | |showMode|String|edit|显示模式,edit,see 编辑/预览模式,isSplit true下无效| 59 | |isShowSplit|Boolean|true|是否分屏,手机只可显示一个,此状态无效| 60 | |toolBars|Array|见下面|工具栏,排序和显示| 61 | |config|Object|见下面|工具栏的配置| 62 | |isSyncScroll|Boolean|true|是否同步滚动| 63 | 64 | 65 | 66 | #### 工具栏 67 | > 开发工具栏例子查看 src/dev/toolBar 68 | 69 | |名称|描述| 70 | |---|---| 71 | |bold|粗体| 72 | |italic|斜体| 73 | |header|标题| 74 | |underline|下划线| 75 | |strikethrough|中划线| 76 | |thumb-tack|标记| 77 | |superscript|上角标| 78 | |subscript|下角标| 79 | |align-left|居左| 80 | |align-center|居中| 81 | |align-right|居右| 82 | |quote-left|段落引用| 83 | |list-ol|有序列表| 84 | |list-ul|无须列表| 85 | |link|链接| 86 | |picture|图片| 87 | |code|代码块| 88 | |table|表格| 89 | |emoji|表情| 90 | |undo|上一步| 91 | |repeat|下一步| 92 | |trash|清空| 93 | |about|关于,希望保留| 94 | |separator|分隔符| 95 | #### 工具栏配置 96 | ```javascript 97 | config: { 98 | emojis:{ 99 | // 配置多个表情 100 | more:[{name:'test',datas:['1','2']}], 101 | //是否覆盖默认的 102 | isCover: true 103 | }, 104 | // 配置图片上传 105 | picture:{ 106 | // 需要传回去上传后的路径 107 | // from paste/drag/upload 粘贴/拖拽/上传 108 | // 回调 异步请使用promise 案例看 src/dev/App.vue 109 | uploadCallback: (file,from)=>{ 110 | return new Promise(resolve => { 111 | resolve({name:'',url: ''}); 112 | }); 113 | }, 114 | // 是否解析图片列表 115 | resolving: true 116 | } 117 | } 118 | toolBars:[ 119 | 'bold','italic',...更多 120 | ] 121 | ``` 122 | 123 | #### 自定义工具栏 124 | ```javascript 125 | let editor = this.$refs.editor; 126 | let toolBar1 = editor.registerToolBarComponent('demo1',require(Example1.vue)); 127 | editor.addToolBar(toolBar1/*,0 插入位置*/); 128 | ``` 129 | 130 | ### 方法 131 | |名称|参数|描述| 132 | |---|---|---| 133 | |registerToolBarComponent|组件名,组件|动态注册组件作为工具栏,使用方法看上| 134 | |addToolBar|registerToolBarComponent返回的实例化组件,添加位置(默认最后)|添加工具栏组件| 135 | |delToolBar|删除位置|删除指定位置工具栏(不包括通过插槽加入的)| 136 | |insertContent|前缀,内容,后缀,是否强制替换内容,是否插入的时候选择|插入内容| 137 | 138 | ### 事件 139 | |名称|参数|描述| 140 | |---|---|---| 141 | |input|内容|输入内容| 142 | |ready|markdownit|加载完毕| 143 | 144 | ### 插槽 145 | |名称|描述| 146 | |---|---| 147 | |tool-bar-left-head|工具栏左侧头部插槽| 148 | |tool-bar-left-foot|工具栏左侧尾部插槽| 149 | |tool-bar-right-head|工具栏右侧头部插槽| 150 | |tool-bar-right-foot|工具栏右侧尾部插槽| 151 | 152 | ### 高级扩展 153 | - 允许自行调用markdownit 注册插件 (从ready事件中获取/直接从ref中获取) 154 | 155 | 156 | ## TODO 157 | - [X] 基础工具栏 158 | - [X] 实现撤销恢复功能 159 | - [X] 兼容手机 160 | - [X] 样式美化 161 | - [X] 本项目中打包dev演示页面 162 | - [X] highlight 样式引用(*) 163 | - [X] 图片上传回调配置 164 | - [X] 图片粘贴上传 165 | - [x] 图片拖拽上传 166 | - [x] 同步滚动(暂时按滚动条高度计算**) 167 | > 录音兼容问题(测试chrom/firefox正常,edge申请失败) 168 | 169 | > 必须运行在 https 下 测试可以正常运行 170 | - [x] 自定义录音工具栏组件(*) 171 | - [ ] 本地图片粘贴上传不支持(待解决) 172 | ## 测试 173 | - [x] 自定义工具栏 174 | - [x] 表情配置 175 | - [X] 图片上传 176 | - [X] 粘贴上传 177 | - [x] 拖入上传 178 | 179 | ## BUG 180 | - [X] 最底部插入列表无法自动换行到下一行显示(使用回车自动向下滚动) 181 | - [ ] 手机模式下工具栏最后一栏位置异常(pc模拟手机 还原不了无法测试) 182 | - [ ] 实时渲染 太卡导致连输,不正常显示 183 | #### 希望大家一起开发好用的工具栏吧 184 | 185 | ## 赞助 186 | ![UTOOLS1567434353534.png](https://images.gitee.com/uploads/images/2019/0909/094047_e4635129_1130434.png) 187 | -------------------------------------------------------------------------------- /dist/css/main.css: -------------------------------------------------------------------------------- 1 | .mark-down-editor .tool-bar{box-shadow:0 0 3px rgba(0,0,0,.157),0 0 3px rgba(0,0,0,.227);display:flex;flex-wrap:wrap;align-items:center;background-color:#fff;border:1px solid #ccc;padding:0 10px;line-height:35px}.mark-down-editor .tool-bar>div{height:35px;margin-right:10px;cursor:pointer}.mark-down-editor .tool-bar .tool-right>span{margin-right:10px}.mark-down-editor .tool-bar>div .mark-down-name{position:relative}.mark-down-editor .tool-bar>div .mark-down-name:hover>.tool-bar-box{display:block}.mark-down-editor .tool-bar>div .tool-bar-box{position:absolute;background:#fff;min-width:130px;z-index:1600;box-shadow:0 0 4px rgba(0,0,0,.157),0 0 4px rgba(0,0,0,.227);-webkit-transition:all .2s linear 0s;transition:all .2s linear 0s;display:none;left:-50px;border-radius:5px}.mark-down-editor .tool-bar>div .mark-down-form-box{padding:10px}.mark-down-editor .tool-bar>div .mark-down-form-box>div{margin:10px 0}.mark-down-editor .tool-bar>div .mark-down-dropdown>ul{list-style:none;padding:0;margin:0}.mark-down-editor .tool-bar>div .mark-down-dropdown>ul>li{text-align:center;height:35px;line-height:35px;font-size:12px;-webkit-transition:all .2s linear 0s;transition:all .2s linear 0s;position:relative}.mark-down-editor .tool-bar>div .mark-down-dropdown>ul>li:hover{background:#eaeaea}.mark-down-editor{display:flex;flex-direction:column}.mark-down-editor .container{display:flex;height:100%}.mark-down-editor .container .middle{width:10px;cursor:col-resize;font-size:16px;display:flex;align-items:center;justify-content:center;background:#fff}.mark-down-editor .container.container-column{flex-direction:column}.mark-down-editor .container .box{box-shadow:0 0 3px rgba(0,0,0,.157),0 0 3px rgba(0,0,0,.227);width:50%;height:100%;background:#fff;position:relative;overflow-y:auto;overflow-x:hidden}.mark-down-editor .container .box .box-padding{padding:10px;height:94%}.mark-down-editor .container .box.markdown-body{overflow:auto}.mark-down-editor .container .editor{width:100%;height:100%;outline:0 none;border:none!important;resize:none;position:absolute;top:0;left:0;overflow:hidden}.mark-down-editor .container .editor,.mark-down-editor .container .editor-pre{font-size:15px;line-height:1.5;font-family:Menlo,Ubuntu Mono,Consolas,Courier New,Microsoft Yahei,Hiragino Sans GB,WenQuanYi Micro Hei,sans-serif}.mark-down-editor .container .editor-pre{visibility:hidden;white-space:pre-wrap;word-wrap:break-word;margin:0}.markdown-body .hljs-left{text-align:left}.markdown-body .hljs-center{text-align:center}.markdown-body .hljs-right{text-align:right}.mark-down-editor ::-webkit-scrollbar-track-piece{background-color:#e5e5e5}.mark-down-editor ::-webkit-scrollbar{width:8px;height:9px}.mark-down-editor ::-webkit-scrollbar-thumb{background-color:#b7b7b7;background-clip:padding-box;min-height:50px;border-radius:20%}.mark-down-editor ::-webkit-scrollbar-thumb:hover{background-color:#a1a1a1}.about .mark-down-form-box{width:250px;text-align:center}.about .mark-down-form-box p{margin:0}.emoji .tool-bar-box{width:300px}.emoji .emoji-name-list>span{color:#9abbc8;padding:5px}.emoji .emoji-name-list>span.active{background:#f0f0f0}.emoji .emoji-list{display:flex;flex-wrap:wrap}.emoji .emoji-list>span{text-align:center;padding:4px 2px;margin:-1px 0 0 -1px;border:1px solid #e8e8e8}.emoji .emoji-list>span:hover{border:1px solid #0095cd;z-index:2}.table .unhighlighted{position:relative;height:220px;width:220px;background:url(data:image/gif;base64,R0lGODlhFgAWAKECAPj4+Onp6f///////yH5BAEKAAIALAAAAAAWABYAAAJAlI+pFu0P3wmg2otBm7nbzXgeKFDAiaYqaaouyr6yFnCzG99rHepp7jsBg0NfUXe8JWdLGSsChUyiVN7iis1mCwA7) repeat!important}.table .highlighted{position:absolute;max-height:220px;max-width:220px;background:url(data:image/gif;base64,R0lGODlhFgAWAKECAN3q+8PZ/////////yH5BAEKAAIALAAAAAAWABYAAAJAlI+pFu0P3wmg2otBm7nbzXgeKFDAiaYqaaouyr6yFnCzG99rHepp7jsBg0NfUXe8JWdLGSsChUyiVN7iis1mCwA7) repeat!important}.code .mark-down-form-box{width:400px}.link .mark-down-dropdown{flex-direction:column}.header{position:relative}body{margin:0}#app{background:#9bb9eaf5;background:linear-gradient(0deg,#ade8ff,#9bb9ea);height:100vh}.head-box{text-align:center;padding:10px;color:#fff}.mark-down-editor{height:70vh;width:90%;margin:0 auto;padding:20px}.mark-down-editor .foot{box-shadow:0 0 3px rgba(0,0,0,.157),0 0 3px rgba(0,0,0,.227);font-size:12px;display:flex;justify-content:space-between;align-items:center;background-color:#fff;border:1px solid #ccc;padding:0 10px;line-height:20px}.slot-example2 .mark-down-form-box{width:200px;text-align:center}.canvas .canvas-box{width:70vw;height:80vh;position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);background:#fff;z-index:3;box-shadow:0 0 5px 1px #5a5a5a}.canvas .canvas-box canvas{width:100%;height:100%}.canvas .canvas-box .actions{position:absolute;left:20px}.canvas .canvas-box .colors{position:absolute;top:40px;left:20px;display:flex;flex-direction:column}.canvas .canvas-box .colors>span{border-radius:50%;height:20px;width:20px;margin:10px 0;transition:all .3s}.canvas .canvas-box .colors>span.active{box-shadow:0 0 3px rgba(0,0,0,.95);transform:scale(1.2)}.example2 .mark-down-form-box{width:200px;text-align:center}.markdown-body .audioplayer{height:20px}.markdown-body .audioplayer:focus{outline:none} 2 | /*# sourceMappingURL=main.css.map*/ -------------------------------------------------------------------------------- /dist/css/main.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":[],"names":[],"mappings":"","file":"css/main.css","sourceRoot":""} -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-markdown-editor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-bl-markdown-editor", 3 | "description": "一个基于markdown-it高度可扩展的vue编辑器组件", 4 | "version": "2.0.0", 5 | "author": "blowsnow", 6 | "license": "MIT", 7 | "private": false, 8 | "main": "dist/vue-bl-mark-down-editor.js", 9 | "files": [ 10 | "dist/*" 11 | ], 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/blowsnowit/vue-bl-markdown-editor.git" 15 | }, 16 | "scripts": { 17 | "dev": "cross-env NODE_ENV=development webpack-dev-server --hot", 18 | "build-html": "cross-env NODE_ENV=production webpack --progress --hide-modules", 19 | "build": "cross-env NODE_ENV=production webpack --progress --hide-modules --config webpack.build.config.js" 20 | }, 21 | "dependencies": { 22 | "highlight.js": "^9.11.0", 23 | "highlight.js-async-webpack": "^1.0.4" 24 | }, 25 | "browserslist": [ 26 | "> 1%", 27 | "last 2 versions", 28 | "not ie <= 8" 29 | ], 30 | "devDependencies": { 31 | "axios": "^0.19.0", 32 | "babel-core": "^6.22.1", 33 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 34 | "babel-loader": "^7.1.1", 35 | "babel-plugin-syntax-jsx": "^6.18.0", 36 | "babel-plugin-transform-runtime": "^6.22.0", 37 | "babel-plugin-transform-vue-jsx": "^3.5.0", 38 | "babel-preset-env": "^1.3.2", 39 | "babel-preset-stage-2": "^6.22.0", 40 | "copy-webpack-plugin": "^4.0.1", 41 | "cross-env": "^5.0.5", 42 | "css-loader": "^0.28.7", 43 | "extract-text-webpack-plugin": "^2.1.0", 44 | "file-loader": "^1.1.4", 45 | "github-markdown-css": "^2.6.0", 46 | "html-webpack-plugin": "^2.28.0", 47 | "markdown-it": "^8.3.1", 48 | "markdown-it-abbr": "^1.0.4", 49 | "markdown-it-container": "^2.0.0", 50 | "markdown-it-deflist": "^2.0.0", 51 | "markdown-it-emoji": "^1.1.1", 52 | "markdown-it-footnote": "^3.0.1", 53 | "markdown-it-for-inline": "~0.1.0", 54 | "markdown-it-html5-embed": "^1.0.0", 55 | "markdown-it-images-preview": "^1.0.0", 56 | "markdown-it-ins": "^2.0.0", 57 | "markdown-it-katex-external": "^1.0.0", 58 | "markdown-it-mark": "^2.0.0", 59 | "markdown-it-sub": "^1.0.0", 60 | "markdown-it-sup": "^1.0.0", 61 | "markdown-it-task-lists": "^2.1.1", 62 | "markdown-it-toc": "^1.1.0", 63 | "merges-utils": "^1.0.2", 64 | "optimize-css-assets-webpack-plugin": "^1.3.1", 65 | "postcss-import": "^11.0.0", 66 | "postcss-loader": "^2.0.8", 67 | "postcss-url": "^7.2.1", 68 | "style-loader": "^1.0.0", 69 | "url-loader": "^0.5.8", 70 | "vue": "^2.5.11", 71 | "vue-loader": "^13.0.5", 72 | "vue-style-loader": "^3.0.1", 73 | "vue-template-compiler": "^2.4.4", 74 | "webpack": "^3.6.0", 75 | "webpack-bundle-analyzer": "^2.8.1", 76 | "webpack-dev-server": "^2.9.1", 77 | "webpack-md5-hash": "^0.0.5" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/MarkDownEditor.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | 489 | 490 | 666 | -------------------------------------------------------------------------------- /src/core/hljs/lang.hljs.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 'sh': 'bash', 3 | 'c': 'cpp', 4 | 'c++': 'cpp', 5 | 'cs': 'cs', 6 | 'css': 'css', 7 | 'delphi': 'delphi', 8 | 'diff': 'diff', 9 | 'dos': 'dos', 10 | 'excel': 'excel', 11 | 'go': 'go', 12 | 'java': 'java', 13 | 'javascript': 'javascript', 14 | 'json': 'json', 15 | 'kotlin': 'kotlin', 16 | 'lua': 'lua', 17 | 'markdown': 'markdown', 18 | 'php': 'php', 19 | 'powershell': 'powershell', 20 | 'python': 'python', 21 | 'ruby': 'ruby', 22 | 'shell': 'shell', 23 | 'sql': 'sql', 24 | 'vbscript': 'vbscript', 25 | 'xml': 'xml', 26 | 'html': 'xml', 27 | 'yaml': 'yaml', 28 | }; 29 | -------------------------------------------------------------------------------- /src/dev/App.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 292 | 293 | 325 | -------------------------------------------------------------------------------- /src/dev/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | vue-markdown-editor 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | -------------------------------------------------------------------------------- /src/dev/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import MarkDownEditor from '../index.js' 4 | 5 | 6 | Vue.use(MarkDownEditor); 7 | new Vue({ 8 | el: '#app', 9 | render: h => h(App) 10 | }) 11 | -------------------------------------------------------------------------------- /src/dev/toolBar/Audio.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 58 | 59 | 70 | -------------------------------------------------------------------------------- /src/dev/toolBar/Example1.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/dev/toolBar/Example2.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 33 | 34 | 43 | -------------------------------------------------------------------------------- /src/dev/toolBar/MyCanvas.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 92 | 93 | 135 | -------------------------------------------------------------------------------- /src/dev/toolBar/SlotExample1.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /src/dev/toolBar/SlotExample2.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 35 | 36 | 45 | -------------------------------------------------------------------------------- /src/dev/toolBar/有更多好的工具栏可以直接提交请求哦: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blowsnowit/vue-bl-markdown-editor/1e32771f00d2c0e084aeb7633027a810936b6cab/src/dev/toolBar/有更多好的工具栏可以直接提交请求哦 -------------------------------------------------------------------------------- /src/dev/utils/HZRecorder.js: -------------------------------------------------------------------------------- 1 | //兼容 2 | window.URL = window.URL || window.webkitURL; 3 | // navigator.mediaDevices.getUserMedia = navigator.mediaDevices.getUserMedia || navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia; 4 | 5 | let HZRecorder = function (stream, config) { 6 | config = config || {}; 7 | config.sampleBits = config.sampleBits || 8; //采样数位 8, 16 8 | config.sampleRate = config.sampleRate || (44100 / 6); //采样率(1/6 44100) 9 | 10 | let context = new (window.webkitAudioContext || window.AudioContext)(); 11 | let audioInput = context.createMediaStreamSource(stream); 12 | let createScript = context.createScriptProcessor || context.createJavaScriptNode; 13 | let recorder = createScript.apply(context, [4096, 1, 1]); 14 | let mediaRecorder = new MediaRecorder(stream); 15 | let blob = null; 16 | let onStop = null; 17 | 18 | let audioData = { 19 | inputSampleRate: context.sampleRate //输入采样率 20 | , inputSampleBits: 16 //输入采样数位 8, 16 21 | , outputSampleRate: config.sampleRate //输出采样率 22 | , oututSampleBits: config.sampleBits //输出采样数位 8, 16 23 | , compress: function (data) { //合并压缩 data 为 buffer 24 | //压缩 25 | let compression = parseInt(this.inputSampleRate / this.outputSampleRate); 26 | let length = data.length / compression; 27 | let result = new Float32Array(length); 28 | let index = 0, j = 0; 29 | while (index < length) { 30 | result[index] = data[j]; 31 | j += compression; 32 | index++; 33 | } 34 | return result; 35 | } 36 | , encodeWAV: function (inBuffer) { 37 | let sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate); 38 | let sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits); 39 | let bytes = this.compress(inBuffer); 40 | let dataLength = bytes.length * (sampleBits / 8); 41 | let buffer = new ArrayBuffer(44 + dataLength); 42 | let data = new DataView(buffer); 43 | let channelCount = 1;//单声道 44 | let offset = 0; 45 | 46 | let writeString = function (str) { 47 | for (let i = 0; i < str.length; i++) { 48 | data.setUint8(offset + i, str.charCodeAt(i)); 49 | } 50 | } 51 | 52 | // 资源交换文件标识符 53 | writeString('RIFF'); offset += 4; 54 | // 下个地址开始到文件尾总字节数,即文件大小-8 55 | data.setUint32(offset, 36 + dataLength, true); offset += 4; 56 | // WAV文件标志 57 | writeString('WAVE'); offset += 4; 58 | // 波形格式标志 59 | writeString('fmt '); offset += 4; 60 | // 过滤字节,一般为 0x10 = 16 61 | data.setUint32(offset, 16, true); offset += 4; 62 | // 格式类别 (PCM形式采样数据) 63 | data.setUint16(offset, 1, true); offset += 2; 64 | // 通道数 65 | data.setUint16(offset, channelCount, true); offset += 2; 66 | // 采样率,每秒样本数,表示每个通道的播放速度 67 | data.setUint32(offset, sampleRate, true); offset += 4; 68 | // 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8 69 | data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4; 70 | // 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8 71 | data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2; 72 | // 每样本数据位数 73 | data.setUint16(offset, sampleBits, true); offset += 2; 74 | // 数据标识符 75 | writeString('data'); offset += 4; 76 | // 采样数据总数,即数据总大小-44 77 | data.setUint32(offset, dataLength, true); offset += 4; 78 | // 写入采样数据 79 | if (sampleBits === 8) { 80 | for (let i = 0; i < bytes.length; i++, offset++) { 81 | let s = Math.max(-1, Math.min(1, bytes[i])); 82 | let val = s < 0 ? s * 0x8000 : s * 0x7FFF; 83 | val = parseInt(255 / (65535 / (val + 32768))); 84 | data.setInt8(offset, val, true); 85 | } 86 | } else { 87 | for (let i = 0; i < bytes.length; i++, offset += 2) { 88 | let s = Math.max(-1, Math.min(1, bytes[i])); 89 | data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true); 90 | } 91 | } 92 | 93 | return new Blob([data], { type: 'audio/wav' }); 94 | } 95 | }; 96 | 97 | 98 | //开始录音 99 | this.start = function () { 100 | if (mediaRecorder.state === "recording") { 101 | mediaRecorder.stop(); 102 | } 103 | mediaRecorder.start(); 104 | console.log(mediaRecorder.state); 105 | } 106 | 107 | //停止 108 | this.stop = function () { 109 | mediaRecorder.stop(); 110 | } 111 | 112 | //获取音频文件 113 | this.getBlob = function () { 114 | console.log(this.blob); 115 | return new Promise(resolve => { 116 | resolve(this.blob) 117 | // return; 118 | // //转换blob为 buffer 119 | // let reader = new FileReader(); 120 | // //byte为blob对象 121 | // reader.readAsArrayBuffer(this.blob); 122 | // reader.onload= ()=>{ 123 | // console.log(reader.result); 124 | // let buffer = new Uint8Array(reader.result); 125 | // console.log('转换数据',buffer); 126 | // resolve(audioData.encodeWAV(buffer)); 127 | // } 128 | }) 129 | } 130 | 131 | //回放 132 | this.play = function (audio) { 133 | audio.src = window.URL.createObjectURL(this.getBlob()); 134 | } 135 | 136 | mediaRecorder.onstop = e=>{ 137 | console.log('onstop ',e); 138 | this.getBlob().then((blob)=>{ 139 | console.log('转换完毕数据',blob); 140 | this.onStop(blob); 141 | }) 142 | } 143 | 144 | mediaRecorder.ondataavailable = e=>{ 145 | this.blob = e.data; 146 | console.log('数据采集',this.blob); 147 | } 148 | //音频采集 149 | recorder.onaudioprocess = function (e) { 150 | audioData.input(e.inputBuffer.getChannelData(0)); 151 | //record(e.inputBuffer.getChannelData(0)); 152 | } 153 | 154 | }; 155 | //抛出异常 156 | HZRecorder.throwError = function (message) { 157 | alert(message); 158 | throw new function () { this.toString = function () { return message; } } 159 | } 160 | //是否支持录音 161 | HZRecorder.canRecording = (navigator.mediaDevices && navigator.mediaDevices.getUserMedia != null); 162 | //获取录音机 163 | HZRecorder.get = function (callback, config) { 164 | if (callback) { 165 | if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) { 166 | navigator.mediaDevices.getUserMedia({ audio: true }).then(stream=>{ 167 | let rec = new HZRecorder(stream, config); 168 | callback(rec); 169 | }).catch(error=>{ 170 | console.log('申请',error); 171 | switch (error.code || error.name) { 172 | case 'PERMISSION_DENIED': 173 | case 'PermissionDeniedError': 174 | case 'NotAllowedError': 175 | HZRecorder.throwError('用户拒绝提供录音权限。'); 176 | break; 177 | case 'NOT_SUPPORTED_ERROR': 178 | case 'NotSupportedError': 179 | HZRecorder.throwError('浏览器不支持硬件设备。'); 180 | break; 181 | case 'MANDATORY_UNSATISFIED_ERROR': 182 | case 'MandatoryUnsatisfiedError': 183 | HZRecorder.throwError('无法发现指定的硬件设备。'); 184 | break; 185 | default: 186 | HZRecorder.throwError('无法打开麦克风。异常信息:' + (error.code || error.name)); 187 | break; 188 | } 189 | }) 190 | } else { 191 | HZRecorder.throwError('当前浏览器不支持录音功能。'); return; 192 | } 193 | } 194 | } 195 | 196 | 197 | 198 | 199 | 200 | export default HZRecorder; 201 | -------------------------------------------------------------------------------- /src/dev/utils/axios.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | // 是否是生产环境,日志只对非生成环境生效 4 | let noProduction = process.env.NODE_ENV !== 'production'; 5 | 6 | axios.defaults.baseURL = ''; // 多环境地址 7 | axios.defaults.timeout = 45000; // 响应时间 8 | axios.defaults.withCredentials = true; // 允许跨域请求Cookie 9 | axios.defaults.headers['Content-Type'] = 'application/json'; 10 | axios.defaults.headers['Accept'] = 'application/json'; 11 | 12 | /** 13 | * 默认向外暴露一个请求对象,不建议全部import,尽量按需引入 14 | */ 15 | export default { 16 | 17 | /** 18 | * post请求 19 | * @param options 请求数据对象 20 | * @returns {Promise} 21 | */ 22 | requestPost(options) { 23 | return new Promise((resolve, reject) => { 24 | axios.post(options.api, options.param).then(response => { 25 | resolve(response.data); 26 | }, error => { 27 | reject(error); 28 | }).catch(throws => { 29 | if (noProduction) { 30 | console.log("requestPost.catch返回:", throws); 31 | } 32 | reject("网络异常!") 33 | }) 34 | }) 35 | }, 36 | 37 | /** 38 | * get请求 39 | * @param options 请求数据对象 40 | * @returns {Promise} 41 | */ 42 | requestGet(options) { 43 | return new Promise((resolve, reject) => { 44 | axios.get(options.api, { 45 | params: options.param 46 | }).then(response => { 47 | //请求成功 48 | resolve(response.data); 49 | }, error => { 50 | reject(error); 51 | }).catch(throws => { 52 | if (noProduction) { 53 | console.log("requestGet.catch返回:", throws); 54 | } 55 | reject("网络异常!") 56 | }) 57 | }) 58 | }, 59 | 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const MarkDownEditor = require('./MarkDownEditor'); 2 | const bold = require('./toolBar/bold.vue'); //粗体 3 | const italic = require('./toolBar/italic.vue'); //斜体 4 | const header = require('./toolBar/header.vue'); //标题 5 | const underline = require('./toolBar/underline.vue'); //下划线 6 | const strikethrough = require('./toolBar/strikethrough.vue'); //中划线 7 | const thumbTack = require('./toolBar/thumb-tack.vue'); //标记 8 | const superscript = require('./toolBar/superscript.vue'); //上角标 9 | const subscript = require('./toolBar/subscript.vue'); //下角标 10 | const alignLeft = require('./toolBar/align-left.vue'); //居左 11 | const alignCenter = require('./toolBar/align-center.vue'); //居中 12 | const alignRight = require('./toolBar/align-right.vue'); //居右 13 | const quoteLeft = require('./toolBar/quote-left.vue'); //段落引用 14 | const listOl = require('./toolBar/list-ol.vue'); //有序列表 15 | const listUl = require('./toolBar/list-ul.vue'); //无须列表 16 | const link = require('./toolBar/link.vue'); //链接 17 | const picture = require('./toolBar/picture.vue'); //图片 18 | const code = require('./toolBar/code.vue'); //代码块 19 | const table = require('./toolBar/table.vue'); //表格 20 | const emoji = require('./toolBar/emoji.vue'); 21 | const undo = require('./toolBar/undo.vue'); //上一步 22 | const repeat = require('./toolBar/repeat.vue'); //下一步 23 | const trash = require('./toolBar/trash.vue'); //清空 24 | const about = require('./toolBar/about.vue'); //关于 25 | 26 | 27 | const install = function(Vue) { 28 | Vue.component('mark-down-editor', MarkDownEditor.default); 29 | }; 30 | 31 | if (typeof window !== 'undefined' && window.Vue) { 32 | install(window.Vue); 33 | } 34 | 35 | const VueMarkDownEditor = { 36 | install: function(Vue) { 37 | Vue.component('mark-down-editor', MarkDownEditor.default); 38 | }, 39 | MarkDownEditor, 40 | bold, 41 | italic, 42 | header, 43 | underline, 44 | strikethrough, 45 | thumbTack, 46 | superscript, 47 | subscript, 48 | alignLeft, 49 | alignCenter, 50 | alignRight, 51 | quoteLeft, 52 | listOl, 53 | listUl, 54 | link, 55 | picture, 56 | code, 57 | table, 58 | emoji, 59 | undo, 60 | repeat, 61 | trash, 62 | about, 63 | }; 64 | 65 | module.exports = VueMarkDownEditor; 66 | -------------------------------------------------------------------------------- /src/lib/Markdown.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 使用 markdown-it 解析 3 | * 默认功能: 4 | * 代码块(>),无序列表(-),标题(#) 5 | */ 6 | 7 | const markdownIt = require('markdown-it'); 8 | //region使用插件 9 | // 表情 10 | const emoji = require('markdown-it-emoji'); 11 | // 下标 12 | const sub = require('markdown-it-sub') 13 | // 上标 14 | const sup = require('markdown-it-sup') 15 | //
16 | const deflist = require('markdown-it-deflist') 17 | // 18 | const abbr = require('markdown-it-abbr') 19 | // footnote 20 | const footnote = require('markdown-it-footnote') 21 | // insert 带有下划线 样式 ++ ++ 22 | const insert = require('markdown-it-ins') 23 | // mark 24 | const mark = require('markdown-it-mark') 25 | // taskLists 26 | const taskLists = require('markdown-it-task-lists') 27 | // container 用于创建自定义的块级容器 28 | const container = require('markdown-it-container') 29 | // 30 | const toc = require('markdown-it-toc') 31 | // math katex 32 | const katex = require('markdown-it-katex-external'); 33 | //图片预览 34 | const miip = require('markdown-it-images-preview'); 35 | 36 | // const mhljs = require('markdown-it-highlightjs'); 37 | // 38 | // const mihe = require('markdown-it-highlightjs-external'); 39 | import hljsLangs from '../core/hljs/lang.hljs.js' 40 | const hljs = require('highlight.js'); 41 | const missLangs = {}; 42 | const needLangs = []; 43 | const hljs_opts = { 44 | hljs: 'auto', 45 | highlighted: true, 46 | langCheck: function(lang) { 47 | console.log(langCheck); 48 | if (lang && hljsLangs[lang] && !missLangs[lang]) { 49 | missLangs[lang] = 1; 50 | needLangs.push(hljsLangs[lang]) 51 | } 52 | } 53 | }; 54 | //endregion 55 | const defaultConfig = { 56 | html: true, // Enable HTML tags in source 57 | xhtmlOut: true, // Use '/' to close single tags (
). 58 | breaks: true, // Convert '\n' in paragraphs into
59 | langPrefix: 'lang-', // CSS language prefix for fenced blocks. Can be 60 | linkify: false, // 自动识别url 61 | typographer: true, 62 | quotes: '“”‘’', 63 | highlight: function (str, lang) { 64 | str = str.replace(/</g, "<"); 65 | str = str.replace(/>/g, ">"); 66 | // console.log('highlight',str, lang); 67 | if (lang && hljs.getLanguage(lang)) { 68 | try { 69 | return '
' +
 70 |           hljs.highlight(lang, str, true).value +
 71 |           '
'; 72 | } catch (__) {} 73 | } 74 | 75 | return '
' + md.utils.escapeHtml(str) + '
'; 76 | } 77 | } 78 | const md = new markdownIt(defaultConfig); 79 | 80 | const defaultRender = md.renderer.rules.link_open || function(tokens, idx, options, env, self) { 81 | return self.renderToken(tokens, idx, options); 82 | }; 83 | md.renderer.rules.link_open = function (tokens, idx, options, env, self) { 84 | // If you are sure other plugins can't add `target` - drop check below 85 | var aIndex = tokens[idx].attrIndex('target'); 86 | 87 | if (aIndex < 0) { 88 | tokens[idx].attrPush(['target', '_blank']); // add new attribute 89 | } else { 90 | tokens[idx].attrs[aIndex][1] = '_blank'; // replace value of existing attr 91 | } 92 | 93 | // pass token to default renderer. 94 | return defaultRender(tokens, idx, options, env, self); 95 | }; 96 | 97 | 98 | md.use(emoji) 99 | .use(sup) 100 | .use(sub) 101 | .use(container) 102 | .use(container, 'hljs-left') /* align left */ 103 | .use(container, 'hljs-center')/* align center */ 104 | .use(container, 'hljs-right')/* align right */ 105 | .use(deflist) 106 | .use(abbr) 107 | .use(footnote) 108 | .use(insert) 109 | .use(mark) 110 | .use(miip) 111 | .use(katex) 112 | .use(taskLists) 113 | .use(toc) 114 | // .use(mhljs) 115 | 116 | export default md; 117 | -------------------------------------------------------------------------------- /src/lib/MarkdownFunction.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 滚动条联动 3 | */ 4 | let mainFlag = false; // 抵消两个滚动事件之间互相触发 5 | let preFlag = false; // 如果两个 flag 都为 true,证明是反弹过来的事件引起的 6 | export const scrollLink = (event, who,editor,preview) => { 7 | let radio = (editor.scrollHeight - editor.clientHeight) / (preview.scrollHeight - preview.clientHeight); 8 | if(who == 'preview'){ 9 | preFlag = true; 10 | if (mainFlag === true){ // 抵消两个滚动事件之间互相触发 11 | mainFlag = false; 12 | preFlag = false; 13 | return; 14 | } 15 | // console.log(who,radio,preview.scrollHeight,editor.scrollHeight); 16 | editor.scrollTop = Math.round(preview.scrollTop * radio); 17 | return; 18 | } 19 | if(who == 'editor'){ 20 | mainFlag = true; 21 | if (preFlag === true){ // 抵消两个滚动事件之间互相触发 22 | mainFlag = false; 23 | preFlag = false; 24 | return; 25 | } 26 | // console.log(who,radio); 27 | preview.scrollTop = Math.round( editor.scrollTop / radio); 28 | return; 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/toolBar/about.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 31 | 32 | 44 | -------------------------------------------------------------------------------- /src/toolBar/align-center.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/align-left.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/align-right.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/bold.vue: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/toolBar/code.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 24 | 25 | 57 | 58 | 66 | -------------------------------------------------------------------------------- /src/toolBar/emoji.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 68 | 69 | 101 | -------------------------------------------------------------------------------- /src/toolBar/header.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 45 | 46 | 51 | -------------------------------------------------------------------------------- /src/toolBar/italic.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/link.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 22 | 23 | 45 | 46 | 54 | -------------------------------------------------------------------------------- /src/toolBar/list-ol.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/list-ul.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/picture.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 29 | 30 | 157 | 158 | 163 | -------------------------------------------------------------------------------- /src/toolBar/quote-left.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/repeat.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/separator.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 18 | -------------------------------------------------------------------------------- /src/toolBar/strikethrough.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/subscript.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/superscript.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/table.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 81 | 82 | 99 | -------------------------------------------------------------------------------- /src/toolBar/thumb-tack.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /src/toolBar/trash.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 27 | 28 | 33 | -------------------------------------------------------------------------------- /src/toolBar/underline.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /src/toolBar/undo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 25 | 26 | 31 | -------------------------------------------------------------------------------- /webpack.build.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin'); 4 | 5 | function resolve (dir) { 6 | return path.join(__dirname, '..', dir) 7 | } 8 | 9 | const extractCSS = new ExtractTextPlugin('css/[name].css'); 10 | 11 | module.exports = { 12 | entry: path.resolve(__dirname, './src/index.js'), 13 | output: { 14 | path: path.resolve(__dirname, './dist'), 15 | publicPath: './', 16 | filename: 'vue-bl-mark-down-editor.js', 17 | chunkFilename: 'js/[name].js', 18 | library: 'MarkDownEditor', 19 | libraryTarget: 'umd', 20 | umdNamedDefine: true 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.css$/, 26 | use: extractCSS.extract({ 27 | fallback: "style-loader", 28 | use: [ 29 | "css-loader", 30 | "vue-style-loader" 31 | ] 32 | }) 33 | // use: [ 34 | // 'vue-style-loader', 35 | // 'css-loader' 36 | // ], 37 | }, 38 | { 39 | test: /\.vue$/, 40 | loader: 'vue-loader', 41 | options: { 42 | extractCSS: true, 43 | loaders: { 44 | } 45 | // other vue-loader options go here 46 | } 47 | }, 48 | { 49 | test: /\.js$/, 50 | loader: 'babel-loader', 51 | exclude: /node_modules/ 52 | }, 53 | { 54 | test: /\.(png|jpg|gif)$/, 55 | loader: 'file-loader', 56 | options: { 57 | limit: 10000, 58 | publicPath: '../', 59 | name: 'images/[name].[ext]?[hash:7]' 60 | } 61 | }, 62 | //{ test: /\.(woff|ttf|eot|svg)$/, loader: 'url-loader?name=font/[name].[hash:7].[ext]&publicPath=../' }, 63 | { 64 | test: /\.(eot|ttf|woff|woff2|svg)(\?.*)?$/, 65 | loader: 'file-loader', 66 | options: { 67 | limit: 10000, 68 | publicPath: '../', 69 | name: 'fonts/[name].[hash:7].[ext]' 70 | } 71 | } 72 | ] 73 | }, 74 | resolve: { 75 | alias: { 76 | 'vue$': 'vue/dist/vue.esm.js', 77 | '@': resolve('src'), 78 | }, 79 | extensions: ['*', '.js', '.vue', '.json'] 80 | }, 81 | devServer: { 82 | historyApiFallback: true, 83 | noInfo: true, 84 | overlay: true 85 | }, 86 | performance: { 87 | hints: false 88 | }, 89 | devtool: '#source-map', 90 | plugins:[ 91 | // 分离css 92 | extractCSS, 93 | new webpack.DefinePlugin({ 94 | 'process.env': { 95 | NODE_ENV: '"production"' 96 | } 97 | }), 98 | new webpack.optimize.UglifyJsPlugin({ 99 | sourceMap: false, 100 | compress: { 101 | warnings: false, 102 | drop_debugger: true, 103 | drop_console: true 104 | } 105 | }), 106 | new webpack.LoaderOptionsPlugin({ 107 | minimize: true 108 | }), 109 | ] 110 | } 111 | 112 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | var HtmlWebpackPlugin = require('html-webpack-plugin') 4 | 5 | function resolve (dir) { 6 | return path.join(__dirname, '..', dir) 7 | } 8 | 9 | module.exports = { 10 | entry: './src/dev/main.js', 11 | output: { 12 | path: path.resolve(__dirname, './html'), 13 | publicPath: '/', 14 | filename: '[name].[hash:7].js', 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.css$/, 20 | use: [ 21 | 'vue-style-loader', 22 | 'css-loader' 23 | ], 24 | }, { 25 | test: /\.vue$/, 26 | loader: 'vue-loader', 27 | options: { 28 | loaders: { 29 | } 30 | // other vue-loader options go here 31 | } 32 | }, 33 | { 34 | test: /\.js$/, 35 | loader: 'babel-loader', 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(png|jpg|gif|svg)$/, 40 | loader: 'file-loader', 41 | options: { 42 | name: 'images/[name].[ext]?[hash]' 43 | } 44 | }, 45 | { 46 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 47 | loader: 'url-loader', 48 | options: { 49 | limit: 10000, 50 | name: 'fonts/[name].[hash:7].[ext]' 51 | } 52 | } 53 | ] 54 | }, 55 | resolve: { 56 | alias: { 57 | 'vue$': 'vue/dist/vue.esm.js', 58 | '@': resolve('src'), 59 | }, 60 | extensions: ['*', '.js', '.vue', '.json'] 61 | }, 62 | devServer: { 63 | historyApiFallback: true, 64 | noInfo: true, 65 | overlay: true 66 | }, 67 | performance: { 68 | hints: false 69 | }, 70 | devtool: '#eval-source-map', 71 | plugins: [ 72 | new HtmlWebpackPlugin({ 73 | filename: path.resolve(__dirname, './html/index.html'), 74 | template: path.resolve(__dirname, './src/dev/index.html'), 75 | inject: true 76 | }), 77 | ] 78 | } 79 | 80 | if (process.env.NODE_ENV === 'production') { 81 | module.exports.output = { 82 | path: path.resolve(__dirname, './html'), 83 | publicPath: './', 84 | filename: '[name].[hash:7].js', 85 | }, 86 | module.exports.devtool = '#source-map' 87 | // http://vue-loader.vuejs.org/en/workflow/production.html 88 | module.exports.plugins = (module.exports.plugins || []).concat([ 89 | new webpack.DefinePlugin({ 90 | 'process.env': { 91 | NODE_ENV: '"production"' 92 | } 93 | }), 94 | new webpack.optimize.UglifyJsPlugin({ 95 | sourceMap: false, 96 | compress: { 97 | warnings: false 98 | } 99 | }), 100 | new webpack.LoaderOptionsPlugin({ 101 | minimize: true 102 | }), 103 | 104 | ]) 105 | } 106 | --------------------------------------------------------------------------------