refactor: use uni-app framework (#55)

* update framework to uni-app

* fix: bug fix

* fix: bug fix

* 修改输出路径

* feat: publicPath

* feat: manifest update

* fix: cssEditor theme

* fix: style

* style: format code with prettier

* fix: table style & copy style on the night mode

* fix: upload image

* fix: style

Co-authored-by: yanglbme <szuyanglb@outlook.com>
This commit is contained in:
JimQing 2021-02-28 14:50:52 +08:00 committed by GitHub
parent 432db15576
commit b50ae32834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 28044 additions and 16714 deletions

View File

View File

@ -1,2 +0,0 @@
> 1%
last 2 versions

View File

@ -1,5 +1,18 @@
[*.{js,jsx,ts,tsx,vue}]
# https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 80
trim_trailing_whitespace = true
[*.md]
max_line_length = 0
trim_trailing_whitespace = false
[COMMIT_EDITMSG]
max_line_length = 0

View File

@ -1,3 +1,60 @@
const plugins = [];
if (process.env.UNI_OPT_TREESHAKINGNG) {
plugins.push(
require("@dcloudio/vue-cli-plugin-uni-optimize/packages/babel-plugin-uni-api/index.js")
);
}
if (
(process.env.UNI_PLATFORM === "app-plus" && process.env.UNI_USING_V8) ||
(process.env.UNI_PLATFORM === "h5" &&
process.env.UNI_H5_BROWSER === "builtin")
) {
const path = require("path");
const isWin = /^win/.test(process.platform);
const normalizePath = (path) => (isWin ? path.replace(/\\/g, "/") : path);
const input = normalizePath(process.env.UNI_INPUT_DIR);
try {
plugins.push([
require("@dcloudio/vue-cli-plugin-hbuilderx/packages/babel-plugin-console"),
{
file(file) {
file = normalizePath(file);
if (file.indexOf(input) === 0) {
return path.relative(input, file);
}
return false;
},
},
]);
} catch (e) {}
}
process.UNI_LIBRARIES = process.UNI_LIBRARIES || ["@dcloudio/uni-ui"];
process.UNI_LIBRARIES.forEach((libraryName) => {
plugins.push([
"import",
{
libraryName: libraryName,
customName: (name) => {
return `${libraryName}/lib/${name}/${name}`;
},
},
]);
});
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
presets: [
[
"@vue/app",
{
modules: "commonjs",
useBuiltIns: process.env.UNI_PLATFORM === "h5" ? "usage" : "entry",
},
],
],
plugins,
};

View File

@ -1,3 +0,0 @@
module.exports = {
preset: "@vue/cli-plugin-unit-jest",
};

