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_style = space
indent_size = 4
trim_trailing_whitespace = true
insert_final_newline = 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,27 +1,27 @@
module.exports = { module.exports = {
root: true, root: true,
env: { env: {
node: true, node: true,
},
extends: ["plugin:vue/essential", "@vue/standard"],
rules: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
camelcase: "off",
eqeqeq: "off",
},
parserOptions: {
parser: "babel-eslint",
},
overrides: [
{
files: [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)",
],
env: {
jest: true,
},
}, },
extends: ["plugin:vue/essential", "@vue/standard"], ],
rules: {
"no-console": process.env.NODE_ENV === "production" ? "error" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",
camelcase: "off",
eqeqeq: "off",
},
parserOptions: {
parser: "babel-eslint",
},
overrides: [
{
files: [
"**/__tests__/*.{j,t}s?(x)",
"**/tests/unit/**/*.spec.{j,t}s?(x)",
],
env: {
jest: true,
},
},
],
}; };

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 = { 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",
};

27468
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,53 +1,112 @@
{ {
"name": "vue-md", "name": "md",
"version": "1.4.7", "version": "1.5.0",
"homepage": ".", "private": true,
"description": "An open-source wechat markdown editor.",
"author": "doocs",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "npm run dev:h5",
"build": "vue-cli-service build", "build": "npm run build:h5",
"lint": "vue-cli-service lint", "build:app-plus": "cross-env NODE_ENV=production UNI_PLATFORM=app-plus vue-cli-service uni-build",
"test:unit": "vue-cli-service test:unit" "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": { "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", "ali-oss": "^6.11.2",
"axios": "^0.21.0", "axios": "^0.21.0",
"buffer-from": "^1.1.1", "buffer-from": "^1.1.1",
"codemirror": "^5.58.3", "codemirror": "^5.58.3",
"core-js": "^3.7.0", "core-js": "^3.8.3",
"cos-js-sdk-v5": "^1.1.0", "cos-js-sdk-v5": "^1.1.0",
"crypto-js": "^4.0.0", "crypto-js": "^4.0.0",
"element-ui": "^2.14.1", "element-ui": "^2.14.1",
"flyio": "^0.6.2",
"jquery": "^3.5.1", "jquery": "^3.5.1",
"juice": "^7.0.0", "juice": "^7.0.0",
"less": "^3.12.2",
"marked": "^2.0.0", "marked": "^2.0.0",
"prettier": "^2.2.0", "prettier": "^2.2.0",
"prettify": "^0.1.7", "prettify": "^0.1.7",
"qiniu-js": "^3.1.2", "qiniu-js": "^3.1.2",
"regenerator-runtime": "^0.12.1",
"uuid": "^8.3.1", "uuid": "^8.3.1",
"vue": "^2.6.12", "vue": "^2.6.12",
"vue-router": "^3.4.9", "vue-router": "^3.4.9",
"vuex": "^3.5.1" "vuex": "^3.5.1"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.5.9", "@dcloudio/types": "*",
"@vue/cli-plugin-eslint": "^4.5.9", "@dcloudio/uni-automator": "^2.0.0-30720210122002",
"@vue/cli-plugin-unit-jest": "^4.5.9", "@dcloudio/uni-cli-shared": "^2.0.0-30720210122002",
"@vue/cli-service": "^4.5.9", "@dcloudio/uni-migration": "^2.0.0-30720210122002",
"@vue/eslint-config-standard": "^5.1.2", "@dcloudio/uni-template-compiler": "^2.0.0-30720210122002",
"@vue/test-utils": "1.1.1", "@dcloudio/vue-cli-plugin-hbuilderx": "^2.0.0-30720210122002",
"babel-eslint": "^10.1.0", "@dcloudio/vue-cli-plugin-uni": "^2.0.0-30720210122002",
"eslint": "^7.14.0", "@dcloudio/vue-cli-plugin-uni-optimize": "^2.0.0-30720210122002",
"eslint-plugin-import": "^2.22.1", "@dcloudio/webpack-uni-mp-loader": "^2.0.0-30720210122002",
"eslint-plugin-node": "^11.1.0", "@dcloudio/webpack-uni-pages-loader": "^2.0.0-30720210122002",
"eslint-plugin-promise": "^4.2.1", "@vue/cli-plugin-babel": "~4.5.0",
"eslint-plugin-standard": "^5.0.0", "@vue/cli-service": "~4.5.0",
"eslint-plugin-vue": "^7.1.0", "async-validator": "^1.11.5",
"less-loader": "^7.1.0", "babel-plugin-import": "^1.11.0",
"node-sass": "^5.0.0", "cross-env": "^7.0.2",
"sass-loader": "^10.1.0", "jest": "^25.4.0",
"vue-template-compiler": "^2.6.12" "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> <!DOCTYPE html>
<html lang="en"> <html lang="zh-CN">
<head>
<meta charset="UTF-8" /> <head>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <meta charset="utf-8">
<meta <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
name="keywords" <meta name="keywords" content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs" />
content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs" <meta name="description" content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器" />
/> <meta name="viewport"
<meta content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" />
name="description" <!-- <title>
content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器" <%= htmlWebpackPlugin.options.title %>
/> </title> -->
<meta <title>微信 Markdown 编辑器</title>
name="viewport" <link rel="stylesheet" href="<%= BASE_URL %>static/index.<%= VUE_APP_INDEX_CSS_HASH %>.css" />
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0" <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" />
<title>微信 Markdown 编辑器</title> <script src="https://cdn.bootcdn.net/ajax/libs/prettify/r224/prettify.min.js"></script>
<link </head>
rel="shortcut icon"
href="https://gitee.com/yanglbme/resource/raw/master/doocs-md/favicon.png" <body>
/> <noscript>
<link <strong>Please enable JavaScript to continue.</strong>
rel="apple-touch-icon-precomposed" </noscript>
href="https://gitee.com/yanglbme/resource/raw/master/doocs-md/qrcode.png" <div id="app"></div>
/> <!-- built files will be auto injected -->
<script src="https://cdn.bootcdn.net/ajax/libs/prettify/r224/prettify.min.js"></script> </body>
</head>
<body>
<div id="app"></div>
</body>
</html> </html>

View File

@ -1,43 +1,76 @@
<template>
<transition name="fade" v-if="loading">
<loading />
</transition>
<codemirror-editor v-else />
</template>
<script> <script>
import Loading from "./components/Loading";
import CodemirrorEditor from "./view/CodemirrorEditor";
export default { export default {
name: "App", onLaunch: function () {
components: { console.log("App Launch");
Loading, },
CodemirrorEditor, onShow: function () {
}, console.log("App Show");
data() { },
return { onHide: function () {
loading: true, console.log("App Hide");
}; },
},
mounted() {
setTimeout(() => {
this.loading = false;
}, 100);
},
}; };
</script> </script>
<style lang="scss" scoped> <style lang="less">
.fade-enter, /* 每个页面公共css */
.fade-leave-to { @import url("./assets/less/style-mirror.css");
opacity: 0; @import url("./assets/less/theme.less");
::-webkit-scrollbar {
width: 6px;
height: 6px;
background-color: #fff;
} }
.fade-enter-to,
.fade-leave { ::-webkit-scrollbar-track {
opacity: 1; border-radius: 6px;
background-color: rgba(200, 200, 200, 0.3);
} }
.fade-enter-active,
.fade-leave-active { ::-webkit-scrollbar-thumb {
transition: all 1s; 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> </style>

View File

@ -1,55 +1,55 @@
const githubConfig = { const githubConfig = {
username: "filess", username: "filess",
repoList: Array.from( repoList: Array.from(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
(e) => `img${e}` (e) => `img${e}`
), ),
branch: "main", branch: "main",
accessTokenList: [ accessTokenList: [
"7715d7ca67b5d3837cfdoocsmde8c38421815aa423510af", "7715d7ca67b5d3837cfdoocsmde8c38421815aa423510af",
"c411415bf95dbe39625doocsmd5047ba9b7a2a6c9642abe", "c411415bf95dbe39625doocsmd5047ba9b7a2a6c9642abe",
"2821cd8819fa345c053doocsmdca86ac653f8bc20db1f1b", "2821cd8819fa345c053doocsmdca86ac653f8bc20db1f1b",
"445f0dae46ef1f2a4d6doocsmdc797301e94797b4750a4c", "445f0dae46ef1f2a4d6doocsmdc797301e94797b4750a4c",
"cc1d0c1426d0fd0902bdoocsmdd2d7184b14da61b86ec46", "cc1d0c1426d0fd0902bdoocsmdd2d7184b14da61b86ec46",
"b67e9d15cb6f910492fdoocsmdac6b44d379c953bb19eff", "b67e9d15cb6f910492fdoocsmdac6b44d379c953bb19eff",
"618c4dc2244ccbbc088doocsmd125d17fd31b7d06a50cf3", "618c4dc2244ccbbc088doocsmd125d17fd31b7d06a50cf3",
"a4b581732e1c1507458doocsmdc5b223b27dae5e2e16a55", "a4b581732e1c1507458doocsmdc5b223b27dae5e2e16a55",
"77904db41aee57ad79bdoocsmd760f848201dac9c96fd5e", "77904db41aee57ad79bdoocsmd760f848201dac9c96fd5e",
"02f251cb14ac62ab100doocsmdddbfc8527d773f1f04ce1", "02f251cb14ac62ab100doocsmdddbfc8527d773f1f04ce1",
"eb321079a95ba7028d9doocsmde2e84c502dac70de7cf08", "eb321079a95ba7028d9doocsmde2e84c502dac70de7cf08",
"22f74fcfb071a961fa2doocsmde28dabc746f0503a15e5d", "22f74fcfb071a961fa2doocsmde28dabc746f0503a15e5d",
"85124c2bfe7abba0938doocsmd0af7f67918b99d085a5fd", "85124c2bfe7abba0938doocsmd0af7f67918b99d085a5fd",
"0a561b4d4bbecb2de7edoocsmdd9ba3833d11dbc5e430f5", "0a561b4d4bbecb2de7edoocsmdd9ba3833d11dbc5e430f5",
"e8a01491188d8d5a097doocsmd03ede0aad1fe9e3af24e9", "e8a01491188d8d5a097doocsmd03ede0aad1fe9e3af24e9",
"36e1f420d7e5bdebd67doocsmd65463562f5f25b20b8377", "36e1f420d7e5bdebd67doocsmd65463562f5f25b20b8377",
], ],
}; };
const giteeConfig = { const giteeConfig = {
username: "filesss", username: "filesss",
repoList: Array.from( repoList: Array.from(
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
(e) => `img${e}` (e) => `img${e}`
), ),
branch: "main", branch: "main",
accessTokenList: [ accessTokenList: [
"ed5fc9866bd6c2fdoocsmddd433f806fd2f399c", "ed5fc9866bd6c2fdoocsmddd433f806fd2f399c",
"5448ffebbbf1151doocsmdc4e337cf814fc8a62", "5448ffebbbf1151doocsmdc4e337cf814fc8a62",
"25b05efd2557ca2doocsmd75b5c0835e3395911", "25b05efd2557ca2doocsmd75b5c0835e3395911",
"11628c7a5aef015doocsmd2eeff9fb9566f0458", "11628c7a5aef015doocsmd2eeff9fb9566f0458",
"cb2f5145ed938dedoocsmdbd063b4ed244eecf8", "cb2f5145ed938dedoocsmdbd063b4ed244eecf8",
"d8c0b57500672c1doocsmd55f48b866b5ebcd98", "d8c0b57500672c1doocsmd55f48b866b5ebcd98",
"78c56eadb88e453doocsmd43ddd95753351771a", "78c56eadb88e453doocsmd43ddd95753351771a",
"03e1a688003948fdoocsmda16fcf41e6f03f1f0", "03e1a688003948fdoocsmda16fcf41e6f03f1f0",
"c49121cf4d191fbdoocsmdd6a7877ed537e474a", "c49121cf4d191fbdoocsmdd6a7877ed537e474a",
"adfeb2fadcdc4aadoocsmdfe1ee869ac9c968ff", "adfeb2fadcdc4aadoocsmdfe1ee869ac9c968ff",
"116c94549ca4a0ddoocsmd192653af5c0694616", "116c94549ca4a0ddoocsmd192653af5c0694616",
"ecf30ed7f2eb184doocsmd51ea4ec8300371d9e", "ecf30ed7f2eb184doocsmd51ea4ec8300371d9e",
"5837cf2bd5afd93doocsmd73904bed31934949e", "5837cf2bd5afd93doocsmd73904bed31934949e",
"b5b7e1c7d57e01fdoocsmd5266f552574297d78", "b5b7e1c7d57e01fdoocsmd5266f552574297d78",
"684d55564ffbd0bdoocsmd7d747e5cc23aed6d6", "684d55564ffbd0bdoocsmd7d747e5cc23aed6d6",
"3fc04a9d272ab71doocsmd010c56cb57d88d2ba", "3fc04a9d272ab71doocsmd010c56cb57d88d2ba",
], ],
}; };
export { githubConfig, giteeConfig }; export { githubConfig, giteeConfig };

View File

@ -2,29 +2,29 @@ import axios from "axios";
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: "", baseURL: "",
timeout: 10 * 1000, // 请求超时时间 timeout: 10 * 1000, // 请求超时时间
}); });
service.interceptors.request.use( service.interceptors.request.use(
(config) => { (config) => {
if (/^(post)|(put)|(delete)$/i.test(config.method)) { if (/^(post)|(put)|(delete)$/i.test(config.method)) {
if (config.data && config.data.upload) { if (config.data && config.data.upload) {
config.headers["Content-Type"] = "multipart/form-data"; config.headers["Content-Type"] = "multipart/form-data";
} }
}
return config;
},
(error) => {
Promise.reject(error);
} }
return config;
},
(error) => {
Promise.reject(error);
}
); );
service.interceptors.response.use( service.interceptors.response.use(
(res) => { (res) => {
return res.data ? res.data : Promise.reject(res); return res.data ? res.data : Promise.reject(res);
}, },
(error) => Promise.reject(error) (error) => Promise.reject(error)
); );
export default service; export default service;

View File

@ -9,51 +9,51 @@ import * as qiniu from "qiniu-js";
import { utf16to8, base64encode, safe64 } from "../assets/scripts/tokenTools"; import { utf16to8, base64encode, safe64 } from "../assets/scripts/tokenTools";
function getConfig(useDefault, platform) { function getConfig(useDefault, platform) {
if (useDefault) { if (useDefault) {
// load default config file // load default config file
const config = platform === "github" ? githubConfig : giteeConfig; const config = platform === "github" ? githubConfig : giteeConfig;
const { username, repoList, branch, accessTokenList } = config; const { username, repoList, branch, accessTokenList } = config;
// choose random token from access_token list // choose random token from access_token list
const tokenIndex = Math.floor(Math.random() * accessTokenList.length); const tokenIndex = Math.floor(Math.random() * accessTokenList.length);
const accessToken = accessTokenList[tokenIndex].replace("doocsmd", ""); const accessToken = accessTokenList[tokenIndex].replace("doocsmd", "");
// choose random repo from repo list // choose random repo from repo list
const repoIndex = Math.floor(Math.random() * repoList.length); const repoIndex = Math.floor(Math.random() * repoList.length);
const repo = repoList[repoIndex]; const repo = repoList[repoIndex];
return { username, repo, branch, accessToken }; return { username, repo, branch, accessToken };
} }
// load configuration from localStorage // load configuration from localStorage
const customConfig = JSON.parse(localStorage.getItem(`${platform}Config`)); const customConfig = JSON.parse(localStorage.getItem(`${platform}Config`));
// split username/repo // split username/repo
const repoUrl = customConfig.repo const repoUrl = customConfig.repo
.replace(`https://${platform}.com/`, "") .replace(`https://${platform}.com/`, "")
.replace(`http://${platform}.com/`, "") .replace(`http://${platform}.com/`, "")
.replace(`${platform}.com/`, "") .replace(`${platform}.com/`, "")
.split("/"); .split("/");
return { return {
username: repoUrl[0], username: repoUrl[0],
repo: repoUrl[1], repo: repoUrl[1],
branch: customConfig.branch || "master", branch: customConfig.branch || "master",
accessToken: customConfig.accessToken, accessToken: customConfig.accessToken,
}; };
} }
function getDir() { function getDir() {
const date = new Date(); const date = new Date();
const year = date.getFullYear(); const year = date.getFullYear();
const month = (date.getMonth() + 1).toString().padStart(2, "0"); const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0"); const day = date.getDate().toString().padStart(2, "0");
return `${year}/${month}/${day}`; return `${year}/${month}/${day}`;
} }
function getDateFilename(filename) { function getDateFilename(filename) {
const currentTimestamp = new Date().getTime(); const currentTimestamp = new Date().getTime();
const fileSuffix = filename.split(".")[1]; const fileSuffix = filename.split(".")[1];
return `${currentTimestamp}-${uuidv4()}.${fileSuffix}`; return `${currentTimestamp}-${uuidv4()}.${fileSuffix}`;
} }
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@ -61,32 +61,32 @@ function getDateFilename(filename) {
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
async function ghFileUpload(content, filename) { async function ghFileUpload(content, filename) {
const useDefault = localStorage.getItem("imgHost") === "default"; const useDefault = localStorage.getItem("imgHost") === "default";
const { username, repo, branch, accessToken } = getConfig( const { username, repo, branch, accessToken } = getConfig(
useDefault, useDefault,
"github" "github"
); );
const dir = getDir(); const dir = getDir();
const url = `https://api.github.com/repos/${username}/${repo}/contents/${dir}/`; const url = `https://api.github.com/repos/${username}/${repo}/contents/${dir}/`;
const dateFilename = getDateFilename(filename); const dateFilename = getDateFilename(filename);
const res = await fetch({ const res = await fetch({
url: url + dateFilename, url: url + dateFilename,
method: "put", method: "put",
headers: { headers: {
Authorization: `token ${accessToken}`, Authorization: `token ${accessToken}`,
}, },
data: { data: {
content, content,
branch, branch,
message: `Upload by ${window.location.href}`, message: `Upload by ${window.location.href}`,
}, },
}); });
const githubResourceUrl = `raw.githubusercontent.com/${username}/${repo}/${branch}/`; const githubResourceUrl = `raw.githubusercontent.com/${username}/${repo}/${branch}/`;
const cdnResourceUrl = `cdn.jsdelivr.net/gh/${username}/${repo}@${branch}/`; const cdnResourceUrl = `cdn.jsdelivr.net/gh/${username}/${repo}@${branch}/`;
return useDefault return useDefault
? res.content.download_url.replace(githubResourceUrl, cdnResourceUrl) ? res.content.download_url.replace(githubResourceUrl, cdnResourceUrl)
: res.content.download_url; : res.content.download_url;
} }
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@ -94,25 +94,25 @@ async function ghFileUpload(content, filename) {
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
async function giteeUpload(content, filename) { async function giteeUpload(content, filename) {
const useDefault = localStorage.getItem("imgHost") === "default"; const useDefault = localStorage.getItem("imgHost") === "default";
const { username, repo, branch, accessToken } = getConfig( const { username, repo, branch, accessToken } = getConfig(
useDefault, useDefault,
"gitee" "gitee"
); );
const dir = getDir(); const dir = getDir();
const dateFilename = getDateFilename(filename); const dateFilename = getDateFilename(filename);
const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${dir}/${dateFilename}`; const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${dir}/${dateFilename}`;
const res = await fetch({ const res = await fetch({
url, url,
method: "POST", method: "POST",
data: { data: {
content, content,
branch, branch,
access_token: accessToken, access_token: accessToken,
message: `Upload by ${window.location.href}`, message: `Upload by ${window.location.href}`,
}, },
}); });
return encodeURI(res.content.download_url); return encodeURI(res.content.download_url);
} }
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@ -120,37 +120,37 @@ async function giteeUpload(content, filename) {
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
function getQiniuToken(accessKey, secretKey, putPolicy) { function getQiniuToken(accessKey, secretKey, putPolicy) {
const policy = JSON.stringify(putPolicy); const policy = JSON.stringify(putPolicy);
const encoded = base64encode(utf16to8(policy)); const encoded = base64encode(utf16to8(policy));
const hash = CryptoJS.HmacSHA1(encoded, secretKey); const hash = CryptoJS.HmacSHA1(encoded, secretKey);
const encodedSigned = hash.toString(CryptoJS.enc.Base64); const encodedSigned = hash.toString(CryptoJS.enc.Base64);
return `${accessKey}:${safe64(encodedSigned)}:${encoded}`; return `${accessKey}:${safe64(encodedSigned)}:${encoded}`;
} }
async function qiniuUpload(file) { async function qiniuUpload(file) {
const { accessKey, secretKey, bucket, region, path, domain } = JSON.parse( const { accessKey, secretKey, bucket, region, path, domain } = JSON.parse(
localStorage.getItem("qiniuConfig") localStorage.getItem("qiniuConfig")
); );
const token = getQiniuToken(accessKey, secretKey, { const token = getQiniuToken(accessKey, secretKey, {
scope: bucket, scope: bucket,
deadline: Math.trunc(new Date().getTime() / 1000) + 3600, deadline: Math.trunc(new Date().getTime() / 1000) + 3600,
}); });
const dir = path ? `${path}/` : ""; const dir = path ? `${path}/` : "";
const dateFilename = dir + getDateFilename(file.name); const dateFilename = dir + getDateFilename(file.name);
const observable = qiniu.upload(file, dateFilename, token, {}, { region }); const observable = qiniu.upload(file, dateFilename, token, {}, { region });
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
observable.subscribe({ observable.subscribe({
next: (result) => { next: (result) => {
console.log(result); console.log(result);
}, },
error: (err) => { error: (err) => {
reject(err.message); reject(err.message);
}, },
complete: (result) => { complete: (result) => {
resolve(`${domain}/${result.key}`); resolve(`${domain}/${result.key}`);
}, },
});
}); });
});
} }
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@ -158,30 +158,30 @@ async function qiniuUpload(file) {
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
async function aliOSSFileUpload(content, filename) { async function aliOSSFileUpload(content, filename) {
const dateFilename = getDateFilename(filename); const dateFilename = getDateFilename(filename);
const { const {
region, region,
bucket, bucket,
accessKeyId, accessKeyId,
accessKeySecret, accessKeySecret,
cdnHost, cdnHost,
path, path,
} = JSON.parse(localStorage.getItem("aliOSSConfig")); } = JSON.parse(localStorage.getItem("aliOSSConfig"));
const buffer = Buffer(content, "base64"); const buffer = Buffer(content, "base64");
const dir = `${path}/${dateFilename}`; const dir = `${path}/${dateFilename}`;
const client = new OSS({ const client = new OSS({
region, region,
bucket, bucket,
accessKeyId, accessKeyId,
accessKeySecret, accessKeySecret,
}); });
try { try {
const res = await client.put(dir, buffer); const res = await client.put(dir, buffer);
if (cdnHost == "") return res.url; if (cdnHost == "") return res.url;
return `${cdnHost}/${path == "" ? dateFilename : dir}`; return `${cdnHost}/${path == "" ? dateFilename : dir}`;
} catch (e) { } catch (e) {
return Promise.reject(e); return Promise.reject(e);
} }
} }
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
@ -189,61 +189,61 @@ async function aliOSSFileUpload(content, filename) {
//----------------------------------------------------------------------- //-----------------------------------------------------------------------
async function txCOSFileUpload(file) { async function txCOSFileUpload(file) {
const dateFilename = getDateFilename(file.name); const dateFilename = getDateFilename(file.name);
const { secretId, secretKey, bucket, region, path, cdnHost } = JSON.parse( const { secretId, secretKey, bucket, region, path, cdnHost } = JSON.parse(
localStorage.getItem("txCOSConfig") localStorage.getItem("txCOSConfig")
);
const cos = new COS({
SecretId: secretId,
SecretKey: secretKey,
});
return new Promise((resolve, reject) => {
cos.putObject(
{
Bucket: bucket,
Region: region,
Key: `${path}/${dateFilename}`,
Body: file,
},
function (err, data) {
if (err) {
reject(err);
} else if (cdnHost) {
resolve(
path == ""
? `${cdnHost}/${dateFilename}`
: `${cdnHost}/${path}/${dateFilename}`
);
} else {
resolve(`https://${data.Location}`);
}
}
); );
const cos = new COS({ });
SecretId: secretId,
SecretKey: secretKey,
});
return new Promise((resolve, reject) => {
cos.putObject(
{
Bucket: bucket,
Region: region,
Key: `${path}/${dateFilename}`,
Body: file,
},
function (err, data) {
if (err) {
reject(err);
} else if (cdnHost) {
resolve(
path == ""
? `${cdnHost}/${dateFilename}`
: `${cdnHost}/${path}/${dateFilename}`
);
} else {
resolve(`https://${data.Location}`);
}
}
);
});
} }
function fileUpload(content, file) { function fileUpload(content, file) {
const imgHost = localStorage.getItem("imgHost"); const imgHost = localStorage.getItem("imgHost");
!imgHost && localStorage.setItem("imgHost", "default"); !imgHost && localStorage.setItem("imgHost", "default");
switch (imgHost) { switch (imgHost) {
case "aliOSS": case "aliOSS":
return aliOSSFileUpload(content, file.name); return aliOSSFileUpload(content, file.name);
case "txCOS": case "txCOS":
return txCOSFileUpload(file); return txCOSFileUpload(file);
case "qiniu": case "qiniu":
return qiniuUpload(file); return qiniuUpload(file);
case "gitee": case "gitee":
return giteeUpload(content, file.name); return giteeUpload(content, file.name);
case "github": case "github":
return ghFileUpload(content, file.name); return ghFileUpload(content, file.name);
default: default:
// return file.size / 1024 < 1024 // return file.size / 1024 < 1024
// ? giteeUpload(content, file.name) // ? giteeUpload(content, file.name)
// : ghFileUpload(content, file.name); // : ghFileUpload(content, file.name);
return ghFileUpload(content, file.name); return ghFileUpload(content, file.name);
} }
} }
export default { export default {
fileUpload, fileUpload,
}; };

View File

@ -41,14 +41,6 @@ body {
flex-direction: column; flex-direction: column;
} }
.top {
height: 60px;
padding: 10px 20px;
display: flex;
align-items: center;
margin-right: 20px;
}
.web-title { .web-title {
margin: 0 15px 0 5px; margin: 0 15px 0 5px;
} }
@ -75,7 +67,6 @@ section {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding-top: 0; padding-top: 0;
padding-bottom: 10px;
} }
.ctrl { .ctrl {
@ -92,8 +83,8 @@ section {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
display: flex; display: flex;
overflow: scroll;
word-break: break-all; word-break: break-all;
overflow-y: scroll;
} }
.main-section { .main-section {
@ -107,6 +98,7 @@ section {
} }
.preview { .preview {
position: relative;
margin: 0 -20px; margin: 0 -20px;
width: 375px; width: 375px;
padding: 20px; padding: 20px;
@ -123,31 +115,6 @@ section {
width: 100% !important; 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 */ /* ele ui */
.el-form-item { .el-form-item {
margin-bottom: 0 !important; margin-bottom: 0 !important;
@ -157,33 +124,10 @@ section {
cursor: pointer; cursor: pointer;
} }
::-webkit-scrollbar { uni-page-body,
width: 6px; uni-page-refresh {
height: 6px; display: block;
background-color: #fff; box-sizing: border-box;
} width: 100%;
height: 100%;
::-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;
} }

View File

@ -9,13 +9,19 @@
*/ */
.cm-s-style-mirror.CodeMirror { .cm-s-style-mirror.CodeMirror {
background: #f5f5f5;
color: #444; color: #444;
font-size: 16px; font-size: 16px;
padding: 20px;
line-height: 25px; 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 { .cm-s-style-mirror div.CodeMirror-selected {
background: #e0e0e0; background: #e0e0e0;
} }

View File

@ -129,3 +129,32 @@
background-color: @nightCodeMirrorColor; 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

@ -1,74 +1,74 @@
export default { export default {
builtinFonts: [ builtinFonts: [
{ {
label: "无衬线", label: "无衬线",
value: value:
"-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif", "-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)",
desc: "最新流行",
},
{
label: "翡翠绿",
value: "rgba(0, 152, 116, 1)",
desc: "优雅清新",
},
{
label: "活力橘",
value: "rgba(250, 81, 81, 1)",
desc: "热情活泼",
},
],
codeThemeOption: [
{
label: "微信",
value: "wechat",
desc: "默认样式",
},
{
label: "GitHub",
value: "github",
desc: "精简风格",
},
],
form: {
rows: 1,
cols: 1,
}, },
{
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)",
desc: "最新流行",
},
{
label: "翡翠绿",
value: "rgba(0, 152, 116, 1)",
desc: "优雅清新",
},
{
label: "活力橘",
value: "rgba(250, 81, 81, 1)",
desc: "热情活泼",
},
],
codeThemeOption: [
{
label: "微信",
value: "wechat",
desc: "默认样式",
},
{
label: "GitHub",
value: "github",
desc: "精简风格",
},
],
form: {
rows: 1,
cols: 1,
},
}; };

View File

@ -1,25 +1,25 @@
import juice from "juice"; import juice from "juice";
export function solveWeChatImage() { export function solveWeChatImage() {
const clipboardDiv = document.getElementById("output"); const clipboardDiv = document.getElementById("output");
const images = clipboardDiv.getElementsByTagName("img"); const images = clipboardDiv.getElementsByTagName("img");
for (let i = 0; i < images.length; i++) { for (let i = 0; i < images.length; i++) {
const image = images[i]; const image = images[i];
const width = image.getAttribute("width"); const width = image.getAttribute("width");
const height = image.getAttribute("height"); const height = image.getAttribute("height");
image.removeAttribute("width"); image.removeAttribute("width");
image.removeAttribute("height"); image.removeAttribute("height");
image.style.width = width; image.style.width = width;
image.style.height = height; image.style.height = height;
} }
} }
export function solveHtml() { export function solveHtml() {
const element = document.getElementById("output-wrapper"); const element = document.getElementById("output-wrapper");
let html = element.innerHTML; let html = element.innerHTML;
let res = ""; let res = "";
res = juice.inlineContent(html, { res = juice.inlineContent(html, {
inlinePseudoElements: true, inlinePseudoElements: true,
preserveImportant: true, preserveImportant: true,
}); });
return res; return res;
} }

View File

@ -1,73 +1,73 @@
import marked from "marked"; import marked from "marked";
class WxRenderer { class WxRenderer {
constructor(opts) { constructor(opts) {
this.opts = opts; this.opts = opts;
let ENV_STRETCH_IMAGE = true; let ENV_STRETCH_IMAGE = true;
let footnotes = []; let footnotes = [];
let footnoteIndex = 0; let footnoteIndex = 0;
let styleMapping = null; let styleMapping = null;
const CODE_FONT_FAMILY = const CODE_FONT_FAMILY =
"Menlo, Operator Mono, Consolas, Monaco, monospace"; "Menlo, Operator Mono, Consolas, Monaco, monospace";
let merge = (base, extend) => Object.assign({}, base, extend); let merge = (base, extend) => Object.assign({}, base, extend);
this.buildTheme = (themeTpl) => { this.buildTheme = (themeTpl) => {
let mapping = {}; let mapping = {};
let base = merge(themeTpl.BASE, { let base = merge(themeTpl.BASE, {
"font-family": this.opts.fonts, "font-family": this.opts.fonts,
"font-size": this.opts.size, "font-size": this.opts.size,
}); });
let base_block = merge(base, {}); let base_block = merge(base, {});
for (let ele in themeTpl.inline) { for (let ele in themeTpl.inline) {
if (themeTpl.inline.hasOwnProperty(ele)) { if (themeTpl.inline.hasOwnProperty(ele)) {
let style = themeTpl.inline[ele]; let style = themeTpl.inline[ele];
mapping[ele] = merge(base, style); mapping[ele] = merge(base, style);
} }
} }
for (let ele in themeTpl.block) { for (let ele in themeTpl.block) {
if (themeTpl.block.hasOwnProperty(ele)) { if (themeTpl.block.hasOwnProperty(ele)) {
let style = themeTpl.block[ele]; let style = themeTpl.block[ele];
if (ele === "code") { if (ele === "code") {
style["font-family"] = CODE_FONT_FAMILY; style["font-family"] = CODE_FONT_FAMILY;
} }
mapping[ele] = merge(base_block, style); mapping[ele] = merge(base_block, style);
} }
} }
return mapping; return mapping;
}; };
let getStyles = (tokenName, addition) => { let getStyles = (tokenName, addition) => {
let arr = []; let arr = [];
let dict = styleMapping[tokenName]; let dict = styleMapping[tokenName];
if (!dict) return ""; if (!dict) return "";
for (const key in dict) { for (const key in dict) {
arr.push(key + ":" + dict[key]); arr.push(key + ":" + dict[key]);
} }
return `style="${arr.join(";") + (addition || "")}"`; return `style="${arr.join(";") + (addition || "")}"`;
}; };
let addFootnote = (title, link) => { let addFootnote = (title, link) => {
footnotes.push([++footnoteIndex, title, link]); footnotes.push([++footnoteIndex, title, link]);
return footnoteIndex; return footnoteIndex;
}; };
this.buildFootnotes = () => { this.buildFootnotes = () => {
let footnoteArray = footnotes.map((x) => { let footnoteArray = footnotes.map((x) => {
if (x[1] === x[2]) { if (x[1] === x[2]) {
return `<code style="font-size: 90%; opacity: 0.6;">[${x[0]}]</code>: <i>${x[1]}</i><br/>`; return `<code style="font-size: 90%; opacity: 0.6;">[${x[0]}]</code>: <i>${x[1]}</i><br/>`;
} }
return `<code style="font-size: 90%; opacity: 0.6;">[${x[0]}]</code> ${x[1]}: <i>${x[2]}</i><br/>`; return `<code style="font-size: 90%; opacity: 0.6;">[${x[0]}]</code> ${x[1]}: <i>${x[2]}</i><br/>`;
}); });
return `<h4 ${getStyles("h4")}>引用链接</h4><p ${getStyles( return `<h4 ${getStyles("h4")}>引用链接</h4><p ${getStyles(
"footnotes" "footnotes"
)}>${footnoteArray.join("\n")}</p>`; )}>${footnoteArray.join("\n")}</p>`;
}; };
this.buildAddition = () => { this.buildAddition = () => {
return ` return `
<style> <style>
.preview-wrapper pre::before { .preview-wrapper pre::before {
position: absolute; position: absolute;
@ -83,134 +83,122 @@ class WxRenderer {
} }
</style> </style>
`; `;
}; };
this.setOptions = (newOpts) => { this.setOptions = (newOpts) => {
this.opts = merge(this.opts, newOpts); this.opts = merge(this.opts, newOpts);
}; };
this.hasFootnotes = () => footnotes.length !== 0; this.hasFootnotes = () => footnotes.length !== 0;
this.getRenderer = (status) => { this.getRenderer = (status) => {
footnotes = []; footnotes = [];
footnoteIndex = 0; footnoteIndex = 0;
styleMapping = this.buildTheme(this.opts.theme); styleMapping = this.buildTheme(this.opts.theme);
let renderer = new marked.Renderer(); let renderer = new marked.Renderer();
renderer.heading = (text, level) => { renderer.heading = (text, level) => {
switch (level) { switch (level) {
case 1: case 1:
return `<h1 ${getStyles("h1")}>${text}</h1>`; return `<h1 ${getStyles("h1")}>${text}</h1>`;
case 2: case 2:
return `<h2 ${getStyles("h2")}>${text}</h2>`; return `<h2 ${getStyles("h2")}>${text}</h2>`;
case 3: case 3:
return `<h3 ${getStyles("h3")}>${text}</h3>`; return `<h3 ${getStyles("h3")}>${text}</h3>`;
default: default:
return `<h4 ${getStyles("h4")}>${text}</h4>`; return `<h4 ${getStyles("h4")}>${text}</h4>`;
} }
}; };
renderer.paragraph = (text) => { renderer.paragraph = (text) => {
if ( if (text.indexOf("<figure") != -1 && text.indexOf("<img") != -1) {
text.indexOf("<figure") != -1 && return text;
text.indexOf("<img") != -1 }
) { return text.replace(/ /g, "") === ""
return text; ? ""
} : `<p ${getStyles("p")}>${text}</p>`;
return text.replace(/ /g, "") === "" };
? ""
: `<p ${getStyles("p")}>${text}</p>`;
};
renderer.blockquote = (text) => { renderer.blockquote = (text) => {
text = text.replace( text = text.replace(/<p.*?>/g, `<p ${getStyles("blockquote_p")}>`);
/<p.*?>/g, return `<blockquote ${getStyles("blockquote")}>${text}</blockquote>`;
`<p ${getStyles("blockquote_p")}>` };
); renderer.code = (text, lang) => {
return `<blockquote ${getStyles( text = text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
"blockquote" const codeLines = text
)}>${text}</blockquote>`; .split("\n")
}; .map(
renderer.code = (text, lang) => { (line) =>
text = text.replace(/</g, "&lt;").replace(/>/g, "&gt;"); `<code class="prettyprint"><span class="code-snippet_outer">${
const codeLines = text line || "<br>"
.split("\n") }</span></code>`
.map( );
(line) => const codeTheme = "github";
`<code class="prettyprint"><span class="code-snippet_outer">${ return `
line || "<br>"
}</span></code>`
);
const codeTheme = "github";
return `
<section class="code-snippet__${codeTheme}"> <section class="code-snippet__${codeTheme}">
<pre class="code__pre" data-lang="${lang}"> <pre class="code__pre" data-lang="${lang}">
${codeLines.join("")} ${codeLines.join("")}
</pre> </pre>
</section> </section>
`; `;
}; };
renderer.codespan = (text, lang) => renderer.codespan = (text, lang) =>
`<code ${getStyles("codespan")}>${text}</code>`; `<code ${getStyles("codespan")}>${text}</code>`;
renderer.listitem = (text) => renderer.listitem = (text) =>
`<span ${getStyles( `<span ${getStyles(
"listitem" "listitem"
)}><span style="margin-right: 10px;"><%s/></span>${text}</span>`; )}><span style="margin-right: 10px;"><%s/></span>${text}</span>`;
renderer.list = (text, ordered, start) => { renderer.list = (text, ordered, start) => {
text = text.replace(/<\/*p.*?>/g, ""); text = text.replace(/<\/*p.*?>/g, "");
let segments = text.split(`<%s/>`); let segments = text.split(`<%s/>`);
if (!ordered) { if (!ordered) {
text = segments.join("•"); text = segments.join("•");
return `<p ${getStyles("ul")}>${text}</p>`; return `<p ${getStyles("ul")}>${text}</p>`;
} }
text = segments[0]; text = segments[0];
for (let i = 1; i < segments.length; i++) { for (let i = 1; i < segments.length; i++) {
text = text + i + "." + segments[i]; text = text + i + "." + segments[i];
} }
return `<p ${getStyles("ol")}>${text}</p>`; return `<p ${getStyles("ol")}>${text}</p>`;
}; };
renderer.image = (href, title, text) => { renderer.image = (href, title, text) => {
let subText = ""; let subText = "";
if (text) { if (text) {
subText = `<figcaption ${getStyles( subText = `<figcaption ${getStyles(
"figcaption" "figcaption"
)}>${text}</figcaption>`; )}>${text}</figcaption>`;
} }
let figureStyles = getStyles("figure"); let figureStyles = getStyles("figure");
let imgStyles = getStyles( let imgStyles = getStyles(ENV_STRETCH_IMAGE ? "image" : "image_org");
ENV_STRETCH_IMAGE ? "image" : "image_org" return `<figure ${figureStyles}><img ${imgStyles} src="${href}" title="${title}" alt="${text}"/>${subText}</figure>`;
); };
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")) {
renderer.link = (href, title, text) => { return `<a href="${href}" title="${title || text}" ${getStyles(
if (href.startsWith("https://mp.weixin.qq.com")) { "wx_link"
return `<a href="${href}" title="${ )}>${text}</a>`;
title || text }
}" ${getStyles("wx_link")}>${text}</a>`; if (href === text || !status) {
} return text;
if (href === text || !status) { }
return text; let ref = addFootnote(title || text, href);
} return `<span ${getStyles("link")}>${text}<sup>[${ref}]</sup></span>`;
let ref = addFootnote(title || text, href); };
return `<span ${getStyles( renderer.strong = (text) =>
"link" `<strong ${getStyles("strong")}>${text}</strong>`;
)}>${text}<sup>[${ref}]</sup></span>`; renderer.em = (text) =>
}; `<span style="font-style: italic;">${text}</span>`;
renderer.strong = (text) => renderer.table = (header, body) =>
`<strong ${getStyles("strong")}>${text}</strong>`; `<section style="padding:0 8px;"><table class="preview-table"><thead ${getStyles(
renderer.em = (text) => "thead"
`<span style="font-style: italic;">${text}</span>`; )}>${header}</thead><tbody>${body}</tbody></table></section>`;
renderer.table = (header, body) => renderer.tablecell = (text, flags) =>
`<section style="padding:0 8px;"><table class="preview-table"><thead ${getStyles( `<td ${getStyles("td")}>${text}</td>`;
"thead" renderer.hr = () =>
)}>${header}</thead><tbody>${body}</tbody></table></section>`; `<hr style="border-style: solid;border-width: 1px 0 0;border-color: rgba(0,0,0,0.1);-webkit-transform-origin: 0 0;-webkit-transform: scale(1, 0.5);transform-origin: 0 0;transform: scale(1, 0.5);">`;
renderer.tablecell = (text, flags) => return renderer;
`<td ${getStyles("td")}>${text}</td>`; };
renderer.hr = () => }
`<hr style="border-style: solid;border-width: 1px 0 0;border-color: rgba(0,0,0,0.1);-webkit-transform-origin: 0 0;-webkit-transform: scale(1, 0.5);transform-origin: 0 0;transform: scale(1, 0.5);">`;
return renderer;
};
}
} }
export default WxRenderer; export default WxRenderer;

View File

@ -1,178 +1,178 @@
export default { export default {
BASE: { BASE: {
"text-align": "left", "text-align": "left",
color: "#3f3f3f", color: "#3f3f3f",
"line-height": "1.75", "line-height": "1.75",
},
BASE_BLOCK: {
margin: "1em 8px",
},
block: {
// 一级标题样式
h1: {
"font-size": "1.2em",
"text-align": "center",
"font-weight": "bold",
display: "table",
margin: "2em auto 1em",
padding: "0 1em",
"border-bottom": "2px solid rgba(0, 152, 116, 0.9)",
}, },
BASE_BLOCK: {
margin: "1em 8px", // 二级标题样式
h2: {
"font-size": "1.2em",
"text-align": "center",
"font-weight": "bold",
display: "table",
margin: "4em auto 2em",
padding: "0 0.2em",
background: "rgba(0, 152, 116, 0.9)",
color: "#fff",
}, },
block: {
// 一级标题样式
h1: {
"font-size": "1.2em",
"text-align": "center",
"font-weight": "bold",
display: "table",
margin: "2em auto 1em",
padding: "0 1em",
"border-bottom": "2px solid rgba(0, 152, 116, 0.9)",
},
// 二级标题样式 // 三级标题样式
h2: { h3: {
"font-size": "1.2em", "font-weight": "bold",
"text-align": "center", "font-size": "1.1em",
"font-weight": "bold", margin: "2em 8px 0.75em 0",
display: "table", "line-height": "1.2",
margin: "4em auto 2em", "padding-left": "8px",
padding: "0 0.2em", "border-left": "3px solid rgba(0, 152, 116, 0.9)",
background: "rgba(0, 152, 116, 0.9)",
color: "#fff",
},
// 三级标题样式
h3: {
"font-weight": "bold",
"font-size": "1.1em",
margin: "2em 8px 0.75em 0",
"line-height": "1.2",
"padding-left": "8px",
"border-left": "3px solid rgba(0, 152, 116, 0.9)",
},
// 四级标题样式
h4: {
"font-weight": "bold",
"font-size": "1em",
margin: "2em 8px 0.5em",
color: "rgba(66, 185, 131, 0.9)",
},
// 段落样式
p: {
margin: "1.5em 8px",
"letter-spacing": "0.1em",
},
// 引用样式
blockquote: {
"font-style": "normal",
"border-left": "none",
padding: "1em",
"border-radius": "4px",
color: "#FEEEED",
background: "rgba(27,31,35,.05)",
margin: "2em 8px",
},
blockquote_p: {
"letter-spacing": "0.1em",
color: "rgb(80, 80, 80)",
"font-size": "1em",
display: "block",
},
code: {
"font-size": "80%",
overflow: "auto",
color: "#333",
"white-space": "pre",
background: "rgb(247, 247, 247)",
"border-radius": "2px",
padding: "10px",
"line-height": "1.5",
border: "1px solid rgb(236,236,236)",
margin: "20px 0",
},
image: {
"border-radius": "4px",
display: "block",
margin: "0.1em auto 0.5em",
width: "100% !important",
},
image_org: {
"border-radius": "4px",
display: "block",
},
ol: {
"margin-left": "0",
"padding-left": "1em",
},
ul: {
"margin-left": "0",
"padding-left": "1em",
"list-style": "circle",
},
footnotes: {
margin: "0.5em 8px",
"font-size": "80%",
},
figure: {
margin: "1.5em 8px",
},
}, },
inline: {
listitem: {
"text-indent": "-1em",
display: "block",
margin: "0.2em 8px",
},
codespan: { // 四级标题样式
"font-size": "90%", h4: {
"white-space": "pre", "font-weight": "bold",
color: "#d14", "font-size": "1em",
background: "rgba(27,31,35,.05)", margin: "2em 8px 0.5em",
padding: "3px 5px", color: "rgba(66, 185, 131, 0.9)",
"border-radius": "4px",
},
link: {
color: "#576b95",
},
wx_link: {
color: "#576b95",
"text-decoration": "none",
},
// 字体加粗样式
strong: {
color: "rgba(15, 76, 129, 0.9)",
"font-weight": "bold",
},
table: {
"border-collapse": "collapse",
"text-align": "center",
margin: "1em 8px",
},
thead: {
background: "rgba(0, 0, 0, 0.05)",
"font-weight": "bold",
},
td: {
border: "1px solid #dfdfdf",
padding: "0.25em 0.5em",
},
footnote: {
"font-size": "12px",
},
figcaption: {
"text-align": "center",
color: "#888",
"font-size": "0.8em",
},
}, },
// 段落样式
p: {
margin: "1.5em 8px",
"letter-spacing": "0.1em",
},
// 引用样式
blockquote: {
"font-style": "normal",
"border-left": "none",
padding: "1em",
"border-radius": "4px",
color: "#FEEEED",
background: "rgba(27,31,35,.05)",
margin: "2em 8px",
},
blockquote_p: {
"letter-spacing": "0.1em",
color: "rgb(80, 80, 80)",
"font-size": "1em",
display: "block",
},
code: {
"font-size": "80%",
overflow: "auto",
color: "#333",
"white-space": "pre",
background: "rgb(247, 247, 247)",
"border-radius": "2px",
padding: "10px",
"line-height": "1.5",
border: "1px solid rgb(236,236,236)",
margin: "20px 0",
},
image: {
"border-radius": "4px",
display: "block",
margin: "0.1em auto 0.5em",
width: "100% !important",
},
image_org: {
"border-radius": "4px",
display: "block",
},
ol: {
"margin-left": "0",
"padding-left": "1em",
},
ul: {
"margin-left": "0",
"padding-left": "1em",
"list-style": "circle",
},
footnotes: {
margin: "0.5em 8px",
"font-size": "80%",
},
figure: {
margin: "1.5em 8px",
},
},
inline: {
listitem: {
"text-indent": "-1em",
display: "block",
margin: "0.2em 8px",
},
codespan: {
"font-size": "90%",
"white-space": "pre",
color: "#d14",
background: "rgba(27,31,35,.05)",
padding: "3px 5px",
"border-radius": "4px",
},
link: {
color: "#576b95",
},
wx_link: {
color: "#576b95",
"text-decoration": "none",
},
// 字体加粗样式
strong: {
color: "rgba(15, 76, 129, 0.9)",
"font-weight": "bold",
},
table: {
"border-collapse": "collapse",
"text-align": "center",
margin: "1em 8px",
},
thead: {
background: "rgba(0, 0, 0, 0.05)",
"font-weight": "bold",
},
td: {
border: "1px solid #dfdfdf",
padding: "0.25em 0.5em",
},
footnote: {
"font-size": "12px",
},
figcaption: {
"text-align": "center",
color: "#888",
"font-size": "0.8em",
},
},
}; };

View File

@ -1,269 +1,265 @@
export function utf16to8(str) { export function utf16to8(str) {
var out, i, len, c; var out, i, len, c;
out = ""; out = "";
len = str.length; len = str.length;
for (i = 0; i < len; i++) { for (i = 0; i < len; i++) {
c = str.charCodeAt(i); c = str.charCodeAt(i);
if (c >= 0x0001 && c <= 0x007f) { if (c >= 0x0001 && c <= 0x007f) {
out += str.charAt(i); out += str.charAt(i);
} else if (c > 0x07ff) { } else if (c > 0x07ff) {
out += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f)); out += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f));
out += String.fromCharCode(0x80 | ((c >> 6) & 0x3f)); out += String.fromCharCode(0x80 | ((c >> 6) & 0x3f));
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));
} else { } else {
out += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f)); out += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f));
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f));
}
} }
return out; }
return out;
} }
export function utf8to16(str) { export function utf8to16(str) {
var out, i, len, c; var out, i, len, c;
var char2, char3; var char2, char3;
out = ""; out = "";
len = str.length; len = str.length;
i = 0; i = 0;
while (i < len) { while (i < len) {
c = str.charCodeAt(i++); c = str.charCodeAt(i++);
switch (c >> 4) { switch (c >> 4) {
case 0: case 0:
case 1: case 1:
case 2: case 2:
case 3: case 3:
case 4: case 4:
case 5: case 5:
case 6: case 6:
case 7: case 7:
// 0xxxxxxx // 0xxxxxxx
out += str.charAt(i - 1); out += str.charAt(i - 1);
break; break;
case 12: case 12:
case 13: case 13:
// 110x xxxx 10xx xxxx // 110x xxxx 10xx xxxx
char2 = str.charCodeAt(i++); char2 = str.charCodeAt(i++);
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f)); out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
break; break;
case 14: case 14:
// 1110 xxxx 10xx xxxx 10xx xxxx // 1110 xxxx 10xx xxxx 10xx xxxx
char2 = str.charCodeAt(i++); char2 = str.charCodeAt(i++);
char3 = str.charCodeAt(i++); char3 = str.charCodeAt(i++);
out += String.fromCharCode( out += String.fromCharCode(
((c & 0x0f) << 12) | ((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
((char2 & 0x3f) << 6) | );
((char3 & 0x3f) << 0) break;
);
break;
}
} }
return out; }
return out;
} }
const base64EncodeChars = const base64EncodeChars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
const base64DecodeChars = new Array( const base64DecodeChars = new Array(
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
62, 62,
-1, -1,
-1, -1,
-1, -1,
63, 63,
52, 52,
53, 53,
54, 54,
55, 55,
56, 56,
57, 57,
58, 58,
59, 59,
60, 60,
61, 61,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
0, 0,
1, 1,
2, 2,
3, 3,
4, 4,
5, 5,
6, 6,
7, 7,
8, 8,
9, 9,
10, 10,
11, 11,
12, 12,
13, 13,
14, 14,
15, 15,
16, 16,
17, 17,
18, 18,
19, 19,
20, 20,
21, 21,
22, 22,
23, 23,
24, 24,
25, 25,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
26, 26,
27, 27,
28, 28,
29, 29,
30, 30,
31, 31,
32, 32,
33, 33,
34, 34,
35, 35,
36, 36,
37, 37,
38, 38,
39, 39,
40, 40,
41, 41,
42, 42,
43, 43,
44, 44,
45, 45,
46, 46,
47, 47,
48, 48,
49, 49,
50, 50,
51, 51,
-1, -1,
-1, -1,
-1, -1,
-1, -1,
-1 -1
); );
export function base64encode(str) { export function base64encode(str) {
var out, i, len; var out, i, len;
var c1, c2, c3; var c1, c2, c3;
len = str.length; len = str.length;
i = 0; i = 0;
out = ""; out = "";
while (i < len) { while (i < len) {
c1 = str.charCodeAt(i++) & 0xff; c1 = str.charCodeAt(i++) & 0xff;
if (i == len) { if (i == len) {
out += base64EncodeChars.charAt(c1 >> 2); out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt((c1 & 0x3) << 4); out += base64EncodeChars.charAt((c1 & 0x3) << 4);
out += "=="; out += "==";
break; break;
}
c2 = str.charCodeAt(i++);
if (i == len) {
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(
((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4)
);
out += base64EncodeChars.charAt((c2 & 0xf) << 2);
out += "=";
break;
}
c3 = str.charCodeAt(i++);
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4));
out += base64EncodeChars.charAt(((c2 & 0xf) << 2) | ((c3 & 0xc0) >> 6));
out += base64EncodeChars.charAt(c3 & 0x3f);
} }
return out; c2 = str.charCodeAt(i++);
if (i == len) {
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4));
out += base64EncodeChars.charAt((c2 & 0xf) << 2);
out += "=";
break;
}
c3 = str.charCodeAt(i++);
out += base64EncodeChars.charAt(c1 >> 2);
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4));
out += base64EncodeChars.charAt(((c2 & 0xf) << 2) | ((c3 & 0xc0) >> 6));
out += base64EncodeChars.charAt(c3 & 0x3f);
}
return out;
} }
export function base64decode(str) { export function base64decode(str) {
var c1, c2, c3, c4; var c1, c2, c3, c4;
var i, len, out; var i, len, out;
len = str.length; len = str.length;
i = 0; i = 0;
out = ""; out = "";
while (i < len) { while (i < len) {
/* c1 */ /* c1 */
do { do {
c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff]; c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
} while (i < len && c1 == -1); } while (i < len && c1 == -1);
if (c1 == -1) break; if (c1 == -1) break;
/* c2 */ /* c2 */
do { do {
c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff]; c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff];
} while (i < len && c2 == -1); } while (i < len && c2 == -1);
if (c2 == -1) break; if (c2 == -1) break;
out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4)); out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4));
/* c3 */ /* c3 */
do { do {
c3 = str.charCodeAt(i++) & 0xff; c3 = str.charCodeAt(i++) & 0xff;
if (c3 == 61) return out; if (c3 == 61) return out;
c3 = base64DecodeChars[c3]; c3 = base64DecodeChars[c3];
} while (i < len && c3 == -1); } while (i < len && c3 == -1);
if (c3 == -1) break; if (c3 == -1) break;
out += String.fromCharCode(((c2 & 0xf) << 4) | ((c3 & 0x3c) >> 2)); out += String.fromCharCode(((c2 & 0xf) << 4) | ((c3 & 0x3c) >> 2));
/* c4 */ /* c4 */
do { do {
c4 = str.charCodeAt(i++) & 0xff; c4 = str.charCodeAt(i++) & 0xff;
if (c4 == 61) return out; if (c4 == 61) return out;
c4 = base64DecodeChars[c4]; c4 = base64DecodeChars[c4];
} while (i < len && c4 == -1); } while (i < len && c4 == -1);
if (c4 == -1) break; if (c4 == -1) break;
out += String.fromCharCode(((c3 & 0x03) << 6) | c4); out += String.fromCharCode(((c3 & 0x03) << 6) | c4);
} }
return out; return out;
} }
export function safe64(base64) { export function safe64(base64) {
base64 = base64.replace(/\+/g, "-"); base64 = base64.replace(/\+/g, "-");
base64 = base64.replace(/\//g, "_"); base64 = base64.replace(/\//g, "_");
return base64; return base64;
} }

View File

@ -5,21 +5,7 @@ import prettierCss from "prettier/parser-postcss";
// 设置自定义颜色 // 设置自定义颜色
export function setColorWithTemplate(template) { export function setColorWithTemplate(template) {
return function (color) { return function (color) {
let custom_theme = JSON.parse(JSON.stringify(template));
custom_theme.block.h1["border-bottom"] = `2px solid ${color}`;
custom_theme.block.h2["background"] = color;
custom_theme.block.h3["border-left"] = `3px solid ${color}`;
custom_theme.block.h4["color"] = color;
custom_theme.inline.strong["color"] = color;
return custom_theme;
};
}
export const setColorWithCustomTemplate = function setColorWithCustomTemplate(
template,
color
) {
let custom_theme = JSON.parse(JSON.stringify(template)); let custom_theme = JSON.parse(JSON.stringify(template));
custom_theme.block.h1["border-bottom"] = `2px solid ${color}`; custom_theme.block.h1["border-bottom"] = `2px solid ${color}`;
custom_theme.block.h2["background"] = color; custom_theme.block.h2["background"] = color;
@ -27,69 +13,83 @@ export const setColorWithCustomTemplate = function setColorWithCustomTemplate(
custom_theme.block.h4["color"] = color; custom_theme.block.h4["color"] = color;
custom_theme.inline.strong["color"] = color; custom_theme.inline.strong["color"] = color;
return custom_theme; return custom_theme;
};
}
export const setColorWithCustomTemplate = function setColorWithCustomTemplate(
template,
color
) {
let custom_theme = JSON.parse(JSON.stringify(template));
custom_theme.block.h1["border-bottom"] = `2px solid ${color}`;
custom_theme.block.h2["background"] = color;
custom_theme.block.h3["border-left"] = `3px solid ${color}`;
custom_theme.block.h4["color"] = color;
custom_theme.inline.strong["color"] = color;
return custom_theme;
}; };
// 设置自定义字体大小 // 设置自定义字体大小
export function setFontSizeWithTemplate(template) { export function setFontSizeWithTemplate(template) {
return function (fontSize) { return function (fontSize) {
let custom_theme = JSON.parse(JSON.stringify(template)); let custom_theme = JSON.parse(JSON.stringify(template));
custom_theme.block.h1["font-size"] = `${fontSize * 1.14}px`; custom_theme.block.h1["font-size"] = `${fontSize * 1.14}px`;
custom_theme.block.h2["font-size"] = `${fontSize * 1.1}px`; custom_theme.block.h2["font-size"] = `${fontSize * 1.1}px`;
custom_theme.block.h3["font-size"] = `${fontSize}px`; custom_theme.block.h3["font-size"] = `${fontSize}px`;
custom_theme.block.h4["font-size"] = `${fontSize}px`; custom_theme.block.h4["font-size"] = `${fontSize}px`;
return custom_theme; return custom_theme;
}; };
} }
export const setColor = setColorWithTemplate(default_theme); export const setColor = setColorWithTemplate(default_theme);
export const setFontSize = setFontSizeWithTemplate(default_theme); export const setFontSize = setFontSizeWithTemplate(default_theme);
export function customCssWithTemplate(jsonString, color, theme) { export function customCssWithTemplate(jsonString, color, theme) {
let custom_theme = JSON.parse(JSON.stringify(theme)); let custom_theme = JSON.parse(JSON.stringify(theme));
// block // block
custom_theme.block.h1["border-bottom"] = `2px solid ${color}`; custom_theme.block.h1["border-bottom"] = `2px solid ${color}`;
custom_theme.block.h2["background"] = color; custom_theme.block.h2["background"] = color;
custom_theme.block.h3["border-left"] = `3px solid ${color}`; custom_theme.block.h3["border-left"] = `3px solid ${color}`;
custom_theme.block.h4["color"] = color; custom_theme.block.h4["color"] = color;
custom_theme.inline.strong["color"] = color; custom_theme.inline.strong["color"] = color;
custom_theme.block.h1 = Object.assign(custom_theme.block.h1, jsonString.h1); custom_theme.block.h1 = Object.assign(custom_theme.block.h1, jsonString.h1);
custom_theme.block.h2 = Object.assign(custom_theme.block.h2, jsonString.h2); custom_theme.block.h2 = Object.assign(custom_theme.block.h2, jsonString.h2);
custom_theme.block.h3 = Object.assign(custom_theme.block.h3, jsonString.h3); custom_theme.block.h3 = Object.assign(custom_theme.block.h3, jsonString.h3);
custom_theme.block.h4 = Object.assign(custom_theme.block.h4, jsonString.h4); custom_theme.block.h4 = Object.assign(custom_theme.block.h4, jsonString.h4);
custom_theme.block.p = Object.assign(custom_theme.block.p, jsonString.p); custom_theme.block.p = Object.assign(custom_theme.block.p, jsonString.p);
custom_theme.block.blockquote = Object.assign( custom_theme.block.blockquote = Object.assign(
custom_theme.block.blockquote, custom_theme.block.blockquote,
jsonString.blockquote jsonString.blockquote
); );
custom_theme.block.blockquote_p = Object.assign( custom_theme.block.blockquote_p = Object.assign(
custom_theme.block.blockquote_p, custom_theme.block.blockquote_p,
jsonString.blockquote_p jsonString.blockquote_p
); );
custom_theme.block.image = Object.assign( custom_theme.block.image = Object.assign(
custom_theme.block.image, custom_theme.block.image,
jsonString.image jsonString.image
); );
// inline // inline
custom_theme.inline.strong = Object.assign( custom_theme.inline.strong = Object.assign(
custom_theme.inline.strong, custom_theme.inline.strong,
jsonString.strong jsonString.strong
); );
custom_theme.inline.codespan = Object.assign( custom_theme.inline.codespan = Object.assign(
custom_theme.inline.codespan, custom_theme.inline.codespan,
jsonString.codespan jsonString.codespan
); );
custom_theme.inline.link = Object.assign( custom_theme.inline.link = Object.assign(
custom_theme.inline.link, custom_theme.inline.link,
jsonString.link jsonString.link
); );
custom_theme.inline.wx_link = Object.assign( custom_theme.inline.wx_link = Object.assign(
custom_theme.inline.wx_link, custom_theme.inline.wx_link,
jsonString.wx_link jsonString.wx_link
); );
return custom_theme; return custom_theme;
} }
/** /**
@ -98,81 +98,77 @@ export function customCssWithTemplate(jsonString, color, theme) {
* @param {css字符串} css * @param {css字符串} css
*/ */
export function css2json(css) { export function css2json(css) {
// 移除CSS所有注释 // 移除CSS所有注释
let open, close; let open, close;
while ( while (
(open = css.indexOf("/*")) !== -1 && (open = css.indexOf("/*")) !== -1 &&
(close = css.indexOf("*/")) !== -1 (close = css.indexOf("*/")) !== -1
) { ) {
css = css.substring(0, open) + css.substring(close + 2); css = css.substring(0, open) + css.substring(close + 2);
}
// 初始化返回值
let json = {};
while (css.length > 0 && css.indexOf("{") !== -1 && css.indexOf("}") !== -1) {
// 存储第一个左/右花括号的下标
const lbracket = css.indexOf("{");
const rbracket = css.indexOf("}");
// 第一步将声明转换为Object
// `font: 'Times New Roman' 1em; color: #ff0000; margin-top: 1em;`
// ==>
// `{"font": "'Times New Roman' 1em", "color": "#ff0000", "margin-top": "1em"}`
// 辅助方法将array转为object
function toObject(array) {
let ret = {};
array.forEach((e) => {
const index = e.indexOf(":");
const property = e.substring(0, index).trim();
const value = e.substring(index + 1).trim();
ret[property] = value;
});
return ret;
} }
// 初始化返回值 // 切割声明块并移除空白符,然后放入数组中
let json = {}; let declarations = css
.substring(lbracket + 1, rbracket)
.split(";")
.map((e) => e.trim())
.filter((e) => e.length > 0); // 移除所有""空值
while ( // 转为Object对象
css.length > 0 && declarations = toObject(declarations);
css.indexOf("{") !== -1 &&
css.indexOf("}") !== -1
) {
// 存储第一个左/右花括号的下标
const lbracket = css.indexOf("{");
const rbracket = css.indexOf("}");
// 第一步将声明转换为Object // 第二步:选择器处理,每个选择器会与它对应的声明相关联,如:
// `font: 'Times New Roman' 1em; color: #ff0000; margin-top: 1em;` // `h1, p#bar {color: red}`
// ==> // ==>
// `{"font": "'Times New Roman' 1em", "color": "#ff0000", "margin-top": "1em"}` // {"h1": {color: red}, "p#bar": {color: red}}
// 辅助方法将array转为object let selectors = css
function toObject(array) { .substring(0, lbracket)
let ret = {}; // 以,切割,并移除空格:`"h1, p#bar, span.foo"` => ["h1", "p#bar", "span.foo"]
array.forEach((e) => { .split(",")
const index = e.indexOf(":"); .map((selector) => selector.trim());
const property = e.substring(0, index).trim();
const value = e.substring(index + 1).trim();
ret[property] = value;
});
return ret;
}
// 切割声明块并移除空白符,然后放入数组中 // 迭代赋值
let declarations = css selectors.forEach((selector) => {
.substring(lbracket + 1, rbracket) // 若不存在,则先初始化
.split(";") if (!json[selector]) json[selector] = {};
.map((e) => e.trim()) // 赋值到JSON
.filter((e) => e.length > 0); // 移除所有""空值 Object.keys(declarations).forEach((key) => {
json[selector][key] = declarations[key];
});
});
// 转为Object对象 // 继续下个声明块
declarations = toObject(declarations); css = css.slice(rbracket + 1).trim();
}
// 第二步:选择器处理,每个选择器会与它对应的声明相关联,如: // 返回JSON形式的结果串
// `h1, p#bar {color: red}` return json;
// ==>
// {"h1": {color: red}, "p#bar": {color: red}}
let selectors = css
.substring(0, lbracket)
// 以,切割,并移除空格:`"h1, p#bar, span.foo"` => ["h1", "p#bar", "span.foo"]
.split(",")
.map((selector) => selector.trim());
// 迭代赋值
selectors.forEach((selector) => {
// 若不存在,则先初始化
if (!json[selector]) json[selector] = {};
// 赋值到JSON
Object.keys(declarations).forEach((key) => {
json[selector][key] = declarations[key];
});
});
// 继续下个声明块
css = css.slice(rbracket + 1).trim();
}
// 返回JSON形式的结果串
return json;
} }
/** /**
@ -181,12 +177,12 @@ export function css2json(css) {
* @param {*} name * @param {*} name
*/ */
export function saveEditorContent(editor, name) { export function saveEditorContent(editor, name) {
const content = editor.getValue(0); const content = editor.getValue(0);
if (content) { if (content) {
localStorage.setItem(name, content); localStorage.setItem(name, content);
} else { } else {
localStorage.removeItem(name); localStorage.removeItem(name);
} }
} }
/** /**
@ -194,11 +190,11 @@ export function saveEditorContent(editor, name) {
* @param {文档内容} content * @param {文档内容} content
*/ */
export function formatDoc(content) { export function formatDoc(content) {
const doc = prettier.format(content, { const doc = prettier.format(content, {
parser: "markdown", parser: "markdown",
plugins: [prettierMarkdown], plugins: [prettierMarkdown],
}); });
return doc; return doc;
} }
/** /**
@ -206,20 +202,20 @@ export function formatDoc(content) {
* @param {css内容}} content * @param {css内容}} content
*/ */
export function formatCss(content) { export function formatCss(content) {
const doc = prettier.format(content, { const doc = prettier.format(content, {
parser: "css", parser: "css",
plugins: [prettierCss], plugins: [prettierCss],
}); });
return doc; return doc;
} }
export function fixCodeWhiteSpace(value = "pre") { export function fixCodeWhiteSpace(value = "pre") {
const preDomList = document.getElementsByClassName("code__pre"); const preDomList = document.getElementsByClassName("code__pre");
if (preDomList.length > 0) { if (preDomList.length > 0) {
preDomList.forEach((pre) => { preDomList.forEach((pre) => {
pre.style.whiteSpace = value; pre.style.whiteSpace = value;
}); });
} }
} }
/** /**
@ -227,16 +223,16 @@ export function fixCodeWhiteSpace(value = "pre") {
* @param {文档内容} doc * @param {文档内容} doc
*/ */
export function downloadMD(doc) { export function downloadMD(doc) {
let downLink = document.createElement("a"); let downLink = document.createElement("a");
downLink.download = "content.md"; downLink.download = "content.md";
downLink.style.display = "none"; downLink.style.display = "none";
let blob = new Blob([doc]); let blob = new Blob([doc]);
downLink.href = URL.createObjectURL(blob); downLink.href = URL.createObjectURL(blob);
document.body.appendChild(downLink); document.body.appendChild(downLink);
downLink.click(); downLink.click();
document.body.removeChild(downLink); document.body.removeChild(downLink);
} }
/** /**
@ -246,50 +242,50 @@ export function downloadMD(doc) {
* @param {*} cols * @param {*} cols
*/ */
export function createTable({ data, rows, cols }) { export function createTable({ data, rows, cols }) {
let table = ""; let table = "";
let currRow = []; let currRow = [];
for (let i = 0; i < rows + 2; ++i) { for (let i = 0; i < rows + 2; ++i) {
table += "|\t"; table += "|\t";
currRow = []; currRow = [];
for (let j = 0; j < cols; ++j) { for (let j = 0; j < cols; ++j) {
const rowIdx = i > 1 ? i - 1 : i; const rowIdx = i > 1 ? i - 1 : i;
i === 1 i === 1
? currRow.push("---\t") ? currRow.push("---\t")
: currRow.push(data[`k_${rowIdx}_${j}`] || ""); : currRow.push(data[`k_${rowIdx}_${j}`] || "");
}
table += currRow.join("\t|\t");
table += "\t|\n";
} }
table += currRow.join("\t|\t");
table += "\t|\n";
}
return table; return table;
} }
export const toBase64 = (file) => export const toBase64 = (file) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.readAsDataURL(file); reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result.split(",").pop()); reader.onload = () => resolve(reader.result.split(",").pop());
reader.onerror = (error) => reject(error); reader.onerror = (error) => reject(error);
}); });
export function checkImage(file) { export function checkImage(file) {
// check filename suffix // check filename suffix
const isValidSuffix = /\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name); const isValidSuffix = /\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name);
if (!isValidSuffix) { if (!isValidSuffix) {
return { return {
ok: false, ok: false,
msg: "请上传 JPG/PNG/GIF 格式的图片", msg: "请上传 JPG/PNG/GIF 格式的图片",
}; };
} }
// check file size // check file size
const maxSize = 5; const maxSize = 5;
const isLt5M = file.size / 1024 / 1024 <= maxSize; const isLt5M = file.size / 1024 / 1024 <= maxSize;
if (!isLt5M) { if (!isLt5M) {
return { return {
ok: false, ok: false,
msg: `由于公众号限制,图片大小不能超过 ${maxSize}M`, msg: `由于公众号限制,图片大小不能超过 ${maxSize}M`,
}; };
} }
return { ok: true }; return { ok: true };
} }

