mirror of
https://github.com/doocs/md.git
synced 2025-01-24 21:05:15 +08:00
482 lines
16 KiB
Vue
482 lines
16 KiB
Vue
<template>
|
||
<div class="container" :class="{ container_night: nightMode }">
|
||
<el-container>
|
||
<el-header class="editor__header">
|
||
<editor-header
|
||
ref="header"
|
||
@refresh="onEditorRefresh"
|
||
@cssChanged="cssChanged"
|
||
@downLoad="downloadEditorContent"
|
||
@showCssEditor="showCssEditor = !showCssEditor"
|
||
@showAboutDialog="aboutDialogVisible = true"
|
||
@showDialogForm="dialogFormVisible = true"
|
||
@showDialogUploadImg="dialogUploadImgVisible = true"
|
||
@startCopy="(isCoping = true), (backLight = true)"
|
||
@endCopy="endCopy"
|
||
/>
|
||
</el-header>
|
||
<el-main class="main-body">
|
||
<el-row class="main-section">
|
||
<el-col
|
||
:span="12"
|
||
@contextmenu.prevent.native="openMenu($event)"
|
||
>
|
||
<textarea
|
||
id="editor"
|
||
type="textarea"
|
||
placeholder="Your markdown text here."
|
||
v-model="source"
|
||
>
|
||
</textarea>
|
||
</el-col>
|
||
<el-col
|
||
:span="12"
|
||
class="preview-wrapper"
|
||
id="preview"
|
||
ref="preview"
|
||
:class="{
|
||
'preview-wrapper_night': nightMode && isCoping,
|
||
}"
|
||
>
|
||
<section
|
||
id="output-wrapper"
|
||
:class="{ output_night: nightMode && !backLight }"
|
||
>
|
||
<div class="preview">
|
||
<section id="output" v-html="output"></section>
|
||
<div
|
||
class="loading-mask"
|
||
v-if="nightMode && isCoping"
|
||
>
|
||
<div class="loading__img"></div>
|
||
<span>正在生成</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</el-col>
|
||
<transition
|
||
name="custom-classes-transition"
|
||
enter-active-class="bounceInRight"
|
||
>
|
||
<el-col id="cssBox" :span="12" v-show="showCssEditor">
|
||
<textarea
|
||
id="cssEditor"
|
||
type="textarea"
|
||
placeholder="Your custom css here."
|
||
>
|
||
</textarea>
|
||
</el-col>
|
||
</transition>
|
||
</el-row>
|
||
</el-main>
|
||
</el-container>
|
||
<upload-img-dialog
|
||
v-model="dialogUploadImgVisible"
|
||
@close="dialogUploadImgVisible = false"
|
||
@uploaded="uploaded"
|
||
/>
|
||
<about-dialog v-model="aboutDialogVisible" />
|
||
<insert-form-dialog v-model="dialogFormVisible" />
|
||
<right-click-menu
|
||
v-model="rightClickMenuVisible"
|
||
:left="mouseLeft"
|
||
:top="mouseTop"
|
||
@menuTick="onMenuEvent"
|
||
@closeMenu="closeRightClickMenu"
|
||
/>
|
||
</div>
|
||
</template>
|
||
<script>
|
||
import editorHeader from "../components/CodemirrorEditor/header";
|
||
import aboutDialog from "../components/CodemirrorEditor/aboutDialog";
|
||
import insertFormDialog from "../components/CodemirrorEditor/insertForm";
|
||
import rightClickMenu from "../components/CodemirrorEditor/rightClickMenu";
|
||
import uploadImgDialog from "../components/CodemirrorEditor/uploadImgDialog";
|
||
|
||
import {
|
||
css2json,
|
||
downLoadMD,
|
||
setFontSize,
|
||
saveEditorContent,
|
||
customCssWithTemplate,
|
||
} from "../assets/scripts/util";
|
||
import { uploadImgFile } from "../assets/scripts/uploadImageFile";
|
||
|
||
require("codemirror/mode/javascript/javascript");
|
||
import { mapState, mapMutations } from "vuex";
|
||
export default {
|
||
data() {
|
||
return {
|
||
showCssEditor: false,
|
||
aboutDialogVisible: false,
|
||
dialogUploadImgVisible: false,
|
||
dialogFormVisible: false,
|
||
isCoping: false,
|
||
isImgLoading: false,
|
||
backLight: false,
|
||
timeout: null,
|
||
changeTimer: null,
|
||
source: "",
|
||
mouseLeft: 0,
|
||
mouseTop: 0,
|
||
};
|
||
},
|
||
components: {
|
||
editorHeader,
|
||
aboutDialog,
|
||
insertFormDialog,
|
||
rightClickMenu,
|
||
uploadImgDialog,
|
||
},
|
||
computed: {
|
||
...mapState({
|
||
wxRenderer: (state) => state.wxRenderer,
|
||
output: (state) => state.output,
|
||
editor: (state) => state.editor,
|
||
cssEditor: (state) => state.cssEditor,
|
||
currentSize: (state) => state.currentSize,
|
||
currentColor: (state) => state.currentColor,
|
||
nightMode: (state) => state.nightMode,
|
||
rightClickMenuVisible: (state) => state.rightClickMenuVisible,
|
||
}),
|
||
},
|
||
created() {
|
||
this.initEditorState();
|
||
this.$nextTick(() => {
|
||
this.initEditor();
|
||
this.initCssEditor();
|
||
this.onEditorRefresh();
|
||
});
|
||
},
|
||
methods: {
|
||
initEditor() {
|
||
this.initEditorEntity();
|
||
this.editor.on("change", (cm, e) => {
|
||
if (this.changeTimer) clearTimeout(this.changeTimer);
|
||
this.changeTimer = setTimeout(() => {
|
||
this.onEditorRefresh();
|
||
saveEditorContent(this.editor, "__editor_content");
|
||
}, 300);
|
||
});
|
||
|
||
// 粘贴上传图片并插入
|
||
this.editor.on("paste", (cm, e) => {
|
||
if (
|
||
!(e.clipboardData && e.clipboardData.items) ||
|
||
this.isImgLoading
|
||
) {
|
||
return;
|
||
}
|
||
for (
|
||
let i = 0, len = e.clipboardData.items.length;
|
||
i < len;
|
||
++i
|
||
) {
|
||
let item = e.clipboardData.items[i];
|
||
|
||
if (item.kind === "file") {
|
||
// 校验图床参数
|
||
const imgHost =
|
||
localStorage.getItem("imgHost") || "default";
|
||
if (
|
||
imgHost != "default" &&
|
||
!localStorage.getItem(`${imgHost}Config`)
|
||
) {
|
||
this.$message({
|
||
showClose: true,
|
||
message: "请先配置好图床参数",
|
||
type: "error",
|
||
});
|
||
continue;
|
||
}
|
||
|
||
this.isImgLoading = true;
|
||
const pasteFile = item.getAsFile();
|
||
uploadImgFile(pasteFile)
|
||
.then((res) => {
|
||
this.uploaded(res);
|
||
})
|
||
.catch((err) => {
|
||
this.$message({
|
||
showClose: true,
|
||
message: err,
|
||
type: "error",
|
||
});
|
||
});
|
||
this.isImgLoading = false;
|
||
}
|
||
}
|
||
});
|
||
|
||
this.editor.on("mousedown", () => {
|
||
this.$store.commit("setRightClickMenuVisible", false);
|
||
});
|
||
this.editor.on("blur", () => {
|
||
//!影响到右键菜单的点击事件,右键菜单的点击事件在组件内通过mousedown触发
|
||
this.$store.commit("setRightClickMenuVisible", false);
|
||
});
|
||
this.editor.on("scroll", () => {
|
||
this.$store.commit("setRightClickMenuVisible", false);
|
||
});
|
||
},
|
||
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");
|
||
});
|
||
},
|
||
cssChanged() {
|
||
let json = css2json(this.cssEditor.getValue(0));
|
||
let theme = setFontSize(this.currentSize.replace("px", ""));
|
||
|
||
theme = customCssWithTemplate(json, this.currentColor, theme);
|
||
this.setWxRendererOptions({
|
||
theme: theme,
|
||
});
|
||
this.onEditorRefresh();
|
||
},
|
||
// 图片上传结束
|
||
uploaded(response) {
|
||
if (!response) {
|
||
this.$message({
|
||
showClose: true,
|
||
message: "上传图片未知异常",
|
||
type: "error",
|
||
});
|
||
return;
|
||
}
|
||
this.dialogUploadImgVisible = false;
|
||
// 上传成功,获取光标
|
||
const cursor = this.editor.getCursor();
|
||
const imageUrl = response;
|
||
const markdownImage = `![](${imageUrl})`;
|
||
// 将 Markdown 形式的 URL 插入编辑框光标所在位置
|
||
this.editor.replaceSelection(`\n${markdownImage}\n`, cursor);
|
||
this.$message({
|
||
showClose: true,
|
||
message: "图片上传成功",
|
||
type: "success",
|
||
});
|
||
this.onEditorRefresh();
|
||
},
|
||
// 左右滚动
|
||
leftAndRightScroll() {
|
||
const scrollCB = (text) => {
|
||
let source, target;
|
||
|
||
clearTimeout(this.timeout);
|
||
if (text === "preview") {
|
||
source = this.$refs.preview.$el;
|
||
target = document.getElementsByClassName(
|
||
"CodeMirror-scroll"
|
||
)[0];
|
||
this.editor.off("scroll", editorScrollCB);
|
||
this.timeout = setTimeout(() => {
|
||
this.editor.on("scroll", editorScrollCB);
|
||
}, 300);
|
||
} else if (text === "editor") {
|
||
source = document.getElementsByClassName(
|
||
"CodeMirror-scroll"
|
||
)[0];
|
||
target = this.$refs.preview.$el;
|
||
target.removeEventListener(
|
||
"scroll",
|
||
previewScrollCB,
|
||
false
|
||
);
|
||
this.timeout = setTimeout(() => {
|
||
target.addEventListener(
|
||
"scroll",
|
||
previewScrollCB,
|
||
false
|
||
);
|
||
}, 300);
|
||
}
|
||
|
||
let percentage =
|
||
source.scrollTop /
|
||
(source.scrollHeight - source.offsetHeight);
|
||
let height =
|
||
percentage * (target.scrollHeight - target.offsetHeight);
|
||
|
||
target.scrollTo(0, height);
|
||
};
|
||
const editorScrollCB = () => {
|
||
scrollCB("editor");
|
||
};
|
||
const previewScrollCB = () => {
|
||
scrollCB("preview");
|
||
};
|
||
|
||
this.$refs.preview.$el.addEventListener(
|
||
"scroll",
|
||
previewScrollCB,
|
||
false
|
||
);
|
||
this.editor.on("scroll", editorScrollCB);
|
||
},
|
||
// 更新编辑器
|
||
onEditorRefresh() {
|
||
this.editorRefresh();
|
||
setTimeout(() => PR.prettyPrint(), 0);
|
||
},
|
||
// 复制结束
|
||
endCopy() {
|
||
this.backLight = false;
|
||
setTimeout(() => {
|
||
this.isCoping = false;
|
||
}, 800);
|
||
},
|
||
// 下载编辑器内容到本地
|
||
downloadEditorContent() {
|
||
downLoadMD(this.editor.getValue(0));
|
||
},
|
||
// 右键菜单
|
||
openMenu(e) {
|
||
const menuMinWidth = 105;
|
||
const offsetLeft = this.$el.getBoundingClientRect().left;
|
||
const offsetWidth = this.$el.offsetWidth;
|
||
const maxLeft = offsetWidth - menuMinWidth;
|
||
const left = e.clientX - offsetLeft;
|
||
this.mouseLeft = Math.min(maxLeft, left);
|
||
this.mouseTop = e.clientY + 10;
|
||
this.$store.commit("setRightClickMenuVisible", true);
|
||
},
|
||
closeRightClickMenu() {
|
||
this.$store.commit("setRightClickMenuVisible", false);
|
||
},
|
||
onMenuEvent(type, info = {}) {
|
||
switch (type) {
|
||
case "pageReset":
|
||
this.$refs.header.showResetConfirm = true;
|
||
break;
|
||
case "insertPic":
|
||
this.dialogUploadImgVisible = true;
|
||
break;
|
||
case "downLoad":
|
||
this.downloadEditorContent();
|
||
break;
|
||
case "insertTable":
|
||
this.dialogFormVisible = true;
|
||
default:
|
||
break;
|
||
}
|
||
},
|
||
...mapMutations([
|
||
"initEditorState",
|
||
"initEditorEntity",
|
||
"setWxRendererOptions",
|
||
"editorRefresh",
|
||
"initCssEditorEntity",
|
||
]),
|
||
},
|
||
mounted() {
|
||
setTimeout(() => {
|
||
this.leftAndRightScroll();
|
||
PR.prettyPrint();
|
||
}, 300);
|
||
},
|
||
};
|
||
</script>
|
||
<style lang="less" scoped>
|
||
.main-body {
|
||
padding-top: 12px;
|
||
overflow: hidden;
|
||
}
|
||
.el-main {
|
||
transition: all 0.3s;
|
||
padding: 0;
|
||
margin: 20px;
|
||
margin-top: 0;
|
||
}
|
||
.container {
|
||
transition: all 0.3s;
|
||
}
|
||
.preview {
|
||
transition: background 0s;
|
||
transition-delay: 0.2s;
|
||
}
|
||
.preview-wrapper_night {
|
||
overflow-y: inherit;
|
||
position: relative;
|
||
left: -3px;
|
||
.preview {
|
||
background-color: #fff;
|
||
}
|
||
}
|
||
#output-wrapper {
|
||
position: relative;
|
||
}
|
||
.loading-mask {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 376px;
|
||
height: 101%;
|
||
padding-top: 1px;
|
||
font-size: 15px;
|
||
color: gray;
|
||
background-color: #1e1e1e;
|
||
.loading__img {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 330px;
|
||
width: 50px;
|
||
height: 50px;
|
||
transform: translate(-50%, -50%);
|
||
background: url("../assets/images/favicon.png") no-repeat;
|
||
background-size: cover;
|
||
}
|
||
span {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 390px;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
}
|
||
.bounceInRight {
|
||
animation-name: bounceInRight;
|
||
animation-duration: 1s;
|
||
animation-fill-mode: both;
|
||
}
|
||
@keyframes bounceInRight {
|
||
0%,
|
||
60%,
|
||
75%,
|
||
90%,
|
||
100% {
|
||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||
}
|
||
0% {
|
||
opacity: 0;
|
||
transform: translate3d(3000px, 0, 0);
|
||
}
|
||
60% {
|
||
opacity: 1;
|
||
transform: translate3d(-25px, 0, 0);
|
||
}
|
||
75% {
|
||
transform: translate3d(10px, 0, 0);
|
||
}
|
||
90% {
|
||
transform: translate3d(-5px, 0, 0);
|
||
}
|
||
100% {
|
||
transform: none;
|
||
}
|
||
}
|
||
</style>
|
||
<style lang="less">
|
||
@import url("../assets/less/app.less");
|
||
@import url("../assets/less/style-mirror.css");
|
||
@import url("../assets/less/github-v2.min.css");
|
||
</style>
|