27644
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +1,112 @@
{
"name": "vue-md",
"version": "1.4.7",
"homepage": ".",
"description": "An open-source wechat markdown editor.",
"author": "doocs",
"name": "md",
"version": "1.5.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
"serve": "npm run dev:h5",
"build": "npm run build:h5",
"build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
"build:custom": "cross-env NODE_ENV=production uniapp-cli custom",
"build:h5": "cross-env NODE_ENV=production UNI_OUTPUT_DIR=dist UNI_PLATFORM=h5 vue-cli-service uni-build",
"build:mp-360": "cross-env NODE_ENV=production UNI_PLATFORM=mp-360 vue-cli-service uni-build",
"build:mp-alipay": "cross-env NODE_ENV=production UNI_PLATFORM=mp-alipay vue-cli-service uni-build",
"build:mp-baidu": "cross-env NODE_ENV=production UNI_PLATFORM=mp-baidu vue-cli-service uni-build",
"build:mp-kuaishou": "cross-env NODE_ENV=production UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build",
"build:mp-qq": "cross-env NODE_ENV=production UNI_PLATFORM=mp-qq vue-cli-service uni-build",
"build:mp-toutiao": "cross-env NODE_ENV=production UNI_PLATFORM=mp-toutiao vue-cli-service uni-build",
"build:mp-weixin": "cross-env NODE_ENV=production UNI_PLATFORM=mp-weixin vue-cli-service uni-build",
"build:quickapp-native": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-native vue-cli-service uni-build",
"build:quickapp-webview": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview vue-cli-service uni-build",
"build:quickapp-webview-huawei": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview-huawei vue-cli-service uni-build",
"build:quickapp-webview-union": "cross-env NODE_ENV=production UNI_PLATFORM=quickapp-webview-union vue-cli-service uni-build",
"dev:app-plus": "cross-env NODE_ENV=development UNI_PLATFORM=app-plus vue-cli-service uni-build --watch",
"dev:custom": "cross-env NODE_ENV=development uniapp-cli custom",
"dev:h5": "cross-env NODE_ENV=development UNI_OUTPUT_DIR=dist UNI_PLATFORM=h5 vue-cli-service uni-serve",
"dev:mp-360": "cross-env NODE_ENV=development UNI_PLATFORM=mp-360 vue-cli-service uni-build --watch",
"dev:mp-alipay": "cross-env NODE_ENV=development UNI_PLATFORM=mp-alipay vue-cli-service uni-build --watch",
"dev:mp-baidu": "cross-env NODE_ENV=development UNI_PLATFORM=mp-baidu vue-cli-service uni-build --watch",
"dev:mp-kuaishou": "cross-env NODE_ENV=development UNI_PLATFORM=mp-kuaishou vue-cli-service uni-build --watch",
"dev:mp-qq": "cross-env NODE_ENV=development UNI_PLATFORM=mp-qq vue-cli-service uni-build --watch",
"dev:mp-toutiao": "cross-env NODE_ENV=development UNI_PLATFORM=mp-toutiao vue-cli-service uni-build --watch",
"dev:mp-weixin": "cross-env NODE_ENV=development UNI_PLATFORM=mp-weixin vue-cli-service uni-build --watch",
"dev:quickapp-native": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-native vue-cli-service uni-build --watch",
"dev:quickapp-webview": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview vue-cli-service uni-build --watch",
"dev:quickapp-webview-huawei": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview-huawei vue-cli-service uni-build --watch",
"dev:quickapp-webview-union": "cross-env NODE_ENV=development UNI_PLATFORM=quickapp-webview-union vue-cli-service uni-build --watch",
"info": "node node_modules/@dcloudio/vue-cli-plugin-uni/commands/info.js",
"serve:quickapp-native": "node node_modules/@dcloudio/uni-quickapp-native/bin/serve.js",
"test:android": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=android jest -i",
"test:h5": "cross-env UNI_PLATFORM=h5 jest -i",
"test:ios": "cross-env UNI_PLATFORM=app-plus UNI_OS_NAME=ios jest -i",
"test:mp-baidu": "cross-env UNI_PLATFORM=mp-baidu jest -i",
"test:mp-weixin": "cross-env UNI_PLATFORM=mp-weixin jest -i"
},
"dependencies": {
"@dcloudio/uni-app-plus": "^2.0.0-30720210122002",
"@dcloudio/uni-h5": "^2.0.0-30720210122002",
"@dcloudio/uni-helper-json": "*",
"@dcloudio/uni-mp-360": "^2.0.0-30720210122002",
"@dcloudio/uni-mp-alipay": "^2.0.0-30720210122002",
"@dcloudio/uni-mp-baidu": "^2.0.0-30720210122002",
"@dcloudio/uni-mp-qq": "^2.0.0-30720210122002",
"@dcloudio/uni-mp-toutiao": "^2.0.0-30720210122002",
"@dcloudio/uni-mp-vue": "^2.0.0-30720210122002",
"@dcloudio/uni-mp-weixin": "^2.0.0-30720210122002",
"@dcloudio/uni-quickapp-native": "^2.0.0-30720210122002",
"@dcloudio/uni-quickapp-webview": "^2.0.0-30720210122002",
"@dcloudio/uni-stat": "^2.0.0-30720210122002",
"@vue/shared": "^3.0.0",
"ali-oss": "^6.11.2",
"axios": "^0.21.0",
"buffer-from": "^1.1.1",
"codemirror": "^5.58.3",
"core-js": "^3.7.0",
"core-js": "^3.8.3",
"cos-js-sdk-v5": "^1.1.0",
"crypto-js": "^4.0.0",
"element-ui": "^2.14.1",
"flyio": "^0.6.2",
"jquery": "^3.5.1",
"juice": "^7.0.0",
"less": "^3.12.2",
"marked": "^2.0.0",
"prettier": "^2.2.0",
"prettify": "^0.1.7",
"qiniu-js": "^3.1.2",
"regenerator-runtime": "^0.12.1",
"uuid": "^8.3.1",
"vue": "^2.6.12",
"vue-router": "^3.4.9",
"vuex": "^3.5.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.5.9",
"@vue/cli-plugin-eslint": "^4.5.9",
"@vue/cli-plugin-unit-jest": "^4.5.9",
"@vue/cli-service": "^4.5.9",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/test-utils": "1.1.1",
"babel-eslint": "^10.1.0",
"eslint": "^7.14.0",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^5.0.0",
"eslint-plugin-vue": "^7.1.0",
"less-loader": "^7.1.0",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.0",
"vue-template-compiler": "^2.6.12"
"@dcloudio/types": "*",
"@dcloudio/uni-automator": "^2.0.0-30720210122002",
"@dcloudio/uni-cli-shared": "^2.0.0-30720210122002",
"@dcloudio/uni-migration": "^2.0.0-30720210122002",
"@dcloudio/uni-template-compiler": "^2.0.0-30720210122002",
"@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.0-30720210122002",
"@dcloudio/vue-cli-plugin-uni": "^2.0.0-30720210122002",
"@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.0-30720210122002",
"@dcloudio/webpack-uni-mp-loader": "^2.0.0-30720210122002",
"@dcloudio/webpack-uni-pages-loader": "^2.0.0-30720210122002",
"@vue/cli-plugin-babel": "~4.5.0",
"@vue/cli-service": "~4.5.0",
"async-validator": "^1.11.5",
"babel-plugin-import": "^1.11.0",
"cross-env": "^7.0.2",
"jest": "^25.4.0",
"less": "^3.12.2",
"less-loader": "^5.0.0",
"mini-types": "*",
"miniprogram-api-typings": "*",
"postcss-comment": "^2.0.0",
"sass-loader": "^11.0.1",
"vue-template-compiler": "^2.6.11"
},
"browserslist": [
"Android >= 4",
"ios >= 8"
],
"uni-app": {
"scripts": {}
}
}

