manage data with vux

This commit is contained in:
JimQing 2020-05-01 21:30:25 +08:00
parent 8b57302416
commit f0eaa61659
10 changed files with 757 additions and 483 deletions

View File

@ -17,16 +17,23 @@
Markdown 文档自动即时渲染为微信图文,让你不再为微信文章排版而发愁!只要你会基本的 Markdown 语法,就能做出一篇样式简洁而又美观大方的微信图文。 Markdown 文档自动即时渲染为微信图文,让你不再为微信文章排版而发愁!只要你会基本的 Markdown 语法,就能做出一篇样式简洁而又美观大方的微信图文。
## 在线编辑器地址 ## 在线编辑器地址
- GitHub Pagehttps://doocs.github.io/md - Netlify: https://mdmd.netlify.app
- Gitee Pagehttps://doocs.gitee.io/md - Gitee Pageshttps://doocs.gitee.io/md
- GitHub Pageshttps://doocs.github.io/md
注:推荐使用 Chrome 浏览器,效果最佳。另外,对于国内(中国)的朋友,访问 [Gitee Page](https://doocs.gitee.io/md) 速度会相对快一些。 注:推荐使用 Chrome 浏览器,效果最佳。另外,对于国内(中国)的朋友,访问 [Gitee Pages](https://doocs.gitee.io/md) 速度会相对快一些。
## 为何二次开发 ## 为何二次开发
现有的开源微信 Markdown 编辑器,样式繁杂,也不符合我个人的审美需求。在我使用它们进行文章排版的时候,经常还要自己做一些改动,费时费力,因此动手做了二次开发。 现有的开源微信 Markdown 编辑器,样式繁杂,也不符合我个人的审美需求。在我使用它们进行文章排版的时候,经常还要自己做一些改动,费时费力,因此动手做了二次开发。
欢迎各位朋友随时提交 PR让这款微信 Markdown 编辑器变得更好!如果你有新的想法,也欢迎在 Issues 区反馈。 欢迎各位朋友随时提交 PR让这款微信 Markdown 编辑器变得更好!如果你有新的想法,也欢迎在 Issues 区反馈。
注:目前在非 master 分支上对项目进行重构,更多新特性,敬请期待!
- [Vue 分支](https://github.com/doocs/md/tree/m_create_vue)
- [React 分支](https://github.com/doocs/md/tree/chore-webpack)
## 功能特性 ## 功能特性
- [x] 支持 Markdown 所有基础语法 - [x] 支持 Markdown 所有基础语法
- [x] 支持单独进行字体、字号设置 - [x] 支持单独进行字体、字号设置
@ -57,8 +64,55 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章
![doocs-md-upload-image](https://imgkr.cn-bj.ufileos.com/97db3cd6-bddc-4eff-8635-472631b0a642.gif) ![doocs-md-upload-image](https://imgkr.cn-bj.ufileos.com/97db3cd6-bddc-4eff-8635-472631b0a642.gif)
## 谁在使用
<table>
<tr>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/RNKDCK2KoyeuMeEs6GUrow">
<img src="https://imgkr.cn-bj.ufileos.com/29fbfc6e-b1f2-4995-982f-74f993256626.png" style="width: 50px;"><br>
<sub>Doocs开源社区</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/FpGIX9viQR6Z9iSCEPH86g">
<img src="https://imgkr.cn-bj.ufileos.com/2631fe1d-0521-4f51-abb9-5250f4dda268.jpg" style="width: 50px;"><br>
<sub>掘墓人的小铲子</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/yB3ZH3jmcF_LbzuKmnR9BQ">
<img src="https://imgkr.cn-bj.ufileos.com/4b4b10a4-1146-4056-8799-9f8c1a3e5e9e.png" style="width: 50px;"><br>
<sub>全网重点</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/oc5Z2t9ykbu_Dezjnw5mfQ">
<img src="https://imgkr.cn-bj.ufileos.com/1ddb47f6-4943-4aae-ad24-c75c22c758bf.png" style="width: 50px;"><br>
<sub>爱码士的内心独白</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/SFde8OsZ8FzNGMHwpmDtrg">
<img src="https://imgkr.cn-bj.ufileos.com/830333b7-74b2-4dbc-9384-b5cac63b1d17.jpg" style="width: 50px;"><br>
<sub>乐玩nodejs npm工具库</sub>
</a>
</td>
<td align="center" style="width: 80px;">
<a href="https://mp.weixin.qq.com/s/7UG24ZugfI5ZnhUpo8vfvQ">
<img src="https://imgkr.cn-bj.ufileos.com/95e553de-fd8f-4374-8a98-14809122e80e.jpg" style="width: 50px;"><br>
<sub>简静慢</sub>
</a>
</td>
</tr>
</table>
注:如果你使用了本 Markdown 编辑器进行文章排版,并且希望在本项目 README 中展示你的公众号,请到 [#5](https://github.com/doocs/md/issues/5) 留言。
## 示例文章 ## 示例文章
- [ES6 特性快速扫盲](https://mp.weixin.qq.com/s/I3EzOO0skf8xDCGtyYM5Lg) - [全网首发GPU 驱动自升级原理详解](https://mp.weixin.qq.com/s/7UG24ZugfI5ZnhUpo8vfvQ)
- [Quick Start - 天下武功,唯快不破!效率工具,老少皆宜](https://mp.weixin.qq.com/s/SFde8OsZ8FzNGMHwpmDtrg)
- [死磕JavaScript系列之原来你是对象(一)](https://mp.weixin.qq.com/s/oc5Z2t9ykbu_Dezjnw5mfQ)
- [一文多发神器--ArtiPub&OpenWrite](https://mp.weixin.qq.com/s/FpGIX9viQR6Z9iSCEPH86g)
- [免费且好用的图床,就你了,「图壳」!](https://mp.weixin.qq.com/s/0HhgHLo_tTRFZcC-CVjDbw) - [免费且好用的图床,就你了,「图壳」!](https://mp.weixin.qq.com/s/0HhgHLo_tTRFZcC-CVjDbw)
- [GitHub 项目持续本地化,交给它来做,准没错!](https://mp.weixin.qq.com/s/KO4xHr4EI0YfjF0hiT3pbw) - [GitHub 项目持续本地化,交给它来做,准没错!](https://mp.weixin.qq.com/s/KO4xHr4EI0YfjF0hiT3pbw)
- [阿里又一个 20k+ stars 开源项目诞生,恭喜 fastjson](https://mp.weixin.qq.com/s/RNKDCK2KoyeuMeEs6GUrow) - [阿里又一个 20k+ stars 开源项目诞生,恭喜 fastjson](https://mp.weixin.qq.com/s/RNKDCK2KoyeuMeEs6GUrow)
@ -66,8 +120,6 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章
- [刷掉 90 % 候选人的海量数据面试题(附题解+方法总结)](https://mp.weixin.qq.com/s/rjGqxUvrEqJNlo09GrT1Dw) - [刷掉 90 % 候选人的海量数据面试题(附题解+方法总结)](https://mp.weixin.qq.com/s/rjGqxUvrEqJNlo09GrT1Dw)
- [GitHub 标星 11.5k 的一款开源工具,助你轻松查看 Git 历史](https://mp.weixin.qq.com/s/PK-ikENqF13Lmqy2pcMhYQ) - [GitHub 标星 11.5k 的一款开源工具,助你轻松查看 Git 历史](https://mp.weixin.qq.com/s/PK-ikENqF13Lmqy2pcMhYQ)
注:如果你使用了本 Markdown 编辑器进行文章排版,并且希望将你的文章加入示例列表,欢迎随时提交 PR。
## 项目维护者 ## 项目维护者
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->

32
src/api/fetch.js Normal file
View File

@ -0,0 +1,32 @@
import axios from 'axios';
// 创建axios实例
const service = axios.create({
baseURL: '',
timeout: 10 * 1000 // 请求超时时间
});
service.interceptors.request.use(
config => {
if (/^(post)|(put)|(delete)$/i.test(config.method)) {
if (config.data && config.data.upload) {
config.headers['Content-Type'] = 'multipart/form-data';
config.headers['Access-Control-Allow-Origin'] = 'http://192.168.0.106:8080';
}
}
return config;
}, error => {
Promise.reject(error);
}
);
service.interceptors.response.use(res => {
if (res.data.success) {
return res.data;
} else {
console.log(res);
}
return Promise.reject(res.data);
}, error => Promise.reject(error));
export default service;

15
src/api/file.js Normal file
View File

@ -0,0 +1,15 @@
import fetch from './fetch';
function fileUpload(data) {
return fetch({
url: 'https://imgkr.com/api/files/upload',
method: 'post',
data: data
})
}
export default {
fileUpload
};

View File

@ -25,7 +25,7 @@
<el-form size="mini" class="ctrl" :inline=true> <el-form size="mini" class="ctrl" :inline=true>
<el-form-item> <el-form-item>
<el-select v-model="currentFont" size="mini" placeholder="选择字体" clearable @change="fontChanged"> <el-select v-model="currentFont" size="mini" placeholder="选择字体" clearable @change="fontChanged">
<el-option v-for="font in builtinFonts" :style="{fontFamily: font.value}" :key="font.value" <el-option v-for="font in config.builtinFonts" :style="{fontFamily: font.value}" :key="font.value"
:label="font.label" :value="font.value"> :label="font.label" :value="font.value">
<span class="select-item-left">{{ font.label }}</span> <span class="select-item-left">{{ font.label }}</span>
<span class="select-item-right">Abc</span> <span class="select-item-right">Abc</span>
@ -34,7 +34,7 @@
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-select v-model="currentSize" size="mini" placeholder="选择段落字号" clearable @change="sizeChanged"> <el-select v-model="currentSize" size="mini" placeholder="选择段落字号" clearable @change="sizeChanged">
<el-option v-for="size in sizeOption" :key="size.value" :label="size.label" :value="size.value"> <el-option v-for="size in config.sizeOption" :key="size.value" :label="size.label" :value="size.value">
<span class="select-item-left">{{ size.label }}</span> <span class="select-item-left">{{ size.label }}</span>
<span class="select-item-right">{{ size.desc }}</span> <span class="select-item-right">{{ size.desc }}</span>
</el-option> </el-option>
@ -42,7 +42,7 @@
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-select v-model="currentColor" size="mini" placeholder="选择颜色" clearable @change="colorChanged"> <el-select v-model="currentColor" size="mini" placeholder="选择颜色" clearable @change="colorChanged">
<el-option v-for="color in colorOption" :key="color.value" :label="color.label" :value="color.value"> <el-option v-for="color in config.colorOption" :key="color.value" :label="color.label" :value="color.value">
<span class="select-item-left">{{ color.label }}</span> <span class="select-item-left">{{ color.label }}</span>
<span class="select-item-right">{{ color.hex }}</span> <span class="select-item-right">{{ color.hex }}</span>
</el-option> </el-option>
@ -71,7 +71,7 @@
</el-col> </el-col>
<el-col :span="12" class="preview-wrapper" id="preview"> <el-col :span="12" class="preview-wrapper" id="preview">
<section id="output-wrapper"> <section id="output-wrapper">
<div class="preview" contenteditable="true" > <div class="preview" contenteditable="true">
<section id="output" v-html="output"> <section id="output" v-html="output">
</section> </section>
</div> </div>
@ -105,12 +105,12 @@
</span> </span>
</el-dialog> </el-dialog>
<el-dialog title="插入表格" :visible.sync="dialogFormVisible"> <el-dialog title="插入表格" :visible.sync="dialogFormVisible">
<el-form :model="form"> <el-form :model="config.form">
<el-form-item label="行数(表头不计入行数)"> <el-form-item label="行数(表头不计入行数)">
<el-input v-model="form.rows"></el-input> <el-input v-model="config.form.rows"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="列数"> <el-form-item label="列数">
<el-input v-model="form.cols"></el-input> <el-input v-model="config.form.cols"></el-input>
</el-form-item> </el-form-item>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
@ -121,111 +121,85 @@
</div> </div>
</template> </template>
<script> <script>
import CodeMirror from 'codemirror/lib/codemirror' import CodeMirror from 'codemirror/lib/codemirror'
import 'codemirror/mode/css/css' import 'codemirror/mode/css/css'
import 'codemirror/mode/markdown/markdown' import 'codemirror/mode/markdown/markdown'
import 'codemirror/addon/edit/matchbrackets' import 'codemirror/addon/edit/matchbrackets'
import 'codemirror/addon/selection/active-line' import 'codemirror/addon/selection/active-line'
import 'codemirror/addon/hint/show-hint.js' import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/css-hint.js' import 'codemirror/addon/hint/css-hint.js'
import '../scripts/format.js' import '../scripts/format.js'
import axios from 'axios' import fileApi from '../api/file';
import WxRenderer from '../scripts/renderers/wx-renderer' import marked from 'marked'
import marked from 'marked' import markdown from 'markdown'
import markdown from 'markdown' import juice from 'juice'
import juice from 'juice' import EditorHeader from './codeMirror/header';
import { import {
setColorWithCustomTemplate, setColorWithCustomTemplate,
setColor, setColor,
setFontSize, setFontSize,
css2json, css2json,
customCssWithTemplate customCssWithTemplate,
} from '../scripts/util' saveEditorContent,
import DEFAULT_CONTENT from '../scripts/default-content' checkImage
import DEFAULT_CSS_CONTENT from '../scripts/themes/default-theme-css' } from '../scripts/util'
import DEFAULT_CSS_CONTENT from '../scripts/themes/default-theme-css'
require('codemirror/mode/javascript/javascript') require('codemirror/mode/javascript/javascript')
import '../scripts/closebrackets' import '../scripts/closebrackets'
import $ from 'jquery' import $ from 'jquery'
import { solveWeChatImage, solveHtml, copySafari } from '../scripts/converter' import {
export default { solveWeChatImage,
data () { solveHtml,
let d = { copySafari
wxRenderer: null, } from '../scripts/converter'
aboutOutput: '', import config from '../scripts/config'
output: '', import {mapState, mapMutations} from 'vuex';
source: '', export default {
editor: null, data() {
cssEditor: null, return {
builtinFonts: [ config: config,
{ label: '无衬线', value: '-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif' },
{ label: '衬线', value: "Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif" }
],
sizeOption: [
{ label: '12px', value: '12px', desc: '更小' },
{ label: '13px', value: '13px', desc: '稍小' },
{ label: '14px', value: '14px', desc: '推荐' },
{ label: '15px', value: '15px', desc: '稍大' },
{ label: '16px', value: '16px', desc: '更大' }
],
colorOption: [
{ label: '经典蓝', value: 'rgba(15, 76, 129, 1)', hex: '最新流行' },
{ label: '翡翠绿', value: 'rgba(0, 152, 116, 1)', hex: '优雅清新' },
{ label: '活力橘', value: 'rgba(250, 81, 81, 1)', hex: '热情活泼' }
],
showBox: false, showBox: false,
aboutDialogVisible: false, aboutDialogVisible: false,
dialogFormVisible: false, dialogFormVisible: false,
form: {
rows: 1,
cols: 1
},
timeout: null, timeout: null,
html: '' source: '',
status: '1'
} }
d.currentFont = d.builtinFonts[0].value
d.currentSize = d.sizeOption[2].value
d.currentColor = d.colorOption[1].value
d.status = '1'
return d
}, },
created () { components: {
this.currentFont = localStorage.getItem('fonts') || this.builtinFonts[0].value EditorHeader
this.currentColor = localStorage.getItem('color') || this.colorOption[1].value },
this.currentSize = localStorage.getItem('size') || this.sizeOption[2].value computed: {
this.status = localStorage.getItem('status') === 'true' ...mapState({
wxRenderer: state=> state.wxRenderer,
output: state=> state.output,
editor: state=> state.editor,
cssEditor: state=> state.cssEditor,
html: state=> state.html,
currentFont: state=> state.currentFont,
currentSize: state=> state.currentSize,
currentColor: state=> state.currentColor
})
},
created() {
this.initEditorState()
this.$nextTick(() => { this.$nextTick(() => {
this.initEditor() this.initEditor()
this.initCssEditor() this.initCssEditor()
}) this.editorRefresh()
this.wxRenderer = new WxRenderer({
theme: setColor(this.currentColor),
fonts: this.currentFont,
size: this.currentSize,
status: this.status
}) })
}, },
methods: { methods: {
initEditor() { initEditor() {
this.editor = CodeMirror.fromTextArea( this.initEditorEntity();
document.getElementById('editor'),
{
value: '',
mode: 'text/x-markdown',
theme: 'xq-light',
lineNumbers: false,
lineWrapping: true,
styleActiveLine: true,
autoCloseBrackets: true
}
)
this.editor.on('change', (cm, e) => { this.editor.on('change', (cm, e) => {
this.refresh() this.editorRefresh()
this.saveEditorContent(this.editor, '__editor_content') saveEditorContent(this.editor, '__editor_content')
}) });
// //
this.editor.on('paste', (cm, e) => { this.editor.on('paste', (cm, e) => {
@ -236,142 +210,66 @@ export default {
let item = e.clipboardData.items[i] let item = e.clipboardData.items[i]
if (item.kind === 'file') { if (item.kind === 'file') {
const pasteFile = item.getAsFile() const pasteFile = item.getAsFile()
if (!(this.checkType(pasteFile) && this.checkImageSize(pasteFile))) { const checkImageResult = checkImage(pasteFile);
return
if (checkImageResult) {
this.$message({
showClose: true,
message: checkImageResult,
type: 'error'
});
return;
} }
let data = new FormData() let data = new FormData()
data.append('file', pasteFile) data.append('file', pasteFile)
axios.post(
'https://imgkr.com/api/files/upload', fileApi.fileUpload(data).then(res => {
data,
{
headers: { 'Content-Type': 'multipart/form-data' }
}
).then(resp => {
this.uploaded(resp.data) this.uploaded(resp.data)
}).catch(err => { }).catch(err => {
console.log(err.message) console.log(err.message)
}) })
} }
} }
}) });
//
this.loadLocalStorage(this.editor, '__editor_content', DEFAULT_CONTENT)
}, },
initCssEditor() { initCssEditor() {
this.cssEditor = CodeMirror.fromTextArea( this.initCssEditorEntity();
document.getElementById('cssEditor'), {
value: '',
mode: 'css',
theme: 'style-mirror',
lineNumbers: false,
lineWrapping: true,
matchBrackets: true,
autofocus: true,
extraKeys: {
'Ctrl-F': function autoFormat (editor) {
const totalLines = editor.lineCount()
editor.autoFormatRange({ line: 0, ch: 0 }, { line: totalLines })
}
}
}
)
// //
this.cssEditor.on('keyup', (cm, e) => { this.cssEditor.on('keyup', (cm, e) => {
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) { if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
cm.showHint(e) cm.showHint(e)
} }
}) });
this.cssEditor.on('update', (instance) => { this.cssEditor.on('update', (instance) => {
this.cssChanged() this.cssChanged()
this.saveEditorContent(this.cssEditor, '__css_content') saveEditorContent(this.cssEditor, '__css_content')
}) })
this.loadLocalStorage(this.cssEditor, '__css_content', DEFAULT_CSS_CONTENT)
},
renderWeChat (source) {
let output = marked(source, { renderer: this.wxRenderer.getRenderer(this.status) })
// margin-top
output = output.replace(/(style=".*?)"/, '$1;margin-top: 0"')
if (this.status) {
//
output += this.wxRenderer.buildFootnotes()
// style
output += this.wxRenderer.buildAddition()
}
return output
},
fontChanged (fonts) {
this.wxRenderer.setOptions({
fonts: fonts
})
this.currentFont = fonts
localStorage.setItem('fonts', fonts)
this.refresh()
},
sizeChanged (size) {
this.wxRenderer.setOptions({
size: size
})
let theme = setFontSize(size.replace('px', ''))
theme = setColorWithCustomTemplate(theme, this.currentColor)
this.wxRenderer.setOptions({
theme: theme
})
this.currentSize = size
localStorage.setItem('size', size)
this.refresh()
},
colorChanged (color) {
let theme = setFontSize(this.currentSize.replace('px', ''))
theme = setColorWithCustomTemplate(theme, color)
this.wxRenderer.setOptions({
theme: theme
})
this.currentColor = color
localStorage.setItem('color', color)
this.refresh()
},
cssChanged () {
let json = css2json(this.cssEditor.getValue(0))
console.log(json)
let theme = setFontSize(this.currentSize.replace('px', ''))
theme = customCssWithTemplate(json, this.currentColor, theme)
this.wxRenderer.setOptions({
theme: theme
})
this.refresh()
}, },
// //
beforeUpload (file) { beforeUpload(file) {
return this.checkType(file) && this.checkImageSize(file) const checkImageResult = checkImage(file);
},
// if (checkImageResult) {
checkType (file) {
if (!/\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name)) {
this.$message({ this.$message({
showClose: true, showClose: true,
message: '请上传 JPG/PNG/GIF 格式的图片', message: checkImageResult,
type: 'error' type: 'error'
}) });
return false return false;
} }
return true return true;
}, },
// cssChanged() {
checkImageSize (file) { let json = css2json(this.cssEditor.getValue(0))
if (file.size > 5 * 1024 * 1024) { let theme = setFontSize(this.currentSize.replace('px', ''))
this.$message({ theme = customCssWithTemplate(json, this.currentColor, theme)
showClose: true, this.setWxRendererOptions({
message: '由于公众号限制,图片大小不能超过 5.0M', theme: theme
type: 'error' });
}) this.editorRefresh()
return false
}
return true
}, },
// //
uploaded (response, file, fileList) { uploaded(response, file, fileList) {
if (response.success) { if (response.success) {
// //
const cursor = this.editor.getCursor() const cursor = this.editor.getCursor()
@ -384,7 +282,7 @@ export default {
message: '图片插入成功', message: '图片插入成功',
type: 'success' type: 'success'
}) })
this.refresh() this.editorRefresh()
} else { } else {
// //
this.$message({ this.$message({
@ -394,38 +292,11 @@ export default {
}) })
} }
}, },
//
refresh () {
this.output = this.renderWeChat(this.editor.getValue(0))
},
//
reset () {
this.$confirm('此操作将丢失本地缓存的文本和自定义样式,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--success is-plain',
type: 'warning',
center: true
}).then(() => {
localStorage.clear()
this.editor.setValue(DEFAULT_CONTENT)
this.cssEditor.setValue(DEFAULT_CSS_CONTENT)
this.editor.focus()
this.status = '1'
this.fontChanged(this.builtinFonts[0].value)
this.colorChanged(this.colorOption[1].value)
this.sizeChanged(this.sizeOption[2].value)
this.cssChanged()
}).catch(() => {
this.editor.focus()
})
},
// //
insertTable () { insertTable() {
const cursor = this.editor.getCursor() const cursor = this.editor.getCursor()
const rows = parseInt(this.form.rows) const rows = parseInt(this.config.form.rows)
const cols = parseInt(this.form.cols) const cols = parseInt(this.config.form.cols)
if (isNaN(rows) || isNaN(cols) || rows < 1 || cols < 1) { if (isNaN(rows) || isNaN(cols) || rows < 1 || cols < 1) {
this.$message({ this.$message({
showClose: true, showClose: true,
@ -445,62 +316,16 @@ export default {
this.editor.replaceSelection(`\n${table}\n`, cursor) this.editor.replaceSelection(`\n${table}\n`, cursor)
this.dialogFormVisible = false this.dialogFormVisible = false
this.refresh() this.editorRefresh()
},
statusChanged () {
localStorage.setItem('status', this.status)
this.refresh()
},
// LocalStorage
saveEditorContent (editor, name) {
const content = editor.getValue(0)
if (content) {
localStorage.setItem(name, content)
} else {
localStorage.removeItem(name)
}
},
loadLocalStorage (editor, name, content) {
if (localStorage.getItem(name)) {
editor.setValue(localStorage.getItem(name))
} else {
editor.setValue(content)
}
},
//
downloadEditorContent () {
let downLink = document.createElement('a')
downLink.download = 'content.md'
downLink.style.display = 'none'
let blob = new Blob([this.editor.getValue(0)])
downLink.href = URL.createObjectURL(blob)
document.body.appendChild(downLink)
downLink.click()
document.body.removeChild(downLink)
},
// CSS
async customStyle () {
this.showBox = !this.showBox
this.$nextTick(() => {
if(!this.cssEditor) {
this.cssEditor.refresh()
// this.initCssEditor()
}
})
setTimeout(() => {
this.cssEditor.refresh()
},50)
let flag = await localStorage.getItem('__css_content')
if (!flag) {
this.cssEditor.setValue(DEFAULT_CSS_CONTENT)
}
}, },
// //
copy12 () { copy12() {
let clipboardDiv = document.getElementById('output') let clipboardDiv = document.getElementById('output')
clipboardDiv.focus() clipboardDiv.focus()
window.getSelection().removeAllRanges() window.getSelection().removeAllRanges()
let range = document.createRange() let range = document.createRange()
range.setStartBefore(clipboardDiv.firstChild) range.setStartBefore(clipboardDiv.firstChild)
range.setEndAfter(clipboardDiv.lastChild) range.setEndAfter(clipboardDiv.lastChild)
window.getSelection().addRange(range) window.getSelection().addRange(range)
@ -532,7 +357,7 @@ export default {
}, },
// //
leftAndRightScroll() { leftAndRightScroll() {
$('div.CodeMirror-scroll, #preview').on('scroll', function callback () { $('div.CodeMirror-scroll, #preview').on('scroll', function callback() {
clearTimeout(this.timeout) clearTimeout(this.timeout)
let source = $(this) let source = $(this)
@ -551,12 +376,96 @@ export default {
target.on('scroll', callback) target.on('scroll', callback)
}, 100) }, 100)
}) })
},
//
downloadEditorContent () {
let downLink = document.createElement('a')
downLink.download = 'content.md'
downLink.style.display = 'none'
let blob = new Blob([this.editor.getValue(0)])
downLink.href = URL.createObjectURL(blob)
document.body.appendChild(downLink)
downLink.click()
document.body.removeChild(downLink)
},
//
reset() {
this.$confirm('此操作将丢失本地缓存的文本和自定义样式,是否继续?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
confirmButtonClass: 'el-button--success',
cancelButtonClass: 'el-button--success is-plain',
type: 'warning',
center: true
}).then(() => {
localStorage.clear()
this.clearEditorToDefault();
this.editor.focus()
this.status = '1';
this.fontChanged(this.config.builtinFonts[0].value)
this.colorChanged(this.config.colorOption[1].value)
this.sizeChanged(this.config.sizeOption[2].value)
this.cssChanged()
}).catch(() => {
this.editor.focus()
})
},
fontChanged (fonts) {
this.setWxRendererOptions({
fonts: fonts
})
this.setCurrentFont(fonts);
this.editorRefresh()
},
sizeChanged (size) {
let theme = setFontSize(size.replace('px', ''))
theme = setColorWithCustomTemplate(theme, this.currentColor)
this.setWxRendererOptions({
size: size,
theme: theme
})
this.setCurrentSize(size);
this.editorRefresh()
},
colorChanged (color) {
let theme = setFontSize(this.currentSize.replace('px', ''))
theme = setColorWithCustomTemplate(theme, color)
this.setWxRendererOptions({
theme: theme
})
this.setCurrentColor(color);
this.editorRefresh()
},
statusChanged () {
localStorage.setItem('status', this.status)
this.editorRefresh()
},
// CSS
async customStyle () {
this.showBox = !this.showBox
this.$nextTick(() => {
if(!this.cssEditor) {
this.cssEditor.refresh()
// this.initCssEditor()
}
})
setTimeout(() => {
this.cssEditor.refresh()
},50)
let flag = await localStorage.getItem('__css_content')
if (!flag) {
this.setCssEditorValue(DEFAULT_CSS_CONTENT)
} }
}, },
mounted () { ...mapMutations(['initEditorState', 'initEditorEntity', 'editorRefresh', 'clearEditorToDefault',
'setCurrentFont', 'setCurrentSize', 'setCurrentColor', 'setEditorValue', 'setCssEditorValue',
'initCssEditorEntity', 'setWxRendererOptions'])
},
mounted() {
this.leftAndRightScroll() this.leftAndRightScroll()
} }
} }
</script> </script>
<style lang="scss"> <style lang="scss">

View File

@ -1,15 +0,0 @@
<template>
<div>
</div>
</template>
<script>
export default {
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,77 @@
<template>
<div>
<!-- 图片上传 -->
<el-upload action="https://imgkr.com/api/files/upload" :headers="{'Content-Type': 'multipart/form-data'}"
:show-file-list="false" :multiple="true" accept=".jpg,.jpeg,.png,.gif" name="file"
:before-upload="beforeUpload" :on-success="uploaded">
<el-tooltip class="item" effect="dark" content="上传图片" placement="bottom-start">
<i class="el-icon-upload" size="medium">&nbsp;</i>
</el-tooltip>
</el-upload>
<!-- 下载文本文档 -->
<el-tooltip class="item" effect="dark" content="下载编辑框Markdown文档" placement="bottom-start">
<i class="el-icon-download" size="medium" @click="downloadEditorContent">&nbsp;</i>
</el-tooltip>
<!-- 页面重置 -->
<el-tooltip class="item" effect="dark" content="重置页面" placement="bottom-start">
<i class="el-icon-refresh" size="medium" @click="reset">&nbsp;</i>
</el-tooltip>
<!-- 插入表格 -->
<el-tooltip class="item" effect="dark" content="插入表格" placement="bottom-start">
<i class="el-icon-s-grid" size="medium" @click="dialogFormVisible = true">&nbsp;</i>
</el-tooltip>
<el-form size="mini" class="ctrl" :inline=true>
<el-form-item>
<el-select v-model="currentFont" size="mini" placeholder="选择字体" clearable @change="fontChanged">
<el-option v-for="font in builtinFonts" :style="{fontFamily: font.value}" :key="font.value"
:label="font.label" :value="font.value">
<span class="select-item-left">{{ font.label }}</span>
<span class="select-item-right">Abc</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="currentSize" size="mini" placeholder="选择段落字号" clearable @change="sizeChanged">
<el-option v-for="size in sizeOption" :key="size.value" :label="size.label" :value="size.value">
<span class="select-item-left">{{ size.label }}</span>
<span class="select-item-right">{{ size.desc }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select v-model="currentColor" size="mini" placeholder="选择颜色" clearable @change="colorChanged">
<el-option v-for="color in colorOption" :key="color.value" :label="color.label" :value="color.value">
<span class="select-item-left">{{ color.label }}</span>
<span class="select-item-right">{{ color.hex }}</span>
</el-option>
</el-select>
</el-form-item>
<el-tooltip content="自定义颜色" placement="top">
<el-color-picker v-model="currentColor" size="mini" show-alpha @change="colorChanged"></el-color-picker>
</el-tooltip>
&nbsp;&nbsp;
<el-tooltip content="微信外链自动转为文末引用" placement="top">
<el-switch v-model="status" active-color="#67c23a" inactive-color="#dcdfe6" @change="statusChanged">
</el-switch>
</el-tooltip>
</el-form>
<el-tooltip class="item" effect="dark" content="自定义CSS样式" placement="left">
<el-button type="success" plain size="medium" icon="el-icon-setting" @click="customStyle"></el-button>
</el-tooltip>
<el-button type="success" plain size="medium" @click="copy">复制</el-button>
<el-button type="success" plain size="medium" class="about" @click="aboutDialogVisible = true">关于</el-button>
</div>
</template>
<script>
export default {
name: 'header',
methods: {
},
}
</script>
<style lang="less" scoped>
</style>

60
src/scripts/config.js Normal file
View File

@ -0,0 +1,60 @@
export default {
builtinFonts: [
{
label: '无衬线',
value: '-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif'
},
{
label: '衬线',
value: "Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif"
}
],
sizeOption: [
{
label: '12px',
value: '12px',
desc: '更小'
},
{
label: '13px',
value: '13px',
desc: '稍小'
},
{
label: '14px',
value: '14px',
desc: '推荐'
},
{
label: '15px',
value: '15px',
desc: '稍大'
},
{
label: '16px',
value: '16px',
desc: '更大'
}
],
colorOption: [
{
label: '经典蓝',
value: 'rgba(15, 76, 129, 1)',
hex: '最新流行'
},
{
label: '翡翠绿',
value: 'rgba(0, 152, 116, 1)',
hex: '优雅清新'
},
{
label: '活力橘',
value: 'rgba(250, 81, 81, 1)',
hex: '热情活泼'
}
],
form: {
rows: 1,
cols: 1
}
};

View File

@ -167,3 +167,29 @@ export function css2json (css) {
// 返回JSON形式的结果串 // 返回JSON形式的结果串
return json return json
} }
/**
* 将编辑器内容保存到 LocalStorage
* @param {*} editor
* @param {*} name
*/
export function saveEditorContent(editor, name) {
const content = editor.getValue(0)
if (content) {
localStorage.setItem(name, content)
} else {
localStorage.removeItem(name)
}
}
export function checkImage(file) {
if (!/\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name)) {
return '请上传 JPG/PNG/GIF 格式的图片';
}
if (file.size > 5 * 1024 * 1024) {
return '由于公众号限制,图片大小不能超过 5.0M';
}
return false;
}

View File

@ -1,15 +1,136 @@
import Vue from 'vue' import Vue from 'vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import config from '../scripts/config';
import WxRenderer from '../scripts/renderers/wx-renderer'
import marked from 'marked'
import CodeMirror from 'codemirror/lib/codemirror'
import DEFAULT_CONTENT from '../scripts/default-content'
import DEFAULT_CSS_CONTENT from '../scripts/themes/default-theme-css'
import {
setColor
} from '../scripts/util'
Vue.use(Vuex) Vue.use(Vuex)
export default new Vuex.Store({ const state = {
state: { wxRenderer: null,
output: '',
editor: null,
cssEditor: null,
html: '',
currentFont: {},
currentSize: {},
currentColor: {}
};
const mutations = {
setEditorValue(state, data) {
state.editor.setValue(data)
}, },
mutations: { setCssEditorValue(state, data) {
state.cssEditor.setValue(data)
}, },
actions: { setWxRendererOptions(state, data) {
state.wxRenderer.setOptions(data);
}, },
modules: { setCurrentFont(state, data) {
state.currentFont = data;
localStorage.setItem('fonts', data)
},
setCurrentSize(state, data) {
state.currentSize = data;
localStorage.setItem('size', data)
},
setCurrentColor(state, data) {
state.currentColor = data;
localStorage.setItem('color', data)
},
initEditorState(state) {
state.currentFont = localStorage.getItem('fonts') || config.builtinFonts[0].value
state.currentColor = localStorage.getItem('color') || config.colorOption[1].value
state.currentSize = localStorage.getItem('size') || config.sizeOption[2].value
state.status = localStorage.getItem('status') === 'true'
state.wxRenderer = new WxRenderer({
theme: setColor(state.currentColor),
fonts: state.currentFont,
size: state.currentSize,
status: state.status
})
},
initEditorEntity(state) {
state.editor = CodeMirror.fromTextArea(
document.getElementById('editor'),
{
value: '',
mode: 'text/x-markdown',
theme: 'xq-light',
lineNumbers: false,
lineWrapping: true,
styleActiveLine: true,
autoCloseBrackets: true
} }
)
// 如果有编辑器内容被保存则读取,否则加载默认内容
if (localStorage.getItem('__editor_content')) {
state.editor.setValue(localStorage.getItem('__editor_content'))
} else {
state.editor.setValue(DEFAULT_CONTENT)
}
},
initCssEditorEntity(state) {
state.cssEditor = CodeMirror.fromTextArea(
document.getElementById('cssEditor'), {
value: '',
mode: 'css',
theme: 'style-mirror',
lineNumbers: false,
lineWrapping: true,
matchBrackets: true,
autofocus: true,
extraKeys: {
'Ctrl-F': function autoFormat(editor) {
const totalLines = editor.lineCount()
editor.autoFormatRange({
line: 0,
ch: 0
}, {
line: totalLines
})
}
}
}
)
// 如果有编辑器内容被保存则读取,否则加载默认内容
if (localStorage.getItem('__css_content')) {
state.cssEditor.setValue(localStorage.getItem('__css_content'))
} else {
state.cssEditor.setValue(DEFAULT_CSS_CONTENT)
}
},
editorRefresh(state) {
let output = marked(state.editor.getValue(0), {
renderer: state.wxRenderer.getRenderer(state.status)
})
// 去除第一行的 margin-top
output = output.replace(/(style=".*?)"/, '$1;margin-top: 0"')
if (state.status) {
// 引用脚注
output += state.wxRenderer.buildFootnotes()
// 附加的一些 style
output += state.wxRenderer.buildAddition()
}
state.output = output
},
clearEditorToDefault(state) {
state.editor.setValue(DEFAULT_CONTENT)
state.cssEditor.setValue(DEFAULT_CSS_CONTENT)
}
}
export default new Vuex.Store({
state,
mutations,
actions: {},
modules: {}
}) })

View File

@ -1,17 +1,14 @@
<template> <template>
<div class="home"> <div class="home">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div> </div>
</template> </template>
<script> <script>
// @ is an alias to /src // @ is an alias to /src
import HelloWorld from '@/components/HelloWorld.vue'
export default { export default {
name: 'home', name: 'home',
components: { components: {
HelloWorld
} }
} }
</script> </script>