mirror of
https://github.com/doocs/md.git
synced 2024-11-24 19:10:34 +08:00
manage data with vux
This commit is contained in:
parent
8b57302416
commit
f0eaa61659
64
README.md
64
README.md
@ -17,16 +17,23 @@
|
|||||||
Markdown 文档自动即时渲染为微信图文,让你不再为微信文章排版而发愁!只要你会基本的 Markdown 语法,就能做出一篇样式简洁而又美观大方的微信图文。
|
Markdown 文档自动即时渲染为微信图文,让你不再为微信文章排版而发愁!只要你会基本的 Markdown 语法,就能做出一篇样式简洁而又美观大方的微信图文。
|
||||||
|
|
||||||
## 在线编辑器地址
|
## 在线编辑器地址
|
||||||
- GitHub Page:https://doocs.github.io/md
|
- Netlify: https://mdmd.netlify.app
|
||||||
- Gitee Page:https://doocs.gitee.io/md
|
- Gitee Pages:https://doocs.gitee.io/md
|
||||||
|
- GitHub Pages:https://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
32
src/api/fetch.js
Normal 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
15
src/api/file.js
Normal 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
|
||||||
|
};
|
@ -4,61 +4,61 @@
|
|||||||
<el-header class="top">
|
<el-header class="top">
|
||||||
<!-- 图片上传 -->
|
<!-- 图片上传 -->
|
||||||
<el-upload action="https://imgkr.com/api/files/upload" :headers="{'Content-Type': 'multipart/form-data'}"
|
<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"
|
:show-file-list="false" :multiple="true" accept=".jpg,.jpeg,.png,.gif" name="file"
|
||||||
:before-upload="beforeUpload" :on-success="uploaded">
|
:before-upload="beforeUpload" :on-success="uploaded">
|
||||||
<el-tooltip class="item" effect="dark" content="上传图片" placement="bottom-start">
|
<el-tooltip class="item" effect="dark" content="上传图片" placement="bottom-start">
|
||||||
<i class="el-icon-upload" size="medium"> </i>
|
<i class="el-icon-upload" size="medium"> </i>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<!-- 下载文本文档 -->
|
<!-- 下载文本文档 -->
|
||||||
<el-tooltip class="item" effect="dark" content="下载编辑框Markdown文档" placement="bottom-start">
|
<el-tooltip class="item" effect="dark" content="下载编辑框Markdown文档" placement="bottom-start">
|
||||||
<i class="el-icon-download" size="medium" @click="downloadEditorContent"> </i>
|
<i class="el-icon-download" size="medium" @click="downloadEditorContent"> </i>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<!-- 页面重置 -->
|
<!-- 页面重置 -->
|
||||||
<el-tooltip class="item" effect="dark" content="重置页面" placement="bottom-start">
|
<el-tooltip class="item" effect="dark" content="重置页面" placement="bottom-start">
|
||||||
<i class="el-icon-refresh" size="medium" @click="reset"> </i>
|
<i class="el-icon-refresh" size="medium" @click="reset"> </i>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<!-- 插入表格 -->
|
<!-- 插入表格 -->
|
||||||
<el-tooltip class="item" effect="dark" content="插入表格" placement="bottom-start">
|
<el-tooltip class="item" effect="dark" content="插入表格" placement="bottom-start">
|
||||||
<i class="el-icon-s-grid" size="medium" @click="dialogFormVisible = true"> </i>
|
<i class="el-icon-s-grid" size="medium" @click="dialogFormVisible = true"> </i>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<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>
|
||||||
</el-option>
|
</el-option>
|
||||||
</el-select>
|
</el-select>
|
||||||
</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>
|
||||||
</el-select>
|
</el-select>
|
||||||
</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>
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-tooltip content="自定义颜色" placement="top">
|
<el-tooltip content="自定义颜色" placement="top">
|
||||||
<el-color-picker v-model="currentColor" size="mini" show-alpha @change="colorChanged"></el-color-picker>
|
<el-color-picker v-model="currentColor" size="mini" show-alpha @change="colorChanged"></el-color-picker>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
|
||||||
<el-tooltip content="微信外链自动转为文末引用" placement="top">
|
<el-tooltip content="微信外链自动转为文末引用" placement="top">
|
||||||
<el-switch v-model="status" active-color="#67c23a" inactive-color="#dcdfe6" @change="statusChanged">
|
<el-switch v-model="status" active-color="#67c23a" inactive-color="#dcdfe6" @change="statusChanged">
|
||||||
</el-switch>
|
</el-switch>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-tooltip class="item" effect="dark" content="自定义CSS样式" placement="left">
|
<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-button type="success" plain size="medium" icon="el-icon-setting" @click="customStyle"></el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-button type="success" plain size="medium" @click="copy">复制</el-button>
|
<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>
|
<el-button type="success" plain size="medium" class="about" @click="aboutDialogVisible = true">关于</el-button>
|
||||||
@ -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,442 +121,351 @@
|
|||||||
</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' },
|
showBox: false,
|
||||||
{ label: '衬线', value: "Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif" }
|
aboutDialogVisible: false,
|
||||||
],
|
dialogFormVisible: false,
|
||||||
sizeOption: [
|
timeout: null,
|
||||||
{ label: '12px', value: '12px', desc: '更小' },
|
source: '',
|
||||||
{ label: '13px', value: '13px', desc: '稍小' },
|
status: '1'
|
||||||
{ label: '14px', value: '14px', desc: '推荐' },
|
}
|
||||||
{ label: '15px', value: '15px', desc: '稍大' },
|
},
|
||||||
{ label: '16px', value: '16px', desc: '更大' }
|
components: {
|
||||||
],
|
EditorHeader
|
||||||
colorOption: [
|
},
|
||||||
{ label: '经典蓝', value: 'rgba(15, 76, 129, 1)', hex: '最新流行' },
|
computed: {
|
||||||
{ label: '翡翠绿', value: 'rgba(0, 152, 116, 1)', hex: '优雅清新' },
|
...mapState({
|
||||||
{ label: '活力橘', value: 'rgba(250, 81, 81, 1)', hex: '热情活泼' }
|
wxRenderer: state=> state.wxRenderer,
|
||||||
],
|
output: state=> state.output,
|
||||||
showBox: false,
|
editor: state=> state.editor,
|
||||||
aboutDialogVisible: false,
|
cssEditor: state=> state.cssEditor,
|
||||||
dialogFormVisible: false,
|
html: state=> state.html,
|
||||||
form: {
|
currentFont: state=> state.currentFont,
|
||||||
rows: 1,
|
currentSize: state=> state.currentSize,
|
||||||
cols: 1
|
currentColor: state=> state.currentColor
|
||||||
},
|
})
|
||||||
timeout: null,
|
},
|
||||||
html: ''
|
created() {
|
||||||
}
|
this.initEditorState()
|
||||||
d.currentFont = d.builtinFonts[0].value
|
this.$nextTick(() => {
|
||||||
d.currentSize = d.sizeOption[2].value
|
this.initEditor()
|
||||||
d.currentColor = d.colorOption[1].value
|
this.initCssEditor()
|
||||||
d.status = '1'
|
this.editorRefresh()
|
||||||
return d
|
})
|
||||||
},
|
},
|
||||||
created () {
|
methods: {
|
||||||
this.currentFont = localStorage.getItem('fonts') || this.builtinFonts[0].value
|
initEditor() {
|
||||||
this.currentColor = localStorage.getItem('color') || this.colorOption[1].value
|
this.initEditorEntity();
|
||||||
this.currentSize = localStorage.getItem('size') || this.sizeOption[2].value
|
this.editor.on('change', (cm, e) => {
|
||||||
this.status = localStorage.getItem('status') === 'true'
|
this.editorRefresh()
|
||||||
this.$nextTick(() => {
|
saveEditorContent(this.editor, '__editor_content')
|
||||||
this.initEditor()
|
});
|
||||||
this.initCssEditor()
|
|
||||||
})
|
|
||||||
this.wxRenderer = new WxRenderer({
|
|
||||||
theme: setColor(this.currentColor),
|
|
||||||
fonts: this.currentFont,
|
|
||||||
size: this.currentSize,
|
|
||||||
status: this.status
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
initEditor() {
|
|
||||||
this.editor = CodeMirror.fromTextArea(
|
|
||||||
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.refresh()
|
|
||||||
this.saveEditorContent(this.editor, '__editor_content')
|
|
||||||
})
|
|
||||||
|
|
||||||
// 粘贴上传图片并插入
|
// 粘贴上传图片并插入
|
||||||
this.editor.on('paste', (cm, e) => {
|
this.editor.on('paste', (cm, e) => {
|
||||||
if (!(e.clipboardData && e.clipboardData.items)) {
|
if (!(e.clipboardData && e.clipboardData.items)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for (let i = 0, len = e.clipboardData.items.length; i < len; ++i) {
|
for (let i = 0, len = e.clipboardData.items.length; i < len; ++i) {
|
||||||
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) {
|
||||||
let data = new FormData()
|
this.$message({
|
||||||
data.append('file', pasteFile)
|
showClose: true,
|
||||||
axios.post(
|
message: checkImageResult,
|
||||||
'https://imgkr.com/api/files/upload',
|
type: 'error'
|
||||||
data,
|
});
|
||||||
{
|
return;
|
||||||
headers: { 'Content-Type': 'multipart/form-data' }
|
}
|
||||||
}
|
let data = new FormData()
|
||||||
).then(resp => {
|
data.append('file', pasteFile)
|
||||||
this.uploaded(resp.data)
|
|
||||||
}).catch(err => {
|
fileApi.fileUpload(data).then(res => {
|
||||||
console.log(err.message)
|
this.uploaded(resp.data)
|
||||||
|
}).catch(err => {
|
||||||
|
console.log(err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
initCssEditor() {
|
||||||
|
this.initCssEditorEntity();
|
||||||
|
// 自动提示
|
||||||
|
this.cssEditor.on('keyup', (cm, e) => {
|
||||||
|
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
|
||||||
|
cm.showHint(e)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.cssEditor.on('update', (instance) => {
|
||||||
|
this.cssChanged()
|
||||||
|
saveEditorContent(this.cssEditor, '__css_content')
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
}
|
// 图片上传前的处理
|
||||||
})
|
beforeUpload(file) {
|
||||||
|
const checkImageResult = checkImage(file);
|
||||||
|
|
||||||
// 如果有编辑器内容被保存则读取,否则加载默认内容
|
if (checkImageResult) {
|
||||||
this.loadLocalStorage(this.editor, '__editor_content', DEFAULT_CONTENT)
|
this.$message({
|
||||||
},
|
showClose: true,
|
||||||
initCssEditor() {
|
message: checkImageResult,
|
||||||
this.cssEditor = CodeMirror.fromTextArea(
|
type: 'error'
|
||||||
document.getElementById('cssEditor'), {
|
});
|
||||||
value: '',
|
return false;
|
||||||
mode: 'css',
|
}
|
||||||
theme: 'style-mirror',
|
return true;
|
||||||
lineNumbers: false,
|
},
|
||||||
lineWrapping: true,
|
cssChanged() {
|
||||||
matchBrackets: true,
|
let json = css2json(this.cssEditor.getValue(0))
|
||||||
autofocus: true,
|
let theme = setFontSize(this.currentSize.replace('px', ''))
|
||||||
extraKeys: {
|
theme = customCssWithTemplate(json, this.currentColor, theme)
|
||||||
'Ctrl-F': function autoFormat (editor) {
|
this.setWxRendererOptions({
|
||||||
const totalLines = editor.lineCount()
|
theme: theme
|
||||||
editor.autoFormatRange({ line: 0, ch: 0 }, { line: totalLines })
|
});
|
||||||
|
this.editorRefresh()
|
||||||
|
},
|
||||||
|
// 图片上传结束
|
||||||
|
uploaded(response, file, fileList) {
|
||||||
|
if (response.success) {
|
||||||
|
// 上传成功,获取光标
|
||||||
|
const cursor = this.editor.getCursor()
|
||||||
|
const imageUrl = response.data
|
||||||
|
const markdownImage = `![](${imageUrl})`
|
||||||
|
// 将 Markdown 形式的 URL 插入编辑框光标所在位置
|
||||||
|
this.editor.replaceSelection(`\n${markdownImage}\n`, cursor)
|
||||||
|
this.$message({
|
||||||
|
showClose: true,
|
||||||
|
message: '图片插入成功',
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
this.editorRefresh()
|
||||||
|
} else {
|
||||||
|
// 上传失败
|
||||||
|
this.$message({
|
||||||
|
showClose: true,
|
||||||
|
message: response.message,
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 插入表格
|
||||||
|
insertTable() {
|
||||||
|
const cursor = this.editor.getCursor()
|
||||||
|
const rows = parseInt(this.config.form.rows)
|
||||||
|
const cols = parseInt(this.config.form.cols)
|
||||||
|
if (isNaN(rows) || isNaN(cols) || rows < 1 || cols < 1) {
|
||||||
|
this.$message({
|
||||||
|
showClose: true,
|
||||||
|
message: '输入的行/列数无效,请重新输入',
|
||||||
|
type: 'error'
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
// 自动提示
|
|
||||||
this.cssEditor.on('keyup', (cm, e) => {
|
|
||||||
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
|
|
||||||
cm.showHint(e)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
this.cssEditor.on('update', (instance) => {
|
|
||||||
this.cssChanged()
|
|
||||||
this.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) {
|
|
||||||
return this.checkType(file) && this.checkImageSize(file)
|
|
||||||
},
|
|
||||||
// 检查文件类型
|
|
||||||
checkType (file) {
|
|
||||||
if (!/\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name)) {
|
|
||||||
this.$message({
|
|
||||||
showClose: true,
|
|
||||||
message: '请上传 JPG/PNG/GIF 格式的图片',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
// 检查图片大小
|
|
||||||
checkImageSize (file) {
|
|
||||||
if (file.size > 5 * 1024 * 1024) {
|
|
||||||
this.$message({
|
|
||||||
showClose: true,
|
|
||||||
message: '由于公众号限制,图片大小不能超过 5.0M',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
// 图片上传结束
|
|
||||||
uploaded (response, file, fileList) {
|
|
||||||
if (response.success) {
|
|
||||||
// 上传成功,获取光标
|
|
||||||
const cursor = this.editor.getCursor()
|
|
||||||
const imageUrl = response.data
|
|
||||||
const markdownImage = `![](${imageUrl})`
|
|
||||||
// 将 Markdown 形式的 URL 插入编辑框光标所在位置
|
|
||||||
this.editor.replaceSelection(`\n${markdownImage}\n`, cursor)
|
|
||||||
this.$message({
|
|
||||||
showClose: true,
|
|
||||||
message: '图片插入成功',
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
this.refresh()
|
|
||||||
} else {
|
|
||||||
// 上传失败
|
|
||||||
this.$message({
|
|
||||||
showClose: true,
|
|
||||||
message: response.message,
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 刷新右侧预览
|
|
||||||
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 () {
|
|
||||||
const cursor = this.editor.getCursor()
|
|
||||||
const rows = parseInt(this.form.rows)
|
|
||||||
const cols = parseInt(this.form.cols)
|
|
||||||
if (isNaN(rows) || isNaN(cols) || rows < 1 || cols < 1) {
|
|
||||||
this.$message({
|
|
||||||
showClose: true,
|
|
||||||
message: '输入的行/列数无效,请重新输入',
|
|
||||||
type: 'error'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let table = ''
|
let table = ''
|
||||||
for (let i = 0; i < rows + 2; ++i) {
|
for (let i = 0; i < rows + 2; ++i) {
|
||||||
for (let j = 0; j < cols + 1; ++j) {
|
for (let j = 0; j < cols + 1; ++j) {
|
||||||
table += (j === 0 ? '|' : (i !== 1 ? ' |' : ' --- |'))
|
table += (j === 0 ? '|' : (i !== 1 ? ' |' : ' --- |'))
|
||||||
}
|
}
|
||||||
table += '\n'
|
table += '\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
copy12() {
|
||||||
this.refresh()
|
let clipboardDiv = document.getElementById('output')
|
||||||
},
|
|
||||||
// 将编辑器内容保存到 LocalStorage
|
clipboardDiv.focus()
|
||||||
saveEditorContent (editor, name) {
|
window.getSelection().removeAllRanges()
|
||||||
const content = editor.getValue(0)
|
let range = document.createRange()
|
||||||
if (content) {
|
|
||||||
localStorage.setItem(name, content)
|
range.setStartBefore(clipboardDiv.firstChild)
|
||||||
} else {
|
range.setEndAfter(clipboardDiv.lastChild)
|
||||||
localStorage.removeItem(name)
|
window.getSelection().addRange(range)
|
||||||
}
|
document.execCommand('copy')
|
||||||
},
|
|
||||||
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 () {
|
|
||||||
let clipboardDiv = document.getElementById('output')
|
|
||||||
clipboardDiv.focus()
|
|
||||||
window.getSelection().removeAllRanges()
|
|
||||||
let range = document.createRange()
|
|
||||||
range.setStartBefore(clipboardDiv.firstChild)
|
|
||||||
range.setEndAfter(clipboardDiv.lastChild)
|
|
||||||
window.getSelection().addRange(range)
|
|
||||||
document.execCommand('copy')
|
|
||||||
// 输出提示
|
|
||||||
this.$notify({
|
|
||||||
showClose: true,
|
|
||||||
message: '已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴',
|
|
||||||
offset: 80,
|
|
||||||
duration: 1600,
|
|
||||||
type: 'success'
|
|
||||||
})
|
|
||||||
},
|
|
||||||
// 复制到微信公众号
|
|
||||||
copy() {
|
|
||||||
let clipboardDiv = document.getElementById('output')
|
|
||||||
const clipboardHTML = clipboardDiv.innerHTML
|
|
||||||
// solveWeChatImage()
|
|
||||||
this.html = solveHtml();
|
|
||||||
// 输出提示
|
// 输出提示
|
||||||
this.$notify({
|
this.$notify({
|
||||||
showClose: true,
|
showClose: true,
|
||||||
message: '已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴',
|
message: '已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴',
|
||||||
offset: 80,
|
offset: 80,
|
||||||
duration: 1600,
|
duration: 1600,
|
||||||
type: 'success'
|
type: 'success'
|
||||||
})
|
})
|
||||||
clipboardDiv.innerHTML = clipboardHTML; // 恢复现场
|
},
|
||||||
|
// 复制到微信公众号
|
||||||
|
copy() {
|
||||||
|
let clipboardDiv = document.getElementById('output')
|
||||||
|
const clipboardHTML = clipboardDiv.innerHTML
|
||||||
|
// solveWeChatImage()
|
||||||
|
this.html = solveHtml();
|
||||||
|
// 输出提示
|
||||||
|
this.$notify({
|
||||||
|
showClose: true,
|
||||||
|
message: '已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴',
|
||||||
|
offset: 80,
|
||||||
|
duration: 1600,
|
||||||
|
type: 'success'
|
||||||
|
})
|
||||||
|
clipboardDiv.innerHTML = clipboardHTML; // 恢复现场
|
||||||
|
},
|
||||||
|
// 左右栏同步滚动
|
||||||
|
leftAndRightScroll() {
|
||||||
|
$('div.CodeMirror-scroll, #preview').on('scroll', function callback() {
|
||||||
|
clearTimeout(this.timeout)
|
||||||
|
|
||||||
|
let source = $(this)
|
||||||
|
let target = $(source.is('#preview') ? 'div.CodeMirror-scroll' : '#preview')
|
||||||
|
|
||||||
|
target.off('scroll')
|
||||||
|
|
||||||
|
let source0 = source[0]
|
||||||
|
let target0 = target[0]
|
||||||
|
|
||||||
|
let percentage = source0.scrollTop / (source0.scrollHeight - source0.offsetHeight)
|
||||||
|
let height = percentage * (target0.scrollHeight - target0.offsetHeight)
|
||||||
|
target0.scrollTo(0, height)
|
||||||
|
|
||||||
|
this.timeout = setTimeout(() => {
|
||||||
|
target.on('scroll', callback)
|
||||||
|
}, 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)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
...mapMutations(['initEditorState', 'initEditorEntity', 'editorRefresh', 'clearEditorToDefault',
|
||||||
|
'setCurrentFont', 'setCurrentSize', 'setCurrentColor', 'setEditorValue', 'setCssEditorValue',
|
||||||
|
'initCssEditorEntity', 'setWxRendererOptions'])
|
||||||
},
|
},
|
||||||
// 左右栏同步滚动
|
mounted() {
|
||||||
leftAndRightScroll() {
|
this.leftAndRightScroll()
|
||||||
$('div.CodeMirror-scroll, #preview').on('scroll', function callback () {
|
|
||||||
clearTimeout(this.timeout)
|
|
||||||
|
|
||||||
let source = $(this)
|
|
||||||
let target = $(source.is('#preview') ? 'div.CodeMirror-scroll' : '#preview')
|
|
||||||
|
|
||||||
target.off('scroll')
|
|
||||||
|
|
||||||
let source0 = source[0]
|
|
||||||
let target0 = target[0]
|
|
||||||
|
|
||||||
let percentage = source0.scrollTop / (source0.scrollHeight - source0.offsetHeight)
|
|
||||||
let height = percentage * (target0.scrollHeight - target0.offsetHeight)
|
|
||||||
target0.scrollTo(0, height)
|
|
||||||
|
|
||||||
this.timeout = setTimeout(() => {
|
|
||||||
target.on('scroll', callback)
|
|
||||||
}, 100)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.leftAndRightScroll()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
</style>
|
|
77
src/components/codeMirror/header.vue
Normal file
77
src/components/codeMirror/header.vue
Normal 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"> </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"> </i>
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- 页面重置 -->
|
||||||
|
<el-tooltip class="item" effect="dark" content="重置页面" placement="bottom-start">
|
||||||
|
<i class="el-icon-refresh" size="medium" @click="reset"> </i>
|
||||||
|
</el-tooltip>
|
||||||
|
<!-- 插入表格 -->
|
||||||
|
<el-tooltip class="item" effect="dark" content="插入表格" placement="bottom-start">
|
||||||
|
<i class="el-icon-s-grid" size="medium" @click="dialogFormVisible = true"> </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>
|
||||||
|
|
||||||
|
<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
60
src/scripts/config.js
Normal 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
|
||||||
|
}
|
||||||
|
};
|
@ -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;
|
||||||
|
}
|
@ -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)
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
wxRenderer: null,
|
||||||
|
output: '',
|
||||||
|
editor: null,
|
||||||
|
cssEditor: null,
|
||||||
|
html: '',
|
||||||
|
currentFont: {},
|
||||||
|
currentSize: {},
|
||||||
|
currentColor: {}
|
||||||
|
};
|
||||||
|
const mutations = {
|
||||||
|
setEditorValue(state, data) {
|
||||||
|
state.editor.setValue(data)
|
||||||
|
},
|
||||||
|
setCssEditorValue(state, data) {
|
||||||
|
state.cssEditor.setValue(data)
|
||||||
|
},
|
||||||
|
setWxRendererOptions(state, data) {
|
||||||
|
state.wxRenderer.setOptions(data);
|
||||||
|
},
|
||||||
|
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({
|
export default new Vuex.Store({
|
||||||
state: {
|
state,
|
||||||
},
|
mutations,
|
||||||
mutations: {
|
actions: {},
|
||||||
},
|
modules: {}
|
||||||
actions: {
|
|
||||||
},
|
|
||||||
modules: {
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
@ -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>
|
||||||
|
Loading…
Reference in New Issue
Block a user