22
postcss.config.js Normal file
View File

@ -0,0 +1,22 @@
const path = require("path");
module.exports = {
parser: require("postcss-comment"),
plugins: [
require("postcss-import")({
resolve(id, basedir, importOptions) {
if (id.startsWith("~@/")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(3));
} else if (id.startsWith("@/")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(2));
} else if (id.startsWith("/") && !id.startsWith("//")) {
return path.resolve(process.env.UNI_INPUT_DIR, id.substr(1));
}
return id;
},
}),
require("autoprefixer")({
remove: process.env.UNI_PLATFORM !== "h5",
}),
require("@dcloudio/vue-cli-plugin-uni/packages/postcss"),
],
};

View File

@ -1,33 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta
name="keywords"
content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs"
/>
<meta
name="description"
content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器"
/>
<meta
name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
/>
<title>微信 Markdown 编辑器</title>
<link
rel="shortcut icon"
href="https://gitee.com/yanglbme/resource/raw/master/doocs-md/favicon.png"
/>
<link
rel="apple-touch-icon-precomposed"
href="https://gitee.com/yanglbme/resource/raw/master/doocs-md/qrcode.png"
/>
<script src="https://cdn.bootcdn.net/ajax/libs/prettify/r224/prettify.min.js"></script>
</head>
<html lang="zh-CN">
<body>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="keywords" content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs" />
<meta name="description" content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器" />
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
<!-- <title>
<%= htmlWebpackPlugin.options.title %>
</title> -->
<title>微信 Markdown 编辑器</title>
<link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
<link rel="shortcut icon" href="https://gitee.com/yanglbme/resource/raw/master/doocs-md/favicon.png" />
<link rel="apple-touch-icon-precomposed" href="https://gitee.com/yanglbme/resource/raw/master/doocs-md/qrcode.png" />
<script src="https://cdn.bootcdn.net/ajax/libs/prettify/r224/prettify.min.js"></script>
</head>
<body>
<noscript>
<strong>Please enable JavaScript to continue.</strong>
</noscript>
<div id="app"></div>
</body>
<!-- built files will be auto injected -->
</body>
</html>

View File