View File

@ -1,57 +1,57 @@
<template> <template>
<el-dialog <el-dialog
title="关于" title="关于"
class="about__dialog" class="about__dialog"
:visible="value" :visible="value"
@close="$emit('input', false)" @close="$emit('input', false)"
width="30%" width="30%"
center center
> >
<div style="text-align: center"> <div style="text-align: center">
<h3>一款高度简洁的微信 Markdown 编辑器</h3> <h3>一款高度简洁的微信 Markdown 编辑器</h3>
</div> </div>
<div style="text-align: center; margin-top: 10px"> <div style="text-align: center; margin-top: 10px">
<p>扫码关注我的公众号原创技术文章第一时间推送</p> <p>扫码关注我的公众号原创技术文章第一时间推送</p>
<img <img
src="https://gitee.com/yanglbme/resource/raw/master/doocs-md/qrcode.png" src="https://gitee.com/yanglbme/resource/raw/master/doocs-md/qrcode.png"
style="width: 40%; display: block; margin: 20px auto 10px" style="width: 40%; display: block; margin: 20px auto 10px"
/> />
</div> </div>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button <el-button
type="primary" type="primary"
@click="onRedirect('https://github.com/doocs/md')" @click="onRedirect('https://github.com/doocs/md')"
plain plain
>GitHub 仓库</el-button >GitHub 仓库</el-button
> >
<el-button <el-button
type="primary" type="primary"
@click="onRedirect('https://gitee.com/doocs/md')" @click="onRedirect('https://gitee.com/doocs/md')"
plain plain
>Gitee 仓库</el-button >Gitee 仓库</el-button
> >
</span> </span>
</el-dialog> </el-dialog>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
value: { value: {
type: Boolean, type: Boolean,
default: false, default: false,
},
}, },
methods: { },
onRedirect(url) { methods: {
window.open(url); onRedirect(url) {
}, window.open(url);
}, },
},
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
/deep/ .el-dialog { /deep/ .el-dialog {
min-width: 420px; min-width: 420px;
} }
</style> </style>