@ -1,43 +1,76 @@
<template>
<transition name="fade" v-if="loading">
<loading />
</transition>
<codemirror-editor v-else />
</template>
<script>
import Loading from "./components/Loading";
import CodemirrorEditor from "./view/CodemirrorEditor";
export default {
name: "App",
components: {
Loading,
CodemirrorEditor,
onLaunch: function () {
console.log("App Launch");
},
data() {
return {
loading: true,
};
onShow: function () {
console.log("App Show");
},
mounted() {
setTimeout(() => {
this.loading = false;
}, 100);
onHide: function () {
console.log("App Hide");
},
};
</script>
<style lang="scss" scoped>
.fade-enter,
.fade-leave-to {
opacity: 0;
<style lang="less">
/* 每个页面公共css */
@import url("./assets/less/style-mirror.css");
@import url("./assets/less/theme.less");
::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: #fff;
}
.fade-enter-to,
.fade-leave {
opacity: 1;
::-webkit-scrollbar-track {
border-radius: 6px;
background-color: rgba(200, 200, 200, 0.3);
}
.fade-enter-active,
.fade-leave-active {
transition: all 1s;
::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: rgba(144, 146, 152, 0.5);
transition: background-color 0.3s;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 146, 152, 0.5);
}
/* CSS-hints */
.CodeMirror-hints {
position: absolute;
z-index: 10;
overflow: hidden;
list-style: none;
margin: 0;
padding: 2px;
border-radius: 4px;
background-color: #ffffff;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
min-width: 200px;
font-size: 12px;
font-family: monospace;
max-height: 20em;
overflow-y: auto;
color: #333333;
}
.CodeMirror-hint {
margin: 0;
margin-top: 10px;
padding: 4px 6px;
border-radius: 2px;
white-space: pre;
color: black;
cursor: pointer;
}
.CodeMirror-hint:first-of-type {
margin-top: 0;
}
.CodeMirror-hint:hover {
background: #f0f0f0;
}
</style>

View File

@ -41,14 +41,6 @@ body {
flex-direction: column;
}
.top {
height: 60px;
padding: 10px 20px;
display: flex;
align-items: center;
margin-right: 20px;
}
.web-title {
margin: 0 15px 0 5px;
}
@ -75,7 +67,6 @@ section {
display: flex;
flex-direction: column;
padding-top: 0;
padding-bottom: 10px;
}
.ctrl {
@ -92,8 +83,8 @@ section {
align-items: center;
justify-content: center;
display: flex;
overflow: scroll;
word-break: break-all;
overflow-y: scroll;
}
.main-section {
@ -107,6 +98,7 @@ section {
}
.preview {
position: relative;
margin: 0 -20px;
width: 375px;
padding: 20px;
@ -123,31 +115,6 @@ section {
width: 100% !important;
}
/*
.preview table tr:nth-child(even){
background: rgb(250, 250, 250);
}
*/
.select-item-left {
float: left;
}
.select-item-right {
float: right;
color: #8492a6;
font-size: 13px;
}
.CodeMirror {
height: 100% !important;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
font-size: 14px;
padding: 20px;
width: 100% !important;
font-family: "PingFang SC", BlinkMacSystemFont, Roboto, "Helvetica Neue",
sans-serif !important;
}
/* ele ui */
.el-form-item {
margin-bottom: 0 !important;
@ -157,33 +124,10 @@ section {
cursor: pointer;
}
::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: #fff;
}
::-webkit-scrollbar-track {
border-radius: 6px;
background-color: rgba(200, 200, 200, 0.3);
}
::-webkit-scrollbar-thumb {
border-radius: 6px;
background-color: rgba(144, 146, 152, 0.5);
transition: background-color 0.3s;
}
::-webkit-scrollbar-thumb:hover {
background-color: rgba(144, 146, 152, 0.5);
}
.CodeMirror-vscrollbar:focus {
outline: none;
}
.CodeMirror-scroll,
.preview-wrapper {
overflow: unset;
overflow-y: scroll;
uni-page-body,
uni-page-refresh {
display: block;
box-sizing: border-box;
width: 100%;
height: 100%;
}

View File

@ -9,13 +9,19 @@
*/
.cm-s-style-mirror.CodeMirror {
background: #f5f5f5;
color: #444;
font-size: 16px;
padding: 20px;
line-height: 25px;
}
.cm-s-style-mirror .CodeMirror-scroll {
padding: 20px;
width: 100%;
box-sizing: border-box;
overflow-x: hidden!important;
overflow-y: scroll!important;
}
.cm-s-style-mirror div.CodeMirror-selected {
background: #e0e0e0;
}

View File

@ -129,3 +129,32 @@
background-color: @nightCodeMirrorColor;
}
}
.CodeMirror {
padding-bottom: 0;
height: 100% !important;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
font-size: 14px;
font-family: "PingFang SC", BlinkMacSystemFont, Roboto, "Helvetica Neue",
sans-serif !important;
}
.CodeMirror-vscrollbar:focus {
outline: none;
}
.CodeMirror-scroll {
padding: 20px;
padding-bottom: 0px;
overflow-x: hidden !important;
overflow-y: scroll !important;
}
.CodeMirror-vscrollbar {
width: 0px;
height: 0px;
}
.codeMirror-wrapper {
height: 100% !important;
}

View File

@ -111,10 +111,7 @@ class WxRenderer {
}
};
renderer.paragraph = (text) => {
if (
text.indexOf("<figure") != -1 &&
text.indexOf("<img") != -1
) {
if (text.indexOf("<figure") != -1 && text.indexOf("<img") != -1) {
return text;
}
return text.replace(/ /g, "") === ""
@ -123,13 +120,8 @@ class WxRenderer {
};
renderer.blockquote = (text) => {
text = text.replace(
/<p.*?>/g,
`<p ${getStyles("blockquote_p")}>`
);
return `<blockquote ${getStyles(
"blockquote"
)}>${text}</blockquote>`;
text = text.replace(/<p.*?>/g, `<p ${getStyles("blockquote_p")}>`);
return `<blockquote ${getStyles("blockquote")}>${text}</blockquote>`;
};
renderer.code = (text, lang) => {
text = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
@ -178,24 +170,20 @@ class WxRenderer {
)}>${text}</figcaption>`;
}
let figureStyles = getStyles("figure");
let imgStyles = getStyles(
ENV_STRETCH_IMAGE ? "image" : "image_org"
);
let imgStyles = getStyles(ENV_STRETCH_IMAGE ? "image" : "image_org");
return `<figure ${figureStyles}><img ${imgStyles} src="${href}" title="${title}" alt="${text}"/>${subText}</figure>`;
};
renderer.link = (href, title, text) => {
if (href.startsWith("https://mp.weixin.qq.com")) {
return `<a href="${href}" title="${
title || text
}" ${getStyles("wx_link")}>${text}</a>`;
return `<a href="${href}" title="${title || text}" ${getStyles(
"wx_link"
)}>${text}</a>`;
}
if (href === text || !status) {
return text;
}
let ref = addFootnote(title || text, href);
return `<span ${getStyles(
"link"
)}>${text}<sup>[${ref}]</sup></span>`;
return `<span ${getStyles("link")}>${text}<sup>[${ref}]</sup></span>`;
};
renderer.strong = (text) =>
`<strong ${getStyles("strong")}>${text}</strong>`;

View File

@ -49,9 +49,7 @@ export function utf8to16(str) {
char2 = str.charCodeAt(i++);
char3 = str.charCodeAt(i++);
out += String.fromCharCode(
((c & 0x0f) << 12) |
((char2 & 0x3f) << 6) |
((char3 & 0x3f) << 0)
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
);
break;
}
@ -208,9 +206,7 @@ export function base64encode(str) {
c2 = str.charCodeAt(i++);
if (i == len) {
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(
((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4)
);
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4));
out += base64EncodeChars.charAt((c2 & 0xf) << 2);
out += "=";
break;

View File

@ -110,11 +110,7 @@ export function css2json(css) {
// 初始化返回值
let json = {};
while (
css.length > 0 &&
css.indexOf("{") !== -1 &&
css.indexOf("}") !== -1
) {
while (css.length > 0 && css.indexOf("{") !== -1 && css.indexOf("}") !== -1) {
// 存储第一个左/右花括号的下标
const lbracket = css.indexOf("{");
const rbracket = css.indexOf("}");

View File

@ -1,11 +1,8 @@
<template>
<el-container class="top is-dark">
<div class="left-side">
<!-- 图片上传 -->
<el-tooltip
:effect="effect"
content="上传图片"
placement="bottom-start"
>
<el-tooltip :effect="effect" content="上传图片" placement="bottom-start">
<i
class="el-icon-upload"
size="medium"
@ -51,8 +48,6 @@
@click="$emit('show-dialog-form')"
></i>
</el-tooltip>
<el-form size="mini" class="ctrl" :inline="true">
<el-form-item>
<el-select
v-model="selectFont"
size="mini"
@ -71,8 +66,6 @@
<span class="select-item-right">Abc</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="selectSize"
size="mini"
@ -90,8 +83,6 @@
<span class="select-item-right">{{ size.desc }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="selectColor"
size="mini"
@ -109,7 +100,6 @@
<span class="select-item-right">{{ color.desc }}</span>
</el-option>
</el-select>
</el-form-item>
<el-tooltip content="自定义颜色" :effect="effect" placement="top">
<el-color-picker
v-model="selectColor"
@ -132,7 +122,8 @@
>
</el-switch>
</el-tooltip>
</el-form>
</div>
<div class="right-side">
<el-tooltip
class="item"
:effect="effect"
@ -175,6 +166,7 @@
></div>
<div class="mode__switch" v-else @click="themeChanged"></div>
</el-tooltip>
</div>
<resetDialog
:showResetConfirm="showResetConfirm"
@confirm="confirmReset"
@ -291,8 +283,7 @@ export default {
//
this.$notify({
showClose: true,
message:
"已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴",
message: "已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴",
offset: 80,
duration: 1600,
type: "success",
@ -300,7 +291,6 @@ export default {
this.$emit("refresh");
this.$emit("endCopy");
}, 350);
e.target.blur();
},
// CSS
async customStyle() {
@ -375,6 +365,7 @@ export default {
}
.mode__switch {
margin-left: 24px;
margin-right: 24px;
width: 24px;
height: 24px;
background: url("../../assets/images/night.png") no-repeat;
@ -386,6 +377,37 @@ export default {
background-size: cover;
}
.top {
height: 60px;
padding: 10px 20px;
display: flex;
align-items: center;
margin-right: 0;
}
.el-select {
margin-right: 12px;
}
.left-side {
display: flex;
align-items: center;
flex: 1;
}
.right-side {
display: flex;
align-items: center;
}
/*
.preview table tr:nth-child(even){
background: rgb(250, 250, 250);
}
*/
.select-item-left {
float: left;
}
.select-item-right {
float: right;
color: #8492a6;
font-size: 13px;
}
</style>

View File

@ -47,9 +47,7 @@
<el-button :type="btnType" plain @click="$emit('input', false)"
> </el-button
>
<el-button :type="btnType" @click="insertTable" plain
> </el-button
>
<el-button :type="btnType" @click="insertTable" plain> </el-button>
</div>
</el-dialog>
</template>

View File

@ -5,13 +5,9 @@
:visible="showResetConfirm"
@close="$emit('close')"
>
<div class="text">
此操作将丢失本地缓存的文本和自定义样式是否继续?
</div>
<div class="text">此操作将丢失本地缓存的文本和自定义样式是否继续?</div>
<div slot="footer" class="dialog-footer">
<el-button :type="btnType" plain @click="$emit('close')"
> </el-button
>
<el-button :type="btnType" plain @click="$emit('close')"> </el-button>
<el-button :type="btnType" @click="$emit('confirm')" plain
> </el-button
>

View File

@ -23,7 +23,7 @@
</el-select>
<el-upload
drag
action
action=""
:headers="{ 'Content-Type': 'multipart/form-data' }"
:show-file-list="false"
:multiple="true"
@ -68,14 +68,11 @@
type="primary"
href="https://gitee.com/profile/personal_access_tokens"
target="_blank"
>请在
Gitee设置->安全设置->私人令牌中生成</el-link
>请在 Gitee设置->安全设置->私人令牌中生成</el-link
>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="saveGiteeConfiguration"
<el-button type="primary" @click="saveGiteeConfiguration"
>保存配置</el-button
>
</el-form-item>
@ -114,9 +111,7 @@
>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="saveGitHubConfiguration"
<el-button type="primary" @click="saveGitHubConfiguration"
>保存配置</el-button
>
</el-form-item>
@ -173,9 +168,7 @@
>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="saveAliOSSConfiguration"
<el-button type="primary" @click="saveAliOSSConfiguration"
>保存配置</el-button
>
</el-form-item>
@ -232,9 +225,7 @@
>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="saveTxCOSConfiguration"
<el-button type="primary" @click="saveTxCOSConfiguration"
>保存配置</el-button
>
</el-form-item>
@ -291,9 +282,7 @@
>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="saveQiniuConfiguration"
<el-button type="primary" @click="saveQiniuConfiguration"
>保存配置</el-button
>
</el-form-item>
@ -401,23 +390,16 @@ export default {
},
saveGitHubConfiguration() {
if (!(this.formGitHub.repo && this.formGitHub.accessToken)) {
const blankElement = this.formGitHub.repo
? "token"
: "GitHub 仓库";
const blankElement = this.formGitHub.repo ? "token" : "GitHub 仓库";
this.$message.error(`参数「​${blankElement}」不能为空`);
return;
}
localStorage.setItem(
"githubConfig",
JSON.stringify(this.formGitHub)
);
localStorage.setItem("githubConfig", JSON.stringify(this.formGitHub));
this.$message.success("保存成功");
},
saveGiteeConfiguration() {
if (!(this.formGitee.repo && this.formGitee.accessToken)) {
const blankElement = this.formGitee.repo
? "私人令牌"
: "Gitee 仓库";
const blankElement = this.formGitee.repo ? "私人令牌" : "Gitee 仓库";
this.$message.error(`参数「​${blankElement}」不能为空`);
return;
}
@ -436,10 +418,7 @@ export default {
this.$message.error(`阿里云 OSS 参数配置不全`);
return;
}
localStorage.setItem(
"aliOSSConfig",
JSON.stringify(this.formAliOSS)
);
localStorage.setItem("aliOSSConfig", JSON.stringify(this.formAliOSS));
this.$message.success("保存成功");
},

View File

@ -1,5 +1,5 @@
import Vue from "vue";
import App from "./App.vue";
import App from "./App";
import store from "./store";
import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css";
@ -13,13 +13,15 @@ import "codemirror/addon/edit/matchbrackets";
import "codemirror/addon/selection/active-line";
import "codemirror/addon/hint/show-hint.js";
import "codemirror/addon/hint/css-hint.js";
import "./assets/less/theme.less";
Vue.use(ElementUI);
Vue.config.productionTip = false;
new Vue({
App.mpType = "app";
const app = new Vue({
store,
render: (h) => h(App),
}).$mount("#app");
...App,
});
app.$mount();

71
src/manifest.json Normal file
View File

@ -0,0 +1,71 @@
{
"name": "uni-md",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"h5": {
"publicPath": "/md/"
},
"app-plus": {
"usingComponents": true,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.READ_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.WRITE_CONTACTS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.CALL_PHONE\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios": {},
"sdkConfigs": {}
}
},
"quickapp": {},
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false
},
"usingComponents": true
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"mp-qq": {
"usingComponents": true
}
}

17
src/pages.json Normal file
View File

@ -0,0 +1,17 @@
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": ""
}
}
],
"globalStyle": {
"navigationStyle": "custom",
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
}
}

43
src/pages/index/index.vue Normal file
View File

@ -0,0 +1,43 @@
<template>
<transition name="fade" v-if="loading">
<loading />
</transition>
<codemirror-editor v-else />
</template>
<script>
import Loading from "../../components/Loading";
import CodemirrorEditor from "./view/CodemirrorEditor";
export default {
name: "App",
components: {
Loading,
CodemirrorEditor,
},
data() {
return {
loading: true,
};
},
mounted() {
setTimeout(() => {
this.loading = false;
}, 100);
},
};
</script>
<style scoped>
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.fade-enter-to,
.fade-leave {
opacity: 1;
}
.fade-enter-active,
.fade-leave-active {
transition: all 1s;
}
</style>

View File

@ -0,0 +1,486 @@
<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"
@show-about-dialog="aboutDialogVisible = true"
@show-dialog-form="dialogFormVisible = true"
@show-dialog-upload-img="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"
class="codeMirror-wrapper"
@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"
v-show="showCssEditor"
:span="12"
class="cssEditor-wrapper"
>
<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"
@beforeUpload="beforeUpload"
@uploadImage="uploadImage"
@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,
formatDoc,
setFontSize,
saveEditorContent,
customCssWithTemplate,
checkImage,
} from "../../../assets/scripts/util";
import { toBase64 } from "../../../assets/scripts/util";
import fileApi from "../../../api/file";
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 pasteFile = item.getAsFile();
const isValid = this.beforeUpload(pasteFile);
if (!isValid) {
continue;
}
this.uploadImage(pasteFile);
}
}
});
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();
},
beforeUpload(file) {
// validate image
const checkResult = checkImage(file);
if (!checkResult.ok) {
this.$message.error(checkResult.msg);
return false;
}
// check image host
let imgHost = localStorage.getItem("imgHost");
imgHost = imgHost ? imgHost : "default";
localStorage.setItem("imgHost", imgHost);
const config = localStorage.getItem(`${imgHost}Config`);
const isValidHost = imgHost == "default" || config;
if (!isValidHost) {
this.$message.error(`请先配置 ${imgHost} 图床参数`);
return false;
}
return true;
},
uploadImage(file) {
this.isImgLoading = true;
toBase64(file)
.then((base64Content) => {
fileApi
.fileUpload(base64Content, file)
.then((url) => {
this.uploaded(url);
})
.catch((err) => {
this.$message.error(err.message);
});
})
.catch((err) => {
this.$message.error(err.message);
});
this.isImgLoading = false;
},
//
uploaded(response) {
if (!response) {
this.$message.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.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));
},
//
formatContent() {
const doc = formatDoc(this.editor.getValue(0));
localStorage.setItem("__editor_content", doc);
this.editor.setValue(doc);
},
//
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;
break;
case "formatMarkdown":
this.formatContent();
break;
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;
}
.textarea-wrapper {
height: 100%;
}
.preview-wrapper_night {
overflow-y: inherit;
position: relative;
left: -3px;
.preview {
background-color: #fff;
}
}
#output-wrapper {
position: relative;
user-select: text;
}
.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;
}
/deep/ .preview-table {
border-spacing: 0px;
}
@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" scoped>
@import url("../../../assets/less/app.less");
@import url("../../../assets/less/github-v2.min.css");
</style>

View File

@ -69,8 +69,7 @@ const mutations = {
state.currentSize =
localStorage.getItem("size") || config.sizeOption[2].value;
state.codeTheme =
localStorage.getItem("codeTheme") ||
config.codeThemeOption[0].value;
localStorage.getItem("codeTheme") || config.codeThemeOption[0].value;
state.citeStatus = localStorage.getItem("citeStatus") === "true";
state.nightMode = localStorage.getItem("nightMode") === "true";
state.wxRenderer = new WxRenderer({
@ -81,10 +80,13 @@ const mutations = {
});
},
initEditorEntity(state) {
state.editor = CodeMirror.fromTextArea(
document.getElementById("editor"),
{
value: "",
const editorDom = document.getElementById("editor");
if (!editorDom.value) {
editorDom.value =
localStorage.getItem("__editor_content") || formatDoc(DEFAULT_CONTENT);
}
state.editor = CodeMirror.fromTextArea(editorDom, {
mode: "text/x-markdown",
theme: "xq-light",
lineNumbers: false,
@ -99,20 +101,16 @@ const mutations = {
},
"Ctrl-S": function save(editor) {},
},
}
);
// 如果有编辑器内容被保存则读取,否则加载默认内容
state.editor.setValue(
localStorage.getItem("__editor_content") ||
formatDoc(DEFAULT_CONTENT)
);
});
},
initCssEditorEntity(state) {
state.cssEditor = CodeMirror.fromTextArea(
document.getElementById("cssEditor"),
{
value: "",
const cssEditorDom = document.getElementById("cssEditor");
if (!cssEditorDom.value) {
cssEditorDom.value =
localStorage.getItem("__css_content") || DEFAULT_CSS_CONTENT;
}
state.cssEditor = CodeMirror.fromTextArea(cssEditorDom, {
mode: "css",
theme: "style-mirror",
lineNumbers: false,
@ -127,13 +125,7 @@ const mutations = {
},
"Ctrl-S": function save(editor) {},
},
}
);
// 如果有编辑器内容被保存则读取,否则加载默认内容
state.cssEditor.setValue(
localStorage.getItem("__css_content") || DEFAULT_CSS_CONTENT
);
});
},
editorRefresh(state) {
let output = marked(state.editor.getValue(0), {
@ -152,6 +144,7 @@ const mutations = {
},
clearEditorToDefault(state) {
const doc = formatDoc(DEFAULT_CONTENT);
state.editor.setValue(doc);
state.cssEditor.setValue(DEFAULT_CSS_CONTENT);
},

View File

@ -1,505 +0,0 @@
<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"
@show-about-dialog="aboutDialogVisible = true"
@show-dialog-form="dialogFormVisible = true"
@show-dialog-upload-img="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"
@beforeUpload="beforeUpload"
@uploadImage="uploadImage"
@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,
formatDoc,
setFontSize,
saveEditorContent,
customCssWithTemplate,
checkImage,
} from "../assets/scripts/util";
import { toBase64 } from "../assets/scripts/util";
import fileApi from "../api/file";
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 pasteFile = item.getAsFile();
const isValid = this.beforeUpload(pasteFile);
if (!isValid) {
continue;
}
this.uploadImage(pasteFile);
}
}
});
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();
},
beforeUpload(file) {
// validate image
const checkResult = checkImage(file);
if (!checkResult.ok) {
this.$message.error(checkResult.msg);
return false;
}
// check image host
let imgHost = localStorage.getItem("imgHost");
imgHost = imgHost ? imgHost : "default";
localStorage.setItem("imgHost", imgHost);
const config = localStorage.getItem(`${imgHost}Config`);
const isValidHost = imgHost == "default" || config;
if (!isValidHost) {
this.$message.error(`请先配置 ${imgHost} 图床参数`);
return false;
}
return true;
},
uploadImage(file) {
this.isImgLoading = true;
toBase64(file)
.then((base64Content) => {
fileApi
.fileUpload(base64Content, file)
.then((url) => {
this.uploaded(url);
})
.catch((err) => {
this.$message.error(err.message);
});
})
.catch((err) => {
this.$message.error(err.message);
});
this.isImgLoading = false;
},
//
uploaded(response) {
if (!response) {
this.$message.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.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));
},
//
formatContent() {
const doc = formatDoc(this.editor.getValue(0));
localStorage.setItem("__editor_content", doc);
this.editor.setValue(doc);
},
//
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;
break;
case "formatMarkdown":
this.formatContent();
break;
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>

9
tsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"types": [
"@dcloudio/types",
"miniprogram-api-typings",
"mini-types"
]
}
}

View File

@ -1,4 +1,20 @@
module.exports = {
outputDir: "dist",
publicPath: process.env.NETLIFY ? "/" : "/md/",
};
const fs = require("fs");
function writeManifestJson() {
fs.readFile("./src/manifest.json", function (err, data) {
if (err) {
return console.error(err);
}
const strData = data.toString();
const manifest = JSON.parse(strData);
manifest.h5.publicPath = process.env.NETLIFY ? "/" : "/md/";
const result = JSON.stringify(manifest, null, 4);
fs.writeFile("./src/manifest.json", result, function (err) {
if (err) {
console.error(err);
}
});
});
}
writeManifestJson();

10677
yarn.lock Normal file

File diff suppressed because it is too large Load Diff