View File

@ -1,194 +1,186 @@
<template> <template>
<el-container class="top is-dark"> <el-container class="top is-dark">
<!-- 图片上传 --> <div class="left-side">
<el-tooltip <!-- 图片上传 -->
:effect="effect" <el-tooltip :effect="effect" content="上传图片" placement="bottom-start">
content="上传图片" <i
placement="bottom-start" class="el-icon-upload"
size="medium"
@click="$emit('show-dialog-upload-img')"
></i>
</el-tooltip>
<!-- 下载文本文档 -->
<el-tooltip
class="header__item"
:effect="effect"
content="下载 Markdown 文档"
placement="bottom-start"
>
<i
class="el-icon-download"
size="medium"
@click="$emit('download')"
></i>
</el-tooltip>
<!-- 页面重置 -->
<el-tooltip
class="header__item"
:effect="effect"
content="重置页面"
placement="bottom-start"
>
<i
class="el-icon-refresh"
size="medium"
@click="showResetConfirm = true"
></i>
</el-tooltip>
<!-- 插入表格 -->
<el-tooltip
class="header__item header__item_last"
:effect="effect"
content="插入表格"
placement="bottom-start"
>
<i
class="el-icon-s-grid"
size="medium"
@click="$emit('show-dialog-form')"
></i>
</el-tooltip>
<el-select
v-model="selectFont"
size="mini"
placeholder="选择字体"
clearable
@change="fontChanged"
>
<el-option
v-for="font in config.builtinFonts"
:style="{ fontFamily: font.value }"
:key="font.value"
:label="font.label"
:value="font.value"
> >
<i <span class="select-item-left">{{ font.label }}</span>
class="el-icon-upload" <span class="select-item-right">Abc</span>
size="medium" </el-option>
@click="$emit('show-dialog-upload-img')" </el-select>
></i> <el-select
</el-tooltip> v-model="selectSize"
<!-- 下载文本文档 --> size="mini"
<el-tooltip placeholder="选择段落字号"
class="header__item" clearable
:effect="effect" @change="sizeChanged"
content="下载 Markdown 文档" >
placement="bottom-start" <el-option
v-for="size in config.sizeOption"
:key="size.value"
:label="size.label"
:value="size.value"
> >
<i <span class="select-item-left">{{ size.label }}</span>
class="el-icon-download" <span class="select-item-right">{{ size.desc }}</span>
size="medium" </el-option>
@click="$emit('download')" </el-select>
></i> <el-select
</el-tooltip> v-model="selectColor"
<!-- 页面重置 --> size="mini"
<el-tooltip placeholder="选择颜色"
class="header__item" clearable
:effect="effect" @change="colorChanged"
content="重置页面" >
placement="bottom-start" <el-option
v-for="color in config.colorOption"
:key="color.value"
:label="color.label"
:value="color.value"
> >
<i <span class="select-item-left">{{ color.label }}</span>
class="el-icon-refresh" <span class="select-item-right">{{ color.desc }}</span>
size="medium" </el-option>
@click="showResetConfirm = true" </el-select>
></i> <el-tooltip content="自定义颜色" :effect="effect" placement="top">
</el-tooltip> <el-color-picker
<!-- 插入表格 --> v-model="selectColor"
<el-tooltip size="mini"
class="header__item header__item_last" show-alpha
:effect="effect" @change="colorChanged"
content="插入表格" ></el-color-picker>
placement="bottom-start" </el-tooltip>
<el-tooltip
content="微信外链自动转为文末引用"
:effect="effect"
placement="top"
>
<el-switch
class="header__switch"
v-model="citeStatus"
active-color="#67c23a"
inactive-color="#dcdfe6"
@change="statusChanged"
> >
<i </el-switch>
class="el-icon-s-grid" </el-tooltip>
size="medium" </div>
@click="$emit('show-dialog-form')" <div class="right-side">
></i> <el-tooltip
</el-tooltip> class="item"
<el-form size="mini" class="ctrl" :inline="true"> :effect="effect"
<el-form-item> content="自定义CSS样式"
<el-select placement="left"
v-model="selectFont" >
size="mini"
placeholder="选择字体"
clearable
@change="fontChanged"
>
<el-option
v-for="font in config.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="selectSize"
size="mini"
placeholder="选择段落字号"
clearable
@change="sizeChanged"
>
<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-right">{{ size.desc }}</span>
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-select
v-model="selectColor"
size="mini"
placeholder="选择颜色"
clearable
@change="colorChanged"
>
<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-right">{{ color.desc }}</span>
</el-option>
</el-select>
</el-form-item>
<el-tooltip content="自定义颜色" :effect="effect" placement="top">
<el-color-picker
v-model="selectColor"
size="mini"
show-alpha
@change="colorChanged"
></el-color-picker>
</el-tooltip>
<el-tooltip
content="微信外链自动转为文末引用"
:effect="effect"
placement="top"
>
<el-switch
class="header__switch"
v-model="citeStatus"
active-color="#67c23a"
inactive-color="#dcdfe6"
@change="statusChanged"
>
</el-switch>
</el-tooltip>
</el-form>
<el-tooltip
class="item"
:effect="effect"
content="自定义CSS样式"
placement="left"
>
<el-button
:type="btnType"
plain
size="medium"
icon="el-icon-setting"
@click="customStyle"
></el-button>
</el-tooltip>
<el-button <el-button
:type="btnType" :type="btnType"
plain plain
size="medium" size="medium"
@click="copy" icon="el-icon-setting"
placement="bottom-start" @click="customStyle"
>复制</el-button ></el-button>
> </el-tooltip>
<el-button <el-button
:type="btnType" :type="btnType"
plain plain
size="medium" size="medium"
class="about" @click="copy"
@click="$emit('show-about-dialog')" placement="bottom-start"
>关于</el-button >复制</el-button
> >
<el-tooltip <el-button
:content="btnContent" :type="btnType"
:effect="effect" plain
placement="bottom-start" size="medium"
> class="about"
<div @click="$emit('show-about-dialog')"
class="mode__switch mode__switch_black" >关于</el-button
v-if="nightMode" >
@click="themeChanged" <el-tooltip
></div> :content="btnContent"
<div class="mode__switch" v-else @click="themeChanged"></div> :effect="effect"
</el-tooltip> placement="bottom-start"
<resetDialog >
:showResetConfirm="showResetConfirm" <div
@confirm="confirmReset" class="mode__switch mode__switch_black"
@close="cancelReset" v-if="nightMode"
/> @click="themeChanged"
</el-container> ></div>
<div class="mode__switch" v-else @click="themeChanged"></div>
</el-tooltip>
</div>
<resetDialog
:showResetConfirm="showResetConfirm"
@confirm="confirmReset"
@close="cancelReset"
/>
</el-container>
</template> </template>
<script> <script>
import { import {
downloadMD, downloadMD,
setFontSize, setFontSize,
fixCodeWhiteSpace, fixCodeWhiteSpace,
setColorWithCustomTemplate, setColorWithCustomTemplate,
} from "../../assets/scripts/util"; } from "../../assets/scripts/util";
import { solveWeChatImage, solveHtml } from "../../assets/scripts/converter"; import { solveWeChatImage, solveHtml } from "../../assets/scripts/converter";
import config from "../../assets/scripts/config"; import config from "../../assets/scripts/config";
@ -196,196 +188,226 @@ import DEFAULT_CSS_CONTENT from "../../assets/scripts/themes/default-theme-css";
import resetDialog from "./resetDialog"; import resetDialog from "./resetDialog";
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations } from "vuex";
export default { export default {
name: "editor-header", name: "editor-header",
data() { data() {
return { return {
config: config, config: config,
citeStatus: false, citeStatus: false,
showResetConfirm: false, showResetConfirm: false,
selectFont: "", selectFont: "",
selectSize: "", selectSize: "",
selectColor: "", selectColor: "",
selectCodeTheme: "github", selectCodeTheme: "github",
}; };
},
components: {
resetDialog,
},
computed: {
effect() {
return this.nightMode ? "dark" : "light";
}, },
components: { btnContent() {
resetDialog, return this.nightMode ? "浅色模式" : "暗黑模式";
}, },
computed: { btnType() {
effect() { return this.nightMode ? "default" : "primary";
return this.nightMode ? "dark" : "light";
},
btnContent() {
return this.nightMode ? "浅色模式" : "暗黑模式";
},
btnType() {
return this.nightMode ? "default" : "primary";
},
...mapState({
output: (state) => state.output,
editor: (state) => state.editor,
cssEditor: (state) => state.cssEditor,
currentFont: (state) => state.currentFont,
currentSize: (state) => state.currentSize,
currentColor: (state) => state.currentColor,
codeTheme: (state) => state.codeTheme,
nightMode: (state) => state.nightMode,
}),
}, },
methods: { ...mapState({
fontChanged(fonts) { output: (state) => state.output,
this.setWxRendererOptions({ editor: (state) => state.editor,
fonts: fonts, cssEditor: (state) => state.cssEditor,
}); currentFont: (state) => state.currentFont,
this.setCurrentFont(fonts); currentSize: (state) => state.currentSize,
this.$emit("refresh"); currentColor: (state) => state.currentColor,
}, codeTheme: (state) => state.codeTheme,
sizeChanged(size) { nightMode: (state) => state.nightMode,
let theme = setFontSize(size.replace("px", "")); }),
theme = setColorWithCustomTemplate(theme, this.currentColor); },
this.setWxRendererOptions({ methods: {
size: size, fontChanged(fonts) {
theme: theme, this.setWxRendererOptions({
}); fonts: fonts,
this.setCurrentSize(size); });
this.$emit("refresh"); this.setCurrentFont(fonts);
}, this.$emit("refresh");
colorChanged(color) { },
let theme = setFontSize(this.currentSize.replace("px", "")); sizeChanged(size) {
let theme = setFontSize(size.replace("px", ""));
theme = setColorWithCustomTemplate(theme, this.currentColor);
this.setWxRendererOptions({
size: size,
theme: theme,
});
this.setCurrentSize(size);
this.$emit("refresh");
},
colorChanged(color) {
let theme = setFontSize(this.currentSize.replace("px", ""));
theme = setColorWithCustomTemplate(theme, color); theme = setColorWithCustomTemplate(theme, color);
this.setWxRendererOptions({ this.setWxRendererOptions({
theme: theme, theme: theme,
}); });
this.setCurrentColor(color); this.setCurrentColor(color);
this.$emit("refresh"); this.$emit("refresh");
},
codeThemeChanged(theme) {
this.setCurrentCodeTheme(theme);
this.$emit("refresh");
},
statusChanged(val) {
this.setCiteStatus(val);
this.$emit("refresh");
},
//
copy(e) {
this.$emit("startCopy");
setTimeout(() => {
let clipboardDiv = document.getElementById("output");
solveWeChatImage();
fixCodeWhiteSpace();
solveHtml();
clipboardDiv.focus();
window.getSelection().removeAllRanges();
let range = document.createRange();
range.setStartBefore(clipboardDiv.firstChild);
range.setEndAfter(clipboardDiv.lastChild);
window.getSelection().addRange(range);
document.execCommand("copy");
window.getSelection().removeAllRanges();
fixCodeWhiteSpace("normal");
clipboardDiv.innerHTML = this.output;
//
this.$notify({
showClose: true,
message:
"已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴",
offset: 80,
duration: 1600,
type: "success",
});
this.$emit("refresh");
this.$emit("endCopy");
}, 350);
e.target.blur();
},
// CSS
async customStyle() {
this.$emit("showCssEditor");
this.$nextTick(() => {
if (!this.cssEditor) {
this.cssEditor.refresh();
}
});
setTimeout(() => {
this.cssEditor.refresh();
}, 50);
let flag = await localStorage.getItem("__css_content");
if (!flag) {
this.setCssEditorValue(DEFAULT_CSS_CONTENT);
}
},
//
confirmReset() {
localStorage.clear();
this.clearEditorToDefault();
this.editor.focus();
this.citeStatus = false;
this.statusChanged(false);
this.fontChanged(this.config.builtinFonts[0].value);
this.colorChanged(this.config.colorOption[0].value);
this.sizeChanged(this.config.sizeOption[2].value);
this.$emit("cssChanged");
this.selectFont = this.currentFont;
this.selectSize = this.currentSize;
this.selectColor = this.currentColor;
this.showResetConfirm = false;
},
cancelReset() {
this.showResetConfirm = false;
this.editor.focus();
},
...mapMutations([
"clearEditorToDefault",
"setCurrentColor",
"setCiteStatus",
"themeChanged",
"setCurrentFont",
"setCurrentSize",
"setCssEditorValue",
"setCurrentCodeTheme",
"setWxRendererOptions",
]),
}, },
mounted() { codeThemeChanged(theme) {
this.selectFont = this.currentFont; this.setCurrentCodeTheme(theme);
this.selectSize = this.currentSize; this.$emit("refresh");
this.selectColor = this.currentColor;
this.selectCodeTheme = this.codeTheme;
}, },
statusChanged(val) {
this.setCiteStatus(val);
this.$emit("refresh");
},
//
copy(e) {
this.$emit("startCopy");
setTimeout(() => {
let clipboardDiv = document.getElementById("output");
solveWeChatImage();
fixCodeWhiteSpace();
solveHtml();
clipboardDiv.focus();
window.getSelection().removeAllRanges();
let range = document.createRange();
range.setStartBefore(clipboardDiv.firstChild);
range.setEndAfter(clipboardDiv.lastChild);
window.getSelection().addRange(range);
document.execCommand("copy");
window.getSelection().removeAllRanges();
fixCodeWhiteSpace("normal");
clipboardDiv.innerHTML = this.output;
//
this.$notify({
showClose: true,
message: "已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴",
offset: 80,
duration: 1600,
type: "success",
});
this.$emit("refresh");
this.$emit("endCopy");
}, 350);
},
// CSS
async customStyle() {
this.$emit("showCssEditor");
this.$nextTick(() => {
if (!this.cssEditor) {
this.cssEditor.refresh();
}
});
setTimeout(() => {
this.cssEditor.refresh();
}, 50);
let flag = await localStorage.getItem("__css_content");
if (!flag) {
this.setCssEditorValue(DEFAULT_CSS_CONTENT);
}
},
//
confirmReset() {
localStorage.clear();
this.clearEditorToDefault();
this.editor.focus();
this.citeStatus = false;
this.statusChanged(false);
this.fontChanged(this.config.builtinFonts[0].value);
this.colorChanged(this.config.colorOption[0].value);
this.sizeChanged(this.config.sizeOption[2].value);
this.$emit("cssChanged");
this.selectFont = this.currentFont;
this.selectSize = this.currentSize;
this.selectColor = this.currentColor;
this.showResetConfirm = false;
},
cancelReset() {
this.showResetConfirm = false;
this.editor.focus();
},
...mapMutations([
"clearEditorToDefault",
"setCurrentColor",
"setCiteStatus",
"themeChanged",
"setCurrentFont",
"setCurrentSize",
"setCssEditorValue",
"setCurrentCodeTheme",
"setWxRendererOptions",
]),
},
mounted() {
this.selectFont = this.currentFont;
this.selectSize = this.currentSize;
this.selectColor = this.currentColor;
this.selectCodeTheme = this.codeTheme;
},
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.editor__header { .editor__header {
width: 100%; width: 100%;
} }
.header__item { .header__item {
margin: 0 3px; margin: 0 3px;
} }
.header__item_last { .header__item_last {
margin-right: 8px; margin-right: 8px;
} }
.header__switch { .header__switch {
margin-left: 8px; margin-left: 8px;
} }
.mode__switch { .mode__switch {
margin-left: 24px; margin-left: 24px;
width: 24px; margin-right: 24px;
height: 24px; width: 24px;
background: url("../../assets/images/night.png") no-repeat; height: 24px;
background-size: cover; background: url("../../assets/images/night.png") no-repeat;
transition: all 0.3s; background-size: cover;
transition: all 0.3s;
} }
.mode__switch_black { .mode__switch_black {
background: url("../../assets/images/light.png") no-repeat; background: url("../../assets/images/light.png") no-repeat;
background-size: cover; background-size: cover;
} }
.top { .top {
margin-right: 0; 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> </style>

View File

@ -1,57 +1,55 @@
<template> <template>
<el-dialog <el-dialog
title="插入表格" title="插入表格"
class="insert__dialog" class="insert__dialog"
:visible="value" :visible="value"
@close="$emit('input', false)" @close="$emit('input', false)"
border border
> >
<el-row class="tb-options" type="flex" align="middle" :gutter="10"> <el-row class="tb-options" type="flex" align="middle" :gutter="10">
<el-col> <el-col>
行数 行数
<el-input-number <el-input-number
v-model="rowNum" v-model="rowNum"
controls-position="right" controls-position="right"
:min="1" :min="1"
:max="100" :max="100"
size="small" size="small"
></el-input-number> ></el-input-number>
</el-col> </el-col>
<el-col> <el-col>
列数 列数
<el-input-number <el-input-number
v-model="colNum" v-model="colNum"
controls-position="right" controls-position="right"
:min="1" :min="1"
:max="100" :max="100"
size="small" size="small"
></el-input-number> ></el-input-number>
</el-col> </el-col>
</el-row> </el-row>
<table style="border-collapse: collapse" class="input-table"> <table style="border-collapse: collapse" class="input-table">
<tr <tr
:class="{ 'head-style': row === 1 }" :class="{ 'head-style': row === 1 }"
v-for="row in rowNum + 1" v-for="row in rowNum + 1"
:key="row" :key="row"
> >
<td v-for="col in colNum" :key="col"> <td v-for="col in colNum" :key="col">
<el-input <el-input
align="center" align="center"
v-model="tableData[`k_${row - 1}_${col - 1}`]" v-model="tableData[`k_${row - 1}_${col - 1}`]"
:placeholder="row === 1 ? '表头' : ''" :placeholder="row === 1 ? '表头' : ''"
/> />
</td> </td>
</tr> </tr>
</table> </table>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button :type="btnType" plain @click="$emit('input', false)" <el-button :type="btnType" plain @click="$emit('input', false)"
> </el-button > </el-button
> >
<el-button :type="btnType" @click="insertTable" plain <el-button :type="btnType" @click="insertTable" plain> </el-button>
> </el-button </div>
> </el-dialog>
</div>
</el-dialog>
</template> </template>
<script> <script>
@ -59,66 +57,66 @@ import config from "../../assets/scripts/config";
import { createTable } from "../../assets/scripts/util"; import { createTable } from "../../assets/scripts/util";
import { mapState, mapMutations } from "vuex"; import { mapState, mapMutations } from "vuex";
export default { export default {
props: { props: {
value: { value: {
type: Boolean, type: Boolean,
default: false, default: false,
},
}, },
data() { },
return { data() {
config: config, return {
rowNum: 3, config: config,
colNum: 3, rowNum: 3,
tableData: {}, colNum: 3,
}; tableData: {},
};
},
computed: {
btnType() {
return this.nightMode ? "default" : "primary";
}, },
computed: { ...mapState({
btnType() { nightMode: (state) => state.nightMode,
return this.nightMode ? "default" : "primary"; editor: (state) => state.editor,
}, }),
...mapState({ },
nightMode: (state) => state.nightMode, methods: {
editor: (state) => state.editor, //
}), insertTable() {
}, const cursor = this.editor.getCursor();
methods: { const table = createTable({
// data: this.tableData,
insertTable() { rows: this.rowNum,
const cursor = this.editor.getCursor(); cols: this.colNum,
const table = createTable({ });
data: this.tableData,
rows: this.rowNum,
cols: this.colNum,
});
this.tableData = {}; this.tableData = {};
this.rowNum = 3; this.rowNum = 3;
this.colNum = 3; this.colNum = 3;
this.editor.replaceSelection(`\n${table}\n`, "end"); this.editor.replaceSelection(`\n${table}\n`, "end");
this.$emit("input", false); this.$emit("input", false);
this.editorRefresh(); this.editorRefresh();
},
...mapMutations(["editorRefresh"]),
}, },
...mapMutations(["editorRefresh"]),
},
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
/deep/ .el-dialog { /deep/ .el-dialog {
width: 55%; width: 55%;
min-height: 375px; min-height: 375px;
min-width: 440px; min-width: 440px;
} }
.tb-options { .tb-options {
margin-bottom: 20px; margin-bottom: 20px;
} }
.input-table ::v-deep .el-input__inner { .input-table ::v-deep .el-input__inner {
border-radius: 0; border-radius: 0;
} }
.head-style /deep/ .el-input__inner { .head-style /deep/ .el-input__inner {
background-color: #f2f2f2; background-color: #f2f2f2;
} }
</style> </style>

View File

@ -1,57 +1,53 @@
<template> <template>
<el-dialog <el-dialog
title="提示" title="提示"
class="reset__dialog" class="reset__dialog"
:visible="showResetConfirm" :visible="showResetConfirm"
@close="$emit('close')" @close="$emit('close')"
> >
<div class="text"> <div class="text">此操作将丢失本地缓存的文本和自定义样式是否继续?</div>
此操作将丢失本地缓存的文本和自定义样式是否继续? <div slot="footer" class="dialog-footer">
</div> <el-button :type="btnType" plain @click="$emit('close')"> </el-button>
<div slot="footer" class="dialog-footer"> <el-button :type="btnType" @click="$emit('confirm')" plain
<el-button :type="btnType" plain @click="$emit('close')" > </el-button
> </el-button >
> </div>
<el-button :type="btnType" @click="$emit('confirm')" plain </el-dialog>
> </el-button
>
</div>
</el-dialog>
</template> </template>
<script> <script>
import { mapState } from "vuex"; import { mapState } from "vuex";
export default { export default {
props: { props: {
showResetConfirm: { showResetConfirm: {
type: Boolean, type: Boolean,
default: false, default: false,
},
}, },
computed: { },
btnType() { computed: {
return this.nightMode ? "default" : "primary"; btnType() {
}, return this.nightMode ? "default" : "primary";
...mapState({
nightMode: (state) => state.nightMode,
}),
}, },
...mapState({
nightMode: (state) => state.nightMode,
}),
},
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
/deep/ .el-dialog { /deep/ .el-dialog {
min-width: 440px; min-width: 440px;
} }
.reset__dialog { .reset__dialog {
text-align: center; text-align: center;
} }
.text { .text {
text-align: center; text-align: center;
} }
.dialog-footer { .dialog-footer {
text-align: center; text-align: center;
} }
</style> </style>

View File

@ -1,139 +1,139 @@
<template> <template>
<ul <ul
v-show="value" v-show="value"
id="menu" id="menu"
class="menu" class="menu"
:style="`left: ${left}px;top: ${top}px;`" :style="`left: ${left}px;top: ${top}px;`"
> >
<div class="menu__group" v-for="(menuItem, index) in menu" :key="index"> <div class="menu__group" v-for="(menuItem, index) in menu" :key="index">
<li <li
v-for="item of menuItem" v-for="item of menuItem"
:key="item.key" :key="item.key"
class="menu_item" class="menu_item"
@mousedown="onMouseDown(item.key)" @mousedown="onMouseDown(item.key)"
> >
<span>{{ item.text }}</span> <span>{{ item.text }}</span>
</li> </li>
</div> </div>
</ul> </ul>
</template> </template>
<script> <script>
export default { export default {
props: { props: {
value: { value: {
type: Boolean, type: Boolean,
default: false, default: false,
},
top: {
type: Number,
default: 0,
},
left: {
type: Number,
default: 0,
},
}, },
data() { top: {
return { type: Number,
menu: [ default: 0,
[
{
text: "上传图片",
key: "insertPic",
},
{
text: "插入表格",
key: "insertTable",
},
{
text: "页面重置",
key: "pageReset",
},
],
[
{
text: "下载 Markdown 文档",
key: "download",
},
{
text: "格式化 Markdown 文档",
key: "formatMarkdown",
},
],
],
};
}, },
methods: { left: {
closeCB() { type: Number,
this.$emit("input", false); default: 0,
},
onMouseDown(key) {
this.$emit("menuTick", key);
this.$emit("closeMenu", false);
},
}, },
},
data() {
return {
menu: [
[
{
text: "上传图片",
key: "insertPic",
},
{
text: "插入表格",
key: "insertTable",
},
{
text: "页面重置",
key: "pageReset",
},
],
[
{
text: "下载 Markdown 文档",
key: "download",
},
{
text: "格式化 Markdown 文档",
key: "formatMarkdown",
},
],
],
};
},
methods: {
closeCB() {
this.$emit("input", false);
},
onMouseDown(key) {
this.$emit("menuTick", key);
this.$emit("closeMenu", false);
},
},
}; };
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.menu { .menu {
position: absolute; position: absolute;
border-radius: 4px; border-radius: 4px;
background-color: #ffffff; 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); box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
z-index: 9999; z-index: 9999;
} }
.menu_item { .menu_item {
margin-top: 10px; margin-top: 10px;
min-width: 200px; min-width: 200px;
font-size: 12px; font-size: 12px;
line-height: 20px; line-height: 20px;
color: #333333; color: #333333;
cursor: pointer; cursor: pointer;
&:first-of-type { &:first-of-type {
margin-top: 0; margin-top: 0;
} }
&:hover { &:hover {
background: #f0f0f0; background: #f0f0f0;
} }
span, span,
.btn-upload { .btn-upload {
display: inline-block; display: inline-block;
padding: 4px 0; padding: 4px 0;
padding-left: 24px; padding-left: 24px;
width: 100%; width: 100%;
} }
.btn-upload { .btn-upload {
margin: 0; margin: 0;
border: none; border: none;
outline: none; outline: none;
background: transparent; background: transparent;
} }
.btn-upload:hover { .btn-upload:hover {
background: #aaaaaa; background: #aaaaaa;
} }
::v-deep .el-upload { ::v-deep .el-upload {
width: 100%; width: 100%;
} }
} }
.menu__group { .menu__group {
padding-top: 6px; padding-top: 6px;
padding-bottom: 6px; padding-bottom: 6px;
border-bottom: 1px solid #eeeeee; border-bottom: 1px solid #eeeeee;
&:last-of-type { &:last-of-type {
border-bottom: none; border-bottom: none;
} }
} }
li:hover { li:hover {
background-color: #1790ff; background-color: #1790ff;
} }
li { li {
font-size: 15px; font-size: 15px;
list-style: none; list-style: none;
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +1,47 @@
<template> <template>
<div class="loading" id="loading"> <div class="loading" id="loading">
<div class="loading-wrapper"> <div class="loading-wrapper">
<div class="loading-anim"></div> <div class="loading-anim"></div>
<div class="loading-text">致力于让 Markdown 编辑更简单</div> <div class="loading-text">致力于让 Markdown 编辑更简单</div>
</div>
</div> </div>
</div>
</template> </template>
<script></script> <script></script>
<style lang="less" scoped> <style lang="less" scoped>
.loading-wrapper { .loading-wrapper {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
transform: translateX(-50%) translateY(-50%); transform: translateX(-50%) translateY(-50%);
} }
.loading { .loading {
text-align: center; text-align: center;
position: fixed; position: fixed;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
z-index: 99999; z-index: 99999;
background-color: #f2f2f2; background-color: #f2f2f2;
} }
.loading_night { .loading_night {
background-color: #303133; background-color: #303133;
} }
.loading-text { .loading-text {
font-size: 18px; font-size: 18px;
font-weight: bold; font-weight: bold;
margin-top: 26px; margin-top: 26px;
color: #303133; color: #303133;
} }
.loading-anim { .loading-anim {
display: inline-block; display: inline-block;
width: 100px; width: 100px;
height: 100px; height: 100px;
background: url("../assets/images/favicon.png") no-repeat; background: url("../assets/images/favicon.png") no-repeat;
background-size: cover; background-size: cover;
} }
</style> </style>

View File

@ -1,5 +1,5 @@
import Vue from "vue"; import Vue from "vue";
import App from "./App.vue"; import App from "./App";
import store from "./store"; import store from "./store";
import ElementUI from "element-ui"; import ElementUI from "element-ui";
import "element-ui/lib/theme-chalk/index.css"; 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/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 "./assets/less/theme.less";
Vue.use(ElementUI); Vue.use(ElementUI);
Vue.config.productionTip = false; Vue.config.productionTip = false;
new Vue({ App.mpType = "app";
store,
render: (h) => h(App), const app = new Vue({
}).$mount("#app"); store,
...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

@ -1,22 +1,22 @@
import Vue from "vue"; import Vue from "vue";
import { import {
Container, Container,
Header, Header,
Upload, Upload,
Tooltip, Tooltip,
Form, Form,
FormItem, FormItem,
Select, Select,
Option, Option,
ColorPicker, ColorPicker,
Switch, Switch,
Button, Button,
Main, Main,
Col, Col,
Row, Row,
Dialog, Dialog,
Loading, Loading,
Message, Message,
} from "element-ui"; } from "element-ui";
Vue.use(Container); Vue.use(Container);

View File

@ -11,154 +11,147 @@ import { setColor, formatDoc, formatCss } from "../assets/scripts/util";
Vue.use(Vuex); Vue.use(Vuex);
const state = { const state = {
wxRenderer: null, wxRenderer: null,
output: "", output: "",
html: "", html: "",
editor: null, editor: null,
cssEditor: null, cssEditor: null,
currentFont: "", currentFont: "",
currentSize: "", currentSize: "",
currentColor: "", currentColor: "",
citeStatus: 0, citeStatus: 0,
nightMode: false, nightMode: false,
codeTheme: "github", codeTheme: "github",
rightClickMenuVisible: false, rightClickMenuVisible: false,
}; };
const mutations = { const mutations = {
setEditorValue(state, data) { setEditorValue(state, data) {
state.editor.setValue(data); state.editor.setValue(data);
}, },
setCssEditorValue(state, data) { setCssEditorValue(state, data) {
state.cssEditor.setValue(data); state.cssEditor.setValue(data);
}, },
setWxRendererOptions(state, data) { setWxRendererOptions(state, data) {
state.wxRenderer.setOptions(data); state.wxRenderer.setOptions(data);
}, },
setCiteStatus(state, data) { setCiteStatus(state, data) {
state.citeStatus = data; state.citeStatus = data;
localStorage.setItem("citeStatus", data); localStorage.setItem("citeStatus", data);
}, },
setCurrentFont(state, data) { setCurrentFont(state, data) {
state.currentFont = data; state.currentFont = data;
localStorage.setItem("fonts", data); localStorage.setItem("fonts", data);
}, },
setCurrentSize(state, data) { setCurrentSize(state, data) {
state.currentSize = data; state.currentSize = data;
localStorage.setItem("size", data); localStorage.setItem("size", data);
}, },
setCurrentColor(state, data) { setCurrentColor(state, data) {
state.currentColor = data; state.currentColor = data;
localStorage.setItem("color", data); localStorage.setItem("color", data);
}, },
setCurrentCodeTheme(state, data) { setCurrentCodeTheme(state, data) {
state.codeTheme = data; state.codeTheme = data;
localStorage.setItem("codeTheme", data); localStorage.setItem("codeTheme", data);
}, },
setRightClickMenuVisible(state, data) { setRightClickMenuVisible(state, data) {
state.rightClickMenuVisible = data; state.rightClickMenuVisible = data;
}, },
themeChanged(state) { themeChanged(state) {
state.nightMode = !state.nightMode; state.nightMode = !state.nightMode;
localStorage.setItem("nightMode", state.nightMode); localStorage.setItem("nightMode", state.nightMode);
}, },
initEditorState(state) { initEditorState(state) {
state.currentFont = state.currentFont =
localStorage.getItem("fonts") || config.builtinFonts[0].value; localStorage.getItem("fonts") || config.builtinFonts[0].value;
state.currentColor = state.currentColor =
localStorage.getItem("color") || config.colorOption[0].value; localStorage.getItem("color") || config.colorOption[0].value;
state.currentSize = state.currentSize =
localStorage.getItem("size") || config.sizeOption[2].value; localStorage.getItem("size") || config.sizeOption[2].value;
state.codeTheme = state.codeTheme =
localStorage.getItem("codeTheme") || localStorage.getItem("codeTheme") || config.codeThemeOption[0].value;
config.codeThemeOption[0].value; state.citeStatus = localStorage.getItem("citeStatus") === "true";
state.citeStatus = localStorage.getItem("citeStatus") === "true"; state.nightMode = localStorage.getItem("nightMode") === "true";
state.nightMode = localStorage.getItem("nightMode") === "true"; state.wxRenderer = new WxRenderer({
state.wxRenderer = new WxRenderer({ theme: setColor(state.currentColor),
theme: setColor(state.currentColor), fonts: state.currentFont,
fonts: state.currentFont, size: state.currentSize,
size: state.currentSize, status: state.citeStatus,
status: state.citeStatus, });
}); },
}, initEditorEntity(state) {
initEditorEntity(state) { const editorDom = document.getElementById("editor");
state.editor = CodeMirror.fromTextArea(
document.getElementById("editor"),
{
value: "",
mode: "text/x-markdown",
theme: "xq-light",
lineNumbers: false,
lineWrapping: true,
styleActiveLine: true,
autoCloseBrackets: true,
extraKeys: {
"Ctrl-F": function autoFormat(editor) {
const doc = formatDoc(editor.getValue(0));
localStorage.setItem("__editor_content", doc);
editor.setValue(doc);
},
"Ctrl-S": function save(editor) {},
},
}
);
// 如果有编辑器内容被保存则读取,否则加载默认内容 if (!editorDom.value) {
state.editor.setValue( editorDom.value =
localStorage.getItem("__editor_content") || localStorage.getItem("__editor_content") || formatDoc(DEFAULT_CONTENT);
formatDoc(DEFAULT_CONTENT) }
); state.editor = CodeMirror.fromTextArea(editorDom, {
}, mode: "text/x-markdown",
initCssEditorEntity(state) { theme: "xq-light",
state.cssEditor = CodeMirror.fromTextArea( lineNumbers: false,
document.getElementById("cssEditor"), lineWrapping: true,
{ styleActiveLine: true,
value: "", autoCloseBrackets: true,
mode: "css", extraKeys: {
theme: "style-mirror", "Ctrl-F": function autoFormat(editor) {
lineNumbers: false, const doc = formatDoc(editor.getValue(0));
lineWrapping: true, localStorage.setItem("__editor_content", doc);
matchBrackets: true, editor.setValue(doc);
autofocus: true, },
extraKeys: { "Ctrl-S": function save(editor) {},
"Ctrl-F": function autoFormat(editor) { },
const doc = formatCss(editor.getValue(0)); });
localStorage.setItem("__css_content", doc); },
editor.setValue(doc); initCssEditorEntity(state) {
}, const cssEditorDom = document.getElementById("cssEditor");
"Ctrl-S": function save(editor) {},
},
}
);
// 如果有编辑器内容被保存则读取,否则加载默认内容 if (!cssEditorDom.value) {
state.cssEditor.setValue( cssEditorDom.value =
localStorage.getItem("__css_content") || DEFAULT_CSS_CONTENT localStorage.getItem("__css_content") || DEFAULT_CSS_CONTENT;
); }
}, state.cssEditor = CodeMirror.fromTextArea(cssEditorDom, {
editorRefresh(state) { mode: "css",
let output = marked(state.editor.getValue(0), { theme: "style-mirror",
renderer: state.wxRenderer.getRenderer(state.citeStatus), lineNumbers: false,
}); lineWrapping: true,
matchBrackets: true,
autofocus: true,
extraKeys: {
"Ctrl-F": function autoFormat(editor) {
const doc = formatCss(editor.getValue(0));
localStorage.setItem("__css_content", doc);
editor.setValue(doc);
},
"Ctrl-S": function save(editor) {},
},
});
},
editorRefresh(state) {
let output = marked(state.editor.getValue(0), {
renderer: state.wxRenderer.getRenderer(state.citeStatus),
});
// 去除第一行的 margin-top // 去除第一行的 margin-top
output = output.replace(/(style=".*?)"/, '$1;margin-top: 0"'); output = output.replace(/(style=".*?)"/, '$1;margin-top: 0"');
if (state.citeStatus) { if (state.citeStatus) {
// 引用脚注 // 引用脚注
output += state.wxRenderer.buildFootnotes(); output += state.wxRenderer.buildFootnotes();
// 附加的一些 style // 附加的一些 style
output += state.wxRenderer.buildAddition(); output += state.wxRenderer.buildAddition();
} }
state.output = output; state.output = output;
}, },
clearEditorToDefault(state) { clearEditorToDefault(state) {
const doc = formatDoc(DEFAULT_CONTENT); const doc = formatDoc(DEFAULT_CONTENT);
state.editor.setValue(doc);
state.cssEditor.setValue(DEFAULT_CSS_CONTENT); state.editor.setValue(doc);
}, state.cssEditor.setValue(DEFAULT_CSS_CONTENT);
},
}; };
export default new Vuex.Store({ export default new Vuex.Store({
state, state,
mutations, mutations,
actions: {}, actions: {},
}); });

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 = { const fs = require("fs");
outputDir: "dist", function writeManifestJson() {
publicPath: process.env.NETLIFY ? "/" : "/md/", 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