取消同步页面
This commit is contained in:
commit
935f09a737
|
@ -0,0 +1,18 @@
|
|||
# https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
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
|
|
@ -0,0 +1,25 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
},
|
||||
extends: [`plugin:vue/essential`, `eslint:recommended`, `@vue/prettier`],
|
||||
parserOptions: {
|
||||
parser: `babel-eslint`,
|
||||
},
|
||||
ignorePatterns: [`src/assets/scripts/renderers`],
|
||||
rules: {
|
||||
'prettier/prettier': [
|
||||
`error`,
|
||||
{
|
||||
singleQuote: true,
|
||||
semi: false,
|
||||
},
|
||||
],
|
||||
semi: [`error`, `never`],
|
||||
quotes: [`error`, `backtick`],
|
||||
'no-unused-vars': `off`,
|
||||
'no-console': `off`,
|
||||
'no-debugger': `off`,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
dist
|
||||
lib
|
||||
|
||||
node_modules
|
||||
|
||||
# Log files
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# mockm
|
||||
httpData
|
||||
|
||||
public/upload/**
|
||||
!public/upload/*.gitkeep
|
||||
.history
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll": true
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
Version 2, December 2004
|
||||
|
||||
Copyright (C) 2012 Romain Lespinasse <romain.lespinasse@gmail.com>
|
||||
|
||||
Everyone is permitted to copy and distribute verbatim or modified
|
||||
copies of this license document, and changing it is allowed as long
|
||||
as the name is changed.
|
||||
|
||||
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||
|
||||
0. You just DO WHAT THE FUCK YOU WANT TO.
|
|
@ -0,0 +1,8 @@
|
|||
# 前言
|
||||
|
||||
一直想直接写一套文章在博客和公众号同步使用,因为习惯markdown,而其对公众号的排版有点问题,所以找到了这款专门给公众号排版的项目,但是做了一些适合自己的需求。
|
||||
## 修改记录
|
||||
|
||||
1. 去除原项目自动同步功能,因为同步插件已经停止维护,并且很多功能已经无法使用,所以去除了自动同步功能,只保留公众号模版复制功能
|
||||
2. 将项目作为了自己的本地文档的存储库,添加本地文章列表菜单,可以同步切换展示
|
||||
3. 默认文档底部添加公众号链接
|
|
@ -0,0 +1,3 @@
|
|||
module.exports = {
|
||||
presets: [`@vue/cli-plugin-babel/preset`],
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
httpData/
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"paths": {},
|
||||
"disable": []
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
const fs = require(`fs`)
|
||||
const path = require(`path`)
|
||||
|
||||
const { dcloud } = require(`./util.js`)
|
||||
|
||||
// unicloud 服务空间配置
|
||||
const spaceInfo = {
|
||||
spaceId: ``,
|
||||
clientSecret: ``,
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置说明请参考文档:
|
||||
* https://hongqiye.com/doc/mockm/config/option.html
|
||||
* @type {import('mockm/@types/config').Config}
|
||||
*/
|
||||
module.exports = (util) => {
|
||||
const port = 9000
|
||||
return {
|
||||
port,
|
||||
testPort: 9005,
|
||||
replayPort: 9001,
|
||||
watch: [`./util.js`],
|
||||
api: {
|
||||
async '/upload'(req, res) {
|
||||
const multiparty = await util.toolObj.generate.initPackge(`multiparty`)
|
||||
const form = new multiparty.Form({
|
||||
uploadDir: `../public/upload/`,
|
||||
})
|
||||
form.parse(req, async (err, fields = [], files) => {
|
||||
const file = files.file[0]
|
||||
let url = `http://127.0.0.1:${port}/public/upload/${
|
||||
path.parse(file.path).base
|
||||
}`
|
||||
try {
|
||||
url = await dcloud(spaceInfo)({
|
||||
name: file.originalFilename,
|
||||
file: fs.createReadStream(file.path),
|
||||
})
|
||||
} catch (err) {
|
||||
// console.log(err)
|
||||
}
|
||||
res.json({ url })
|
||||
})
|
||||
},
|
||||
},
|
||||
static: [
|
||||
{
|
||||
// 测试 netlify 部署
|
||||
fileDir: `../dist`,
|
||||
path: `/`,
|
||||
},
|
||||
{
|
||||
// 测试 gitee/github 部署
|
||||
fileDir: `../dist`,
|
||||
path: `/md`,
|
||||
},
|
||||
{
|
||||
// 访问公共目录
|
||||
fileDir: `../public`,
|
||||
path: `/public`,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
# 说明
|
||||
|
||||
此目录是运行命令 `mm --template` 之后生成的 mockm 常用配置, 该命令做了以下事情:
|
||||
|
||||
在运行目录的 package.json 的 scripts 中添加命令 `"mm": "npx mockm --cwd=mm"`, 如果没有 package.json 文件, 会自动创建.
|
||||
|
||||
创建名为 mm 的目录, 文件说明如下, 如果存在则不覆盖:
|
||||
|
||||
```
|
||||
mm/
|
||||
- api/ -- 手动创建的 api
|
||||
- httpData/ -- 请求记录, 一般不提交到版本库
|
||||
- apiWeb.json -- 从 UI 界面上创建的接口信息
|
||||
- util.js -- 一些公用方法
|
||||
- mm.config.js -- mockm 的配置文件
|
||||
```
|
||||
|
||||
## 参考
|
||||
|
||||
- [mm 代码仓库](https://github.com/wll8/mockm/)
|
||||
- [mm 文档](https://hongqiye.com/doc/mockm/)
|
||||
- [mockjs 文档](http://wll8.gitee.io/mockjs-examples/)
|
|
@ -0,0 +1,112 @@
|
|||
const fetch = (...args) =>
|
||||
import(`node-fetch`).then(({ default: fetch }) => fetch(...args))
|
||||
const FormData = require(`form-data`)
|
||||
|
||||
function dcloud(spaceInfo) {
|
||||
if (Boolean(spaceInfo.spaceId && spaceInfo.clientSecret) === false) {
|
||||
throw new Error(`请填写 spaceInfo`)
|
||||
}
|
||||
|
||||
function sign(data, secret) {
|
||||
const hmac = require(`crypto`).createHmac(`md5`, secret)
|
||||
// 排序 obj 再转换为 key=val&key=val 的格式
|
||||
const str = Object.keys(data)
|
||||
.sort()
|
||||
.reduce((acc, cur) => `${acc}&${cur}=${data[cur]}`, ``)
|
||||
.slice(1)
|
||||
hmac.update(str)
|
||||
return hmac.digest(`hex`)
|
||||
}
|
||||
|
||||
async function anonymousAuthorize() {
|
||||
const data = {
|
||||
method: `serverless.auth.user.anonymousAuthorize`,
|
||||
params: `{}`,
|
||||
spaceId: spaceInfo.spaceId,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
return await fetch(`https://api.bspapp.com/client`, {
|
||||
headers: {
|
||||
'x-serverless-sign': sign(data, spaceInfo.clientSecret),
|
||||
},
|
||||
body: `{"method":"serverless.auth.user.anonymousAuthorize","params":"{}","spaceId":"${spaceInfo.spaceId}","timestamp":${data.timestamp}}`,
|
||||
method: `POST`,
|
||||
}).then((res) => res.json())
|
||||
}
|
||||
|
||||
async function report({ id, token }) {
|
||||
const reportReq = {
|
||||
method: `serverless.file.resource.report`,
|
||||
params: `{"id":"${id}"}`,
|
||||
spaceId: spaceInfo.spaceId,
|
||||
timestamp: Date.now(),
|
||||
token: token,
|
||||
}
|
||||
return await fetch(`https://api.bspapp.com/client`, {
|
||||
headers: {
|
||||
'x-basement-token': reportReq.token,
|
||||
'x-serverless-sign': sign(reportReq, spaceInfo.clientSecret),
|
||||
},
|
||||
body: JSON.stringify(reportReq),
|
||||
method: `POST`,
|
||||
}).then((res) => res.json())
|
||||
}
|
||||
|
||||
async function generateProximalSign({ name, token }) {
|
||||
const data = {
|
||||
method: `serverless.file.resource.generateProximalSign`,
|
||||
params: `{"env":"public","filename":"${name}"}`,
|
||||
spaceId: spaceInfo.spaceId,
|
||||
timestamp: Date.now(),
|
||||
token,
|
||||
}
|
||||
const res = await fetch(`https://api.bspapp.com/client`, {
|
||||
headers: {
|
||||
'x-basement-token': data.token,
|
||||
'x-serverless-sign': sign(data, spaceInfo.clientSecret),
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
method: `POST`,
|
||||
}).then((res) => res.json())
|
||||
return res
|
||||
}
|
||||
|
||||
async function upload({ data, file }) {
|
||||
const formdata = new FormData()
|
||||
Object.entries({
|
||||
'Cache-Control': `max-age=2592000`,
|
||||
'Content-Disposition': `attachment`,
|
||||
OSSAccessKeyId: data.accessKeyId,
|
||||
Signature: data.signature,
|
||||
host: data.host,
|
||||
id: data.id,
|
||||
key: data.ossPath,
|
||||
policy: data.policy,
|
||||
success_action_status: 200,
|
||||
file,
|
||||
}).forEach(([key, val]) => formdata.append(key, val))
|
||||
|
||||
return await fetch(`https://${data.host}`, {
|
||||
headers: {
|
||||
'X-OSS-server-side-encrpytion': `AES256`,
|
||||
},
|
||||
body: formdata,
|
||||
method: `POST`,
|
||||
})
|
||||
}
|
||||
|
||||
async function uploadFile({ name = `unnamed.file`, file }) {
|
||||
const token = (await anonymousAuthorize()).data.accessToken
|
||||
const res = await generateProximalSign({ name, token })
|
||||
await upload({ data: res.data, file })
|
||||
await report({ id: res.data.id, token })
|
||||
const fileUrl = `https://${res.data.cdnDomain}/${res.data.ossPath}`
|
||||
return fileUrl
|
||||
}
|
||||
|
||||
return uploadFile
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
dcloud,
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,60 @@
|
|||
{
|
||||
"name": "md",
|
||||
"version": "1.5.9",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"prepare": "husky install",
|
||||
"lint": "vue-cli-service lint src && vue-cli-service lint mm",
|
||||
"start": "npm run lint -- --fix && run-p serve mm",
|
||||
"serve": "vue-cli-service serve",
|
||||
"build:h5-netlify": "cross-env SERVER_ENV=NETLIFY vue-cli-service build",
|
||||
"build": "vue-cli-service build",
|
||||
"mm": "npx mockm --cwd=mm"
|
||||
},
|
||||
"dependencies": {
|
||||
"ali-oss": "^6.17.1",
|
||||
"axios": "^0.27.2",
|
||||
"buffer-from": "^1.1.2",
|
||||
"codemirror": "^5.65.7",
|
||||
"cos-js-sdk-v5": "^1.3.9",
|
||||
"crypto-js": "^4.1.1",
|
||||
"element-ui": "^2.15.9",
|
||||
"form-data": "4.0.0",
|
||||
"highlight.js": "^11.6.0",
|
||||
"juice": "^8.0.0",
|
||||
"marked": "^4.0.18",
|
||||
"minio": "7.0.33",
|
||||
"node-fetch": "^3.2.10",
|
||||
"pinia": "^2.1.6",
|
||||
"prettify": "^0.1.7",
|
||||
"qiniu-js": "^3.4.1",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^2.7.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.5.19",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-service": "^4.5.15",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"async-validator": "^4.0.7",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-plugin-import": "^1.13.3",
|
||||
"cache-loader": "^4.1.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"husky": "^8.0.3",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^7.3.0",
|
||||
"mini-types": "*",
|
||||
"miniprogram-api-typings": "*",
|
||||
"mockm": "^1.1.26-alpha.29",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-comment": "^2.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"shx": "^0.3.4",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 5.7 KiB |
Binary file not shown.
After Width: | Height: | Size: 6.4 KiB |
|
@ -0,0 +1,43 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta
|
||||
name="keywords"
|
||||
content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器"
|
||||
/>
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<title>微信 Markdown 编辑器 | Doocs 开源社区</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/favicon.png"
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon-precomposed"
|
||||
href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/wechatsync/article-syncjs@latest/dist/styles.css"
|
||||
/>
|
||||
<script src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/prettify/r298/prettify.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>Please enable JavaScript to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
<script src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/wechatsync/article-syncjs@latest/dist/main.js"></script>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
<template>
|
||||
<div id="app">
|
||||
<codemirror-editor />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import CodemirrorEditor from '@/views/CodemirrorEditor.vue'
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
// 仿 uniapp 外层全屏
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
/* 每个页面公共css */
|
||||
@import url('./assets/less/style-mirror.css');
|
||||
@import url('./assets/less/theme.less');
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
::-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);
|
||||
}
|
||||
|
||||
/* CSS-hints */
|
||||
.CodeMirror-hints {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
overflow-y: auto;
|
||||
margin: 0;
|
||||
padding: 2px;
|
||||
border-radius: 4px;
|
||||
max-height: 20em;
|
||||
min-width: 200px;
|
||||
font-size: 12px;
|
||||
font-family: monospace;
|
||||
|
||||
color: #333333;
|
||||
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);
|
||||
}
|
||||
|
||||
.CodeMirror-hint {
|
||||
margin-top: 10px;
|
||||
padding: 4px 6px;
|
||||
border-radius: 2px;
|
||||
white-space: pre;
|
||||
color: #000000;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,55 @@
|
|||
const githubConfig = {
|
||||
username: `filess`,
|
||||
repoList: Array.from(
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
|
||||
(e) => `img${e}`
|
||||
),
|
||||
branch: `main`,
|
||||
accessTokenList: [
|
||||
`7715d7ca67b5d3837cfdoocsmde8c38421815aa423510af`,
|
||||
`c411415bf95dbe39625doocsmd5047ba9b7a2a6c9642abe`,
|
||||
`2821cd8819fa345c053doocsmdca86ac653f8bc20db1f1b`,
|
||||
`445f0dae46ef1f2a4d6doocsmdc797301e94797b4750a4c`,
|
||||
`cc1d0c1426d0fd0902bdoocsmdd2d7184b14da61b86ec46`,
|
||||
`b67e9d15cb6f910492fdoocsmdac6b44d379c953bb19eff`,
|
||||
`618c4dc2244ccbbc088doocsmd125d17fd31b7d06a50cf3`,
|
||||
`a4b581732e1c1507458doocsmdc5b223b27dae5e2e16a55`,
|
||||
`77904db41aee57ad79bdoocsmd760f848201dac9c96fd5e`,
|
||||
`02f251cb14ac62ab100doocsmdddbfc8527d773f1f04ce1`,
|
||||
`eb321079a95ba7028d9doocsmde2e84c502dac70de7cf08`,
|
||||
`22f74fcfb071a961fa2doocsmde28dabc746f0503a15e5d`,
|
||||
`85124c2bfe7abba0938doocsmd0af7f67918b99d085a5fd`,
|
||||
`0a561b4d4bbecb2de7edoocsmdd9ba3833d11dbc5e430f5`,
|
||||
`e8a01491188d8d5a097doocsmd03ede0aad1fe9e3af24e9`,
|
||||
`36e1f420d7e5bdebd67doocsmd65463562f5f25b20b8377`,
|
||||
],
|
||||
}
|
||||
|
||||
const giteeConfig = {
|
||||
username: `filesss`,
|
||||
repoList: Array.from(
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19],
|
||||
(e) => `img${e}`
|
||||
),
|
||||
branch: `main`,
|
||||
accessTokenList: [
|
||||
`ed5fc9866bd6c2fdoocsmddd433f806fd2f399c`,
|
||||
`5448ffebbbf1151doocsmdc4e337cf814fc8a62`,
|
||||
`25b05efd2557ca2doocsmd75b5c0835e3395911`,
|
||||
`11628c7a5aef015doocsmd2eeff9fb9566f0458`,
|
||||
`cb2f5145ed938dedoocsmdbd063b4ed244eecf8`,
|
||||
`d8c0b57500672c1doocsmd55f48b866b5ebcd98`,
|
||||
`78c56eadb88e453doocsmd43ddd95753351771a`,
|
||||
`03e1a688003948fdoocsmda16fcf41e6f03f1f0`,
|
||||
`c49121cf4d191fbdoocsmdd6a7877ed537e474a`,
|
||||
`adfeb2fadcdc4aadoocsmdfe1ee869ac9c968ff`,
|
||||
`116c94549ca4a0ddoocsmd192653af5c0694616`,
|
||||
`ecf30ed7f2eb184doocsmd51ea4ec8300371d9e`,
|
||||
`5837cf2bd5afd93doocsmd73904bed31934949e`,
|
||||
`b5b7e1c7d57e01fdoocsmd5266f552574297d78`,
|
||||
`684d55564ffbd0bdoocsmd7d747e5cc23aed6d6`,
|
||||
`3fc04a9d272ab71doocsmd010c56cb57d88d2ba`,
|
||||
],
|
||||
}
|
||||
|
||||
export { githubConfig, giteeConfig }
|
|
@ -0,0 +1,30 @@
|
|||
import axios from 'axios'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: ``,
|
||||
timeout: 30 * 1000, // 请求超时时间
|
||||
})
|
||||
|
||||
service.interceptors.request.use(
|
||||
(config) => {
|
||||
if (/^(post)|(put)|(delete)$/i.test(config.method)) {
|
||||
if (config.data && config.data.upload) {
|
||||
config.headers[`Content-Type`] = `multipart/form-data`
|
||||
}
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
service.interceptors.response.use(
|
||||
(res) => {
|
||||
return res.data ? res.data : Promise.reject(res)
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
export default service
|
|
@ -0,0 +1,334 @@
|
|||
import fetch from './fetch'
|
||||
import { githubConfig, giteeConfig } from './config'
|
||||
import CryptoJS from 'crypto-js'
|
||||
import OSS from 'ali-oss'
|
||||
import * as Minio from 'minio'
|
||||
import COS from 'cos-js-sdk-v5'
|
||||
import Buffer from 'buffer-from'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import * as qiniu from 'qiniu-js'
|
||||
import { utf16to8, base64encode, safe64 } from '../assets/scripts/tokenTools'
|
||||
import * as tokenTools from '../assets/scripts/tokenTools'
|
||||
|
||||
function getConfig(useDefault, platform) {
|
||||
if (useDefault) {
|
||||
// load default config file
|
||||
const config = platform === `github` ? githubConfig : giteeConfig
|
||||
const { username, repoList, branch, accessTokenList } = config
|
||||
|
||||
// choose random token from access_token list
|
||||
const tokenIndex = Math.floor(Math.random() * accessTokenList.length)
|
||||
const accessToken = accessTokenList[tokenIndex].replace(`doocsmd`, ``)
|
||||
|
||||
// choose random repo from repo list
|
||||
const repoIndex = Math.floor(Math.random() * repoList.length)
|
||||
const repo = repoList[repoIndex]
|
||||
|
||||
return { username, repo, branch, accessToken }
|
||||
}
|
||||
|
||||
// load configuration from localStorage
|
||||
const customConfig = JSON.parse(localStorage.getItem(`${platform}Config`))
|
||||
|
||||
// split username/repo
|
||||
const repoUrl = customConfig.repo
|
||||
.replace(`https://${platform}.com/`, ``)
|
||||
.replace(`http://${platform}.com/`, ``)
|
||||
.replace(`${platform}.com/`, ``)
|
||||
.split(`/`)
|
||||
return {
|
||||
username: repoUrl[0],
|
||||
repo: repoUrl[1],
|
||||
branch: customConfig.branch || `master`,
|
||||
accessToken: customConfig.accessToken,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 `年/月/日` 形式的目录
|
||||
* @returns string
|
||||
*/
|
||||
function getDir() {
|
||||
const date = new Date()
|
||||
const year = date.getFullYear()
|
||||
const month = (date.getMonth() + 1).toString().padStart(2, `0`)
|
||||
const day = date.getDate().toString().padStart(2, `0`)
|
||||
return `${year}/${month}/${day}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名获取它以 `时间戳+uuid` 的形式
|
||||
* @param {string} filename 文件名
|
||||
* @returns
|
||||
*/
|
||||
function getDateFilename(filename) {
|
||||
const currentTimestamp = new Date().getTime()
|
||||
const fileSuffix = filename.split(`.`)[1]
|
||||
return `${currentTimestamp}-${uuidv4()}.${fileSuffix}`
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// GitHub File Upload
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
async function ghFileUpload(content, filename) {
|
||||
const useDefault = localStorage.getItem(`imgHost`) === `default`
|
||||
const { username, repo, branch, accessToken } = getConfig(
|
||||
useDefault,
|
||||
`github`
|
||||
)
|
||||
const dir = getDir()
|
||||
const url = `https://api.github.com/repos/${username}/${repo}/contents/${dir}/`
|
||||
const dateFilename = getDateFilename(filename)
|
||||
const res = await fetch({
|
||||
url: url + dateFilename,
|
||||
method: `put`,
|
||||
headers: {
|
||||
Authorization: `token ${accessToken}`,
|
||||
},
|
||||
data: {
|
||||
content,
|
||||
branch,
|
||||
message: `Upload by ${window.location.href}`,
|
||||
},
|
||||
})
|
||||
const githubResourceUrl = `raw.githubusercontent.com/${username}/${repo}/${branch}/`
|
||||
const cdnResourceUrl = `fastly.jsdelivr.net/gh/${username}/${repo}@${branch}/`
|
||||
res.content = res.data?.content || res.content
|
||||
return useDefault
|
||||
? res.content.download_url.replace(githubResourceUrl, cdnResourceUrl)
|
||||
: res.content.download_url
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// Gitee File Upload
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
async function giteeUpload(content, filename) {
|
||||
const useDefault = localStorage.getItem(`imgHost`) === `default`
|
||||
const { username, repo, branch, accessToken } = getConfig(useDefault, `gitee`)
|
||||
const dir = getDir()
|
||||
const dateFilename = getDateFilename(filename)
|
||||
const url = `https://gitee.com/api/v5/repos/${username}/${repo}/contents/${dir}/${dateFilename}`
|
||||
const res = await fetch({
|
||||
url,
|
||||
method: `POST`,
|
||||
data: {
|
||||
content,
|
||||
branch,
|
||||
access_token: accessToken,
|
||||
message: `Upload by ${window.location.href}`,
|
||||
},
|
||||
})
|
||||
res.content = res.data?.content || res.content
|
||||
return encodeURI(res.content.download_url)
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// Qiniu File Upload
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
function getQiniuToken(accessKey, secretKey, putPolicy) {
|
||||
const policy = JSON.stringify(putPolicy)
|
||||
const encoded = base64encode(utf16to8(policy))
|
||||
const hash = CryptoJS.HmacSHA1(encoded, secretKey)
|
||||
const encodedSigned = hash.toString(CryptoJS.enc.Base64)
|
||||
return `${accessKey}:${safe64(encodedSigned)}:${encoded}`
|
||||
}
|
||||
|
||||
async function qiniuUpload(file) {
|
||||
const { accessKey, secretKey, bucket, region, path, domain } = JSON.parse(
|
||||
localStorage.getItem(`qiniuConfig`)
|
||||
)
|
||||
const token = getQiniuToken(accessKey, secretKey, {
|
||||
scope: bucket,
|
||||
deadline: Math.trunc(new Date().getTime() / 1000) + 3600,
|
||||
})
|
||||
const dir = path ? `${path}/` : ``
|
||||
const dateFilename = dir + getDateFilename(file.name)
|
||||
const observable = qiniu.upload(file, dateFilename, token, {}, { region })
|
||||
return new Promise((resolve, reject) => {
|
||||
observable.subscribe({
|
||||
next: (result) => {
|
||||
console.log(result)
|
||||
},
|
||||
error: (err) => {
|
||||
reject(err.message)
|
||||
},
|
||||
complete: (result) => {
|
||||
resolve(`${domain}/${result.key}`)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// AliOSS File Upload
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
async function aliOSSFileUpload(content, filename) {
|
||||
const dateFilename = getDateFilename(filename)
|
||||
const { region, bucket, accessKeyId, accessKeySecret, cdnHost, path } =
|
||||
JSON.parse(localStorage.getItem(`aliOSSConfig`))
|
||||
const buffer = Buffer(content, `base64`)
|
||||
const dir = `${path}/${dateFilename}`
|
||||
const client = new OSS({
|
||||
region,
|
||||
bucket,
|
||||
accessKeyId,
|
||||
accessKeySecret,
|
||||
})
|
||||
try {
|
||||
const res = await client.put(dir, buffer)
|
||||
if (cdnHost === ``) return res.url
|
||||
return `${cdnHost}/${path === `` ? dateFilename : dir}`
|
||||
} catch (e) {
|
||||
return Promise.reject(e)
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// TxCOS File Upload
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
async function txCOSFileUpload(file) {
|
||||
const dateFilename = getDateFilename(file.name)
|
||||
const { secretId, secretKey, bucket, region, path, cdnHost } = JSON.parse(
|
||||
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}`)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// Minio File Upload
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
async function minioFileUpload(content, filename) {
|
||||
const dateFilename = getDateFilename(filename)
|
||||
const { endpoint, port, useSSL, bucket, accessKey, secretKey } = JSON.parse(
|
||||
localStorage.getItem(`minioConfig`)
|
||||
)
|
||||
const buffer = Buffer(content, `base64`)
|
||||
const conf = {
|
||||
endPoint: endpoint,
|
||||
useSSL: useSSL,
|
||||
accessKey: accessKey,
|
||||
secretKey: secretKey,
|
||||
}
|
||||
const p = Number(port || 0)
|
||||
const isCustomPort = p > 0 && p !== 80 && p !== 443
|
||||
if (isCustomPort) {
|
||||
conf.port = p
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
const minioClient = new Minio.Client(conf)
|
||||
try {
|
||||
minioClient.putObject(bucket, dateFilename, buffer, function (e) {
|
||||
if (e) {
|
||||
reject(e)
|
||||
}
|
||||
const host = `${useSSL ? `https://` : `http://`}${endpoint}${
|
||||
isCustomPort ? `:` + port : ``
|
||||
}`
|
||||
const url = `${host}/${bucket}/${dateFilename}`
|
||||
// console.log("文件上传成功: ", url)
|
||||
resolve(url)
|
||||
// return `${endpoint}/${bucket}/${dateFilename}`;
|
||||
})
|
||||
} catch (e) {
|
||||
reject(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------
|
||||
// formCustom File Upload
|
||||
//-----------------------------------------------------------------------
|
||||
|
||||
async function formCustomUpload(content, file) {
|
||||
const str = `
|
||||
async (CUSTOM_ARG) => {
|
||||
${localStorage.getItem(`formCustomConfig`)}
|
||||
}
|
||||
`
|
||||
return new Promise((resolve, reject) => {
|
||||
const exportObj = {
|
||||
content, // 待上传图片的 base64
|
||||
file, // 待上传图片的 file 对象
|
||||
util: {
|
||||
axios: fetch, // axios 实例
|
||||
CryptoJS, // 加密库
|
||||
OSS, // ali-oss
|
||||
COS, // cos-js-sdk-v5
|
||||
Buffer, // buffer-from
|
||||
uuidv4, // uuid
|
||||
qiniu, // qiniu-js
|
||||
tokenTools, // 一些编码转换函数
|
||||
getDir, // 获取 年/月/日 形式的目录
|
||||
getDateFilename, // 根据文件名获取它以 时间戳+uuid 的形式
|
||||
},
|
||||
okCb: resolve, // 重要: 上传成功后给此回调传 url 即可
|
||||
errCb: reject, // 上传失败调用的函数
|
||||
}
|
||||
eval(str)(exportObj).catch((err) => {
|
||||
console.error(err)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function fileUpload(content, file) {
|
||||
const imgHost = localStorage.getItem(`imgHost`)
|
||||
!imgHost && localStorage.setItem(`imgHost`, `default`)
|
||||
switch (imgHost) {
|
||||
case `aliOSS`:
|
||||
return aliOSSFileUpload(content, file.name)
|
||||
case `minio`:
|
||||
return minioFileUpload(content, file.name)
|
||||
case `txCOS`:
|
||||
return txCOSFileUpload(file)
|
||||
case `qiniu`:
|
||||
return qiniuUpload(file)
|
||||
case `gitee`:
|
||||
return giteeUpload(content, file.name)
|
||||
case `github`:
|
||||
return ghFileUpload(content, file.name)
|
||||
case `formCustom`:
|
||||
return formCustomUpload(content, file)
|
||||
default:
|
||||
// return file.size / 1024 < 1024
|
||||
// ? giteeUpload(content, file.name)
|
||||
// : ghFileUpload(content, file.name);
|
||||
return ghFileUpload(content, file.name)
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
fileUpload,
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
# sdfdsf
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
按Ctrl+F可格式化
|
||||
*/
|
||||
/* 一级标题样式 */
|
||||
h1 {
|
||||
}
|
||||
/* 二级标题样式 */
|
||||
h2 {
|
||||
}
|
||||
/* 三级标题样式 */
|
||||
h3 {
|
||||
}
|
||||
/* 四级标题样式 */
|
||||
h4 {
|
||||
}
|
||||
/* 图片样式 */
|
||||
image {
|
||||
}
|
||||
/* 引用样式 */
|
||||
blockquote {
|
||||
}
|
||||
/* 引用段落样式 */
|
||||
blockquote_p {
|
||||
}
|
||||
/* 段落样式 */
|
||||
p {
|
||||
}
|
||||
/* 分割线样式 */
|
||||
hr {
|
||||
}
|
||||
/* 行内代码样式 */
|
||||
codespan {
|
||||
}
|
||||
/* 粗体样式 */
|
||||
strong {
|
||||
}
|
||||
/* 链接样式 */
|
||||
link {
|
||||
}
|
||||
/* 微信链接样式 */
|
||||
wx_link {
|
||||
}
|
||||
/* 有序列表样式 */
|
||||
ol {
|
||||
}
|
||||
/* 无序列表样式 */
|
||||
ul {
|
||||
}
|
||||
/* 列表项样式 */
|
||||
li {
|
||||
}
|
||||
/* 代码块样式 */
|
||||
code {
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
|
@ -0,0 +1,132 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
em {
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", BlinkMacSystemFont, Roboto, "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
.el-message__icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.web-title {
|
||||
margin: 0 15px 0 5px;
|
||||
}
|
||||
|
||||
.web-icon {
|
||||
width: auto;
|
||||
height: 1.5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#editor {
|
||||
height: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
section {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.ctrl {
|
||||
flex-basis: 60px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
word-break: break-all;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.main-section {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hint {
|
||||
opacity: 0.6;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
margin: 0 -20px;
|
||||
width: 375px;
|
||||
padding: 20px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
box-shadow: 0 0 60px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview table {
|
||||
margin-bottom: 10px;
|
||||
border-collapse: collapse;
|
||||
display: table;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* ele ui */
|
||||
.el-form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.el-tooltip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
uni-page-body,
|
||||
uni-page-refresh {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
|
||||
Name: Base16 Default Light
|
||||
Author: Chris Kempson (http://chriskempson.com)
|
||||
|
||||
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
|
||||
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
||||
|
||||
*/
|
||||
|
||||
.cm-s-style-mirror.CodeMirror {
|
||||
color: #444;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-scroll {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror div.CodeMirror-selected {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-line::selection,
|
||||
.cm-s-style-mirror .CodeMirror-line > span::selection,
|
||||
.cm-s-style-mirror .CodeMirror-line > span > span::selection {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-line::-moz-selection,
|
||||
.cm-s-style-mirror .CodeMirror-line > span::-moz-selection,
|
||||
.cm-s-style-mirror .CodeMirror-line > span > span::-moz-selection {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-gutters {
|
||||
background: #f5f5f5;
|
||||
border-right: 0px;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-guttermarker {
|
||||
color: #ac4142;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-guttermarker-subtle {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-linenumber {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-cursor {
|
||||
border-left: 1px solid #505050;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-comment {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-atom {
|
||||
color: #aa759f;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-number {
|
||||
color: #aa759f;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-property,
|
||||
.cm-s-style-mirror span.cm-attribute {
|
||||
color: #90a959;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-keyword {
|
||||
color: #023a52;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-string {
|
||||
color: #e46918;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-variable {
|
||||
color: #90a959;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-variable-2 {
|
||||
color: #00695f;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-variable-3 {
|
||||
color: #2e6e8a;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-def {
|
||||
color: #d28445;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-bracket {
|
||||
color: #202020;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-tag {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-link {
|
||||
color: #b26a00;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-error {
|
||||
/* background: #ac4142;
|
||||
color: #f5f5f5; */
|
||||
text-decoration: underline;
|
||||
text-decoration-style: wavy;
|
||||
text-decoration-color: #df8d8e;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-activeline-background {
|
||||
background: #dddcdc;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-matchingbracket {
|
||||
color: rgb(32, 32, 32) !important;
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
@nightBgColor: #333333;
|
||||
@nightPreviewColor: #1e1e1e;
|
||||
@nightHeaderColor: #3c3c3c;
|
||||
@nightCodeMirrorColor: #1e1e1e;
|
||||
@nightActiveCodeMirrorColor: gray;
|
||||
@nightFontColor: gray;
|
||||
@nightLinkColor: #8e9eb9;
|
||||
@nightLinkTextColor: #84868b;
|
||||
@nightWhiteColor: #f0f0f0;
|
||||
@nightButtonBg: #1e1e1e;
|
||||
@nightButtonHoverColor: #84868b;
|
||||
@nightLineColor: #84868b;
|
||||
|
||||
.container_night {
|
||||
background-color: @nightBgColor;
|
||||
|
||||
.el-main {
|
||||
background-color: @nightBgColor;
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
caret-color: @nightFontColor;
|
||||
color: @nightFontColor;
|
||||
background-color: @nightCodeMirrorColor;
|
||||
box-shadow: inset 0 0 0 1px rgba(100, 37, 37, 0.102);
|
||||
}
|
||||
|
||||
.output_night {
|
||||
.preview {
|
||||
background-color: @nightPreviewColor;
|
||||
box-shadow: 0 0 70px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
background-color: @nightCodeMirrorColor;
|
||||
box-shadow: inset 0 0 0 1px rgba(233, 231, 231, 0.102);
|
||||
}
|
||||
|
||||
.code-snippet__fix {
|
||||
background-color: rgb(238, 238, 238);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-matchingbracket {
|
||||
color: @nightWhiteColor !important;
|
||||
background: rgb(30, 30, 30) !important;
|
||||
}
|
||||
|
||||
.cm-s-xq-light span.cm-variable-2,
|
||||
.cm-s-style-mirror span.cm-tag {
|
||||
color: @nightFontColor;
|
||||
}
|
||||
|
||||
.cm-s-xq-light .CodeMirror-activeline-background {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.cm-s-xq-light span.cm-string {
|
||||
color: @nightLinkColor;
|
||||
}
|
||||
|
||||
.cm-s-xq-light span.cm-link {
|
||||
color: @nightLinkTextColor;
|
||||
}
|
||||
|
||||
.editor__header {
|
||||
background-color: @nightHeaderColor;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
color: @nightWhiteColor;
|
||||
background-color: @nightCodeMirrorColor;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.el-button.is-plain:focus,
|
||||
.el-button.is-plain:hover {
|
||||
background: @nightButtonBg;
|
||||
color: @nightWhiteColor;
|
||||
border: 1px solid @nightWhiteColor;
|
||||
|
||||
i {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
}
|
||||
|
||||
.insert__dialog,
|
||||
.about__dialog,
|
||||
.reset__dialog,
|
||||
.upload__dialog {
|
||||
.el-dialog {
|
||||
background-color: @nightBgColor;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
|
||||
.el-dialog__title,
|
||||
.el-form-item__label {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
color: @nightActiveCodeMirrorColor;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
background-color: @nightLineColor;
|
||||
}
|
||||
|
||||
.is-active {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
|
||||
.el-upload-dragger {
|
||||
background-color: @nightButtonBg;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-icon-upload,
|
||||
.el-icon-download,
|
||||
.el-icon-refresh,
|
||||
.el-icon-s-grid,
|
||||
.el-icon-document {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
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: 0 20px;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
.CodeMirror-vscrollbar {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
.CodeMirror-wrap {
|
||||
padding-top: 20px;
|
||||
padding-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
export default {
|
||||
builtinFonts: [
|
||||
{
|
||||
label: `无衬线`,
|
||||
value: `-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif`,
|
||||
desc: `Abc`,
|
||||
},
|
||||
{
|
||||
label: `衬线`,
|
||||
value: `Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif`,
|
||||
desc: `Abc`,
|
||||
},
|
||||
],
|
||||
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: `github`,
|
||||
value: `https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/highlight.js@11.5.1/styles/github.min.css`,
|
||||
desc: `light`,
|
||||
},
|
||||
{
|
||||
label: `solarized-light`,
|
||||
value: `https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/highlight.js@11.5.1/styles/solarized-light.min.css`,
|
||||
desc: `light`,
|
||||
},
|
||||
{
|
||||
label: `atom-one-dark`,
|
||||
value: `https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/highlight.js@11.5.1/styles/atom-one-dark.min.css`,
|
||||
desc: `dark`,
|
||||
},
|
||||
{
|
||||
label: `obsidian`,
|
||||
value: `https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/highlight.js@11.5.1/styles/obsidian.min.css`,
|
||||
desc: `dark`,
|
||||
},
|
||||
{
|
||||
label: `vs2015`,
|
||||
value: `https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/highlight.js@11.5.1/styles/vs2015.min.css`,
|
||||
desc: `dark`,
|
||||
},
|
||||
],
|
||||
form: {
|
||||
rows: 1,
|
||||
cols: 1,
|
||||
},
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import juice from 'juice'
|
||||
|
||||
export function solveWeChatImage() {
|
||||
const clipboardDiv = document.getElementById(`output`)
|
||||
const images = clipboardDiv.getElementsByTagName(`img`)
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i]
|
||||
const width = image.getAttribute(`width`)
|
||||
const height = image.getAttribute(`height`)
|
||||
image.removeAttribute(`width`)
|
||||
image.removeAttribute(`height`)
|
||||
image.style.width = width
|
||||
image.style.height = height
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeCss(html) {
|
||||
return juice(html, {
|
||||
inlinePseudoElements: true,
|
||||
preserveImportant: true,
|
||||
})
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
import { Renderer } from "marked";
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
class WxRenderer {
|
||||
constructor(opts) {
|
||||
this.opts = opts;
|
||||
let footnotes = [];
|
||||
let footnoteIndex = 0;
|
||||
let styleMapping = new Map();
|
||||
|
||||
let merge = (base, extend) => Object.assign({}, base, extend);
|
||||
|
||||
this.buildTheme = (themeTpl) => {
|
||||
let mapping = {};
|
||||
let base = merge(themeTpl.BASE, {
|
||||
"font-family": this.opts.fonts,
|
||||
"font-size": this.opts.size,
|
||||
});
|
||||
for (let ele in themeTpl.inline) {
|
||||
if (themeTpl.inline.hasOwnProperty(ele)) {
|
||||
let style = themeTpl.inline[ele];
|
||||
mapping[ele] = merge(themeTpl.BASE, style);
|
||||
}
|
||||
}
|
||||
|
||||
let base_block = merge(base, {});
|
||||
for (let ele in themeTpl.block) {
|
||||
if (themeTpl.block.hasOwnProperty(ele)) {
|
||||
let style = themeTpl.block[ele];
|
||||
mapping[ele] = merge(base_block, style);
|
||||
}
|
||||
}
|
||||
return mapping;
|
||||
};
|
||||
|
||||
let getStyles = (tokenName, addition) => {
|
||||
let arr = [];
|
||||
let dict = styleMapping[tokenName];
|
||||
if (!dict) return "";
|
||||
for (const key in dict) {
|
||||
arr.push(key + ":" + dict[key]);
|
||||
}
|
||||
return `style="${arr.join(";") + (addition || "")}"`;
|
||||
};
|
||||
|
||||
let addFootnote = (title, link) => {
|
||||
footnotes.push([++footnoteIndex, title, link]);
|
||||
return footnoteIndex;
|
||||
};
|
||||
|
||||
this.buildFootnotes = () => {
|
||||
let footnoteArray = footnotes.map((x) => {
|
||||
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> ${x[1]}: <i>${x[2]}</i><br/>`;
|
||||
});
|
||||
if (!footnoteArray.length) {
|
||||
return "";
|
||||
}
|
||||
return `<h4 ${getStyles("h4")}>引用链接</h4><p ${getStyles(
|
||||
"footnotes"
|
||||
)}>${footnoteArray.join("\n")}</p>`;
|
||||
};
|
||||
|
||||
this.buildAddition = () => {
|
||||
return `
|
||||
<style>
|
||||
.preview-wrapper pre::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: #ccc;
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
padding: 5px 10px 0;
|
||||
line-height: 15px;
|
||||
height: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
};
|
||||
|
||||
this.setOptions = (newOpts) => {
|
||||
this.opts = merge(this.opts, newOpts);
|
||||
};
|
||||
|
||||
this.hasFootnotes = () => footnotes.length !== 0;
|
||||
|
||||
this.getRenderer = (status) => {
|
||||
footnotes = [];
|
||||
footnoteIndex = 0;
|
||||
|
||||
styleMapping = this.buildTheme(this.opts.theme);
|
||||
let renderer = new Renderer();
|
||||
|
||||
renderer.heading = (text, level) => {
|
||||
switch (level) {
|
||||
case 1:
|
||||
return `<h1 ${getStyles("h1")}>${text}</h1>`;
|
||||
case 2:
|
||||
return `<h2 ${getStyles("h2")}>${text}</h2>`;
|
||||
case 3:
|
||||
return `<h3 ${getStyles("h3")}>${text}</h3>`;
|
||||
default:
|
||||
return `<h4 ${getStyles("h4")}>${text}</h4>`;
|
||||
}
|
||||
};
|
||||
renderer.paragraph = (text) => {
|
||||
if (text.indexOf("<figure") != -1 && text.indexOf("<img") != -1) {
|
||||
return text;
|
||||
}
|
||||
return text.replace(/ /g, "") === ""
|
||||
? ""
|
||||
: `<p ${getStyles("p")}>${text}</p>`;
|
||||
};
|
||||
|
||||
renderer.blockquote = (text) => {
|
||||
text = text.replace(/<p.*?>/g, `<p ${getStyles("blockquote_p")}>`);
|
||||
return `<blockquote ${getStyles("blockquote")}>${text}</blockquote>`;
|
||||
};
|
||||
renderer.code = (text, lang) => {
|
||||
lang = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||
|
||||
text = hljs.highlight(text, {language: lang}).value;
|
||||
|
||||
text = text.replace(/\r\n/g,"<br/>")
|
||||
.replace(/\n/g,"<br/>")
|
||||
.replace(/(>[^<]+)|(^[^<]+)/g, function(str) {
|
||||
return str.replace(/\s/g, ' ')
|
||||
});
|
||||
|
||||
return `<pre class="hljs code__pre" ${getStyles("code_pre")}><code class="prettyprint language-${lang}" ${getStyles("code")}>${text}</code></pre>`
|
||||
};
|
||||
renderer.codespan = (text, lang) =>
|
||||
`<code ${getStyles("codespan")}>${text}</code>`;
|
||||
renderer.listitem = (text) =>
|
||||
`<li ${getStyles(
|
||||
"listitem"
|
||||
)}><span><%s/></span>${text}</li>`;
|
||||
|
||||
renderer.list = (text, ordered, start) => {
|
||||
text = text.replace(/<\/*p .*?>/g, "").replace(/<\/*p>/g, "");
|
||||
let segments = text.split(`<%s/>`);
|
||||
if (!ordered) {
|
||||
text = segments.join("• ");
|
||||
return `<ul ${getStyles("ul")}>${text}</ul>`;
|
||||
}
|
||||
text = segments[0];
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
text = text + i + ". " + segments[i];
|
||||
}
|
||||
return `<ol ${getStyles("ol")}>${text}</ol>`;
|
||||
};
|
||||
renderer.image = (href, title, text) => {
|
||||
let subText = "";
|
||||
if (text) {
|
||||
subText = `<figcaption ${getStyles(
|
||||
"figcaption"
|
||||
)}>${text}</figcaption>`;
|
||||
}
|
||||
let figureStyles = getStyles("figure");
|
||||
let imgStyles = getStyles("image");
|
||||
return `<figure ${figureStyles}><img ${imgStyles} src="${href}" title="${title}" alt="${text}"/>${subText}</figure>`;
|
||||
};
|
||||
renderer.link = (href, title, text) => {
|
||||
if (href.startsWith("https://mp.weixin.qq.com")) {
|
||||
return `<a href="${href}" title="${title || text}" ${getStyles(
|
||||
"wx_link"
|
||||
)}>${text}</a>`;
|
||||
}
|
||||
if (href === text) {
|
||||
return text;
|
||||
}
|
||||
if (status) {
|
||||
let ref = addFootnote(title || text, href);
|
||||
return `<span ${getStyles("link")}>${text}<sup>[${ref}]</sup></span>`;
|
||||
}
|
||||
return `<span ${getStyles("link")}>${text}</span>`;
|
||||
};
|
||||
renderer.strong = (text) =>
|
||||
`<strong ${getStyles("strong")}>${text}</strong>`;
|
||||
renderer.em = (text) =>
|
||||
`<span style="font-style: italic;">${text}</span>`;
|
||||
renderer.table = (header, body) =>
|
||||
`<section style="padding:0 8px;"><table class="preview-table"><thead ${getStyles(
|
||||
"thead"
|
||||
)}>${header}</thead><tbody>${body}</tbody></table></section>`;
|
||||
renderer.tablecell = (text, flags) =>
|
||||
`<td ${getStyles("td")}>${text}</td>`;
|
||||
renderer.hr = () => `<hr ${getStyles("hr")}>`;
|
||||
return renderer;
|
||||
};
|
||||
}
|
||||
}
|
||||
export default WxRenderer;
|
|
@ -0,0 +1,192 @@
|
|||
let baseColor = `#3f3f3f`
|
||||
|
||||
export default {
|
||||
BASE: {
|
||||
'text-align': `left`,
|
||||
'line-height': `1.75`,
|
||||
},
|
||||
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)`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
// 二级标题样式
|
||||
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`,
|
||||
},
|
||||
|
||||
// 三级标题样式
|
||||
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)`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
// 四级标题样式
|
||||
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`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
// 引用样式
|
||||
blockquote: {
|
||||
'font-style': `normal`,
|
||||
'border-left': `none`,
|
||||
padding: `1em`,
|
||||
'border-radius': `8px`,
|
||||
color: `rgba(0,0,0,0.5)`,
|
||||
background: `#f7f7f7`,
|
||||
margin: `2em 8px`,
|
||||
},
|
||||
|
||||
blockquote_p: {
|
||||
'letter-spacing': `0.1em`,
|
||||
color: `rgb(80, 80, 80)`,
|
||||
'font-size': `1em`,
|
||||
display: `block`,
|
||||
},
|
||||
code_pre: {
|
||||
'font-size': `14px`,
|
||||
'overflow-x': `auto`,
|
||||
'border-radius': `8px`,
|
||||
padding: `1em`,
|
||||
'line-height': `1.5`,
|
||||
margin: `10px 8px`,
|
||||
},
|
||||
code: {
|
||||
margin: 0,
|
||||
'white-space': `nowrap`,
|
||||
'font-family': `Menlo, Operator Mono, Consolas, Monaco, monospace`,
|
||||
},
|
||||
|
||||
image: {
|
||||
'border-radius': `4px`,
|
||||
display: `block`,
|
||||
margin: `0.1em auto 0.5em`,
|
||||
width: `100% !important`,
|
||||
},
|
||||
|
||||
ol: {
|
||||
'margin-left': `0`,
|
||||
'padding-left': `1em`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
ul: {
|
||||
'margin-left': `0`,
|
||||
'padding-left': `1em`,
|
||||
'list-style': `circle`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
footnotes: {
|
||||
margin: `0.5em 8px`,
|
||||
'font-size': `80%`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
figure: {
|
||||
margin: `1.5em 8px`,
|
||||
color: baseColor,
|
||||
},
|
||||
hr: {
|
||||
'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)`,
|
||||
},
|
||||
},
|
||||
inline: {
|
||||
listitem: {
|
||||
'text-indent': `-1em`,
|
||||
display: `block`,
|
||||
margin: `0.2em 8px`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
codespan: {
|
||||
'font-size': `90%`,
|
||||
color: `#d14`,
|
||||
background: `rgba(27,31,35,.05)`,
|
||||
padding: `3px 5px`,
|
||||
'border-radius': `4px`,
|
||||
'word-break': `break-all`,
|
||||
},
|
||||
|
||||
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`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
thead: {
|
||||
background: `rgba(0, 0, 0, 0.05)`,
|
||||
'font-weight': `bold`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
td: {
|
||||
border: `1px solid #dfdfdf`,
|
||||
padding: `0.25em 0.5em`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
footnote: {
|
||||
'font-size': `12px`,
|
||||
color: baseColor,
|
||||
},
|
||||
|
||||
figcaption: {
|
||||
'text-align': `center`,
|
||||
color: `#888`,
|
||||
'font-size': `0.8em`,
|
||||
},
|
||||
},
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
export function utf16to8(str) {
|
||||
var out, i, len, c
|
||||
out = ``
|
||||
len = str.length
|
||||
for (i = 0; i < len; i++) {
|
||||
c = str.charCodeAt(i)
|
||||
if (c >= 0x0001 && c <= 0x007f) {
|
||||
out += str.charAt(i)
|
||||
} else if (c > 0x07ff) {
|
||||
out += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f))
|
||||
out += String.fromCharCode(0x80 | ((c >> 6) & 0x3f))
|
||||
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f))
|
||||
} else {
|
||||
out += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f))
|
||||
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function utf8to16(str) {
|
||||
var out, i, len, c
|
||||
var char2, char3
|
||||
out = ``
|
||||
len = str.length
|
||||
i = 0
|
||||
while (i < len) {
|
||||
c = str.charCodeAt(i++)
|
||||
switch (c >> 4) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 6:
|
||||
case 7:
|
||||
// 0xxxxxxx
|
||||
out += str.charAt(i - 1)
|
||||
break
|
||||
case 12:
|
||||
case 13:
|
||||
// 110x xxxx 10xx xxxx
|
||||
char2 = str.charCodeAt(i++)
|
||||
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f))
|
||||
break
|
||||
case 14:
|
||||
// 1110 xxxx 10xx xxxx 10xx xxxx
|
||||
char2 = str.charCodeAt(i++)
|
||||
char3 = str.charCodeAt(i++)
|
||||
out += String.fromCharCode(
|
||||
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
const base64EncodeChars = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_`
|
||||
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,
|
||||
62,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
63,
|
||||
52,
|
||||
53,
|
||||
54,
|
||||
55,
|
||||
56,
|
||||
57,
|
||||
58,
|
||||
59,
|
||||
60,
|
||||
61,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
32,
|
||||
33,
|
||||
34,
|
||||
35,
|
||||
36,
|
||||
37,
|
||||
38,
|
||||
39,
|
||||
40,
|
||||
41,
|
||||
42,
|
||||
43,
|
||||
44,
|
||||
45,
|
||||
46,
|
||||
47,
|
||||
48,
|
||||
49,
|
||||
50,
|
||||
51,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
-1
|
||||
)
|
||||
export function base64encode(str) {
|
||||
var out, i, len
|
||||
var c1, c2, c3
|
||||
len = str.length
|
||||
i = 0
|
||||
out = ``
|
||||
while (i < len) {
|
||||
c1 = str.charCodeAt(i++) & 0xff
|
||||
if (i == len) {
|
||||
out += base64EncodeChars.charAt(c1 >> 2)
|
||||
out += base64EncodeChars.charAt((c1 & 0x3) << 4)
|
||||
out += `==`
|
||||
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
|
||||
}
|
||||
|
||||
export function base64decode(str) {
|
||||
var c1, c2, c3, c4
|
||||
var i, len, out
|
||||
len = str.length
|
||||
i = 0
|
||||
out = ``
|
||||
while (i < len) {
|
||||
/* c1 */
|
||||
do {
|
||||
c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff]
|
||||
} while (i < len && c1 == -1)
|
||||
if (c1 == -1) break
|
||||
/* c2 */
|
||||
do {
|
||||
c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff]
|
||||
} while (i < len && c2 == -1)
|
||||
if (c2 == -1) break
|
||||
out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4))
|
||||
/* c3 */
|
||||
do {
|
||||
c3 = str.charCodeAt(i++) & 0xff
|
||||
if (c3 == 61) return out
|
||||
c3 = base64DecodeChars[c3]
|
||||
} while (i < len && c3 == -1)
|
||||
if (c3 == -1) break
|
||||
out += String.fromCharCode(((c2 & 0xf) << 4) | ((c3 & 0x3c) >> 2))
|
||||
/* c4 */
|
||||
do {
|
||||
c4 = str.charCodeAt(i++) & 0xff
|
||||
if (c4 == 61) return out
|
||||
c4 = base64DecodeChars[c4]
|
||||
} while (i < len && c4 == -1)
|
||||
if (c4 == -1) break
|
||||
out += String.fromCharCode(((c3 & 0x03) << 6) | c4)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function safe64(base64) {
|
||||
base64 = base64.replace(/\+/g, `-`)
|
||||
base64 = base64.replace(/\//g, `_`)
|
||||
return base64
|
||||
}
|
|
@ -0,0 +1,371 @@
|
|||
import prettier from 'prettier/standalone'
|
||||
import prettierCss from 'prettier/parser-postcss'
|
||||
import prettierMarkdown from 'prettier/parser-markdown'
|
||||
import defaultTheme from './themes/default-theme'
|
||||
|
||||
const createCustomTheme = (theme, color) => {
|
||||
const customTheme = JSON.parse(JSON.stringify(theme))
|
||||
customTheme.block.h1[`border-bottom`] = `2px solid ${color}`
|
||||
customTheme.block.h2[`background`] = color
|
||||
customTheme.block.h3[`border-left`] = `3px solid ${color}`
|
||||
customTheme.block.h4[`color`] = color
|
||||
customTheme.inline.strong[`color`] = color
|
||||
return customTheme
|
||||
}
|
||||
|
||||
// 设置自定义颜色
|
||||
export function setColorWithTemplate(theme) {
|
||||
return (color) => {
|
||||
return createCustomTheme(theme, color)
|
||||
}
|
||||
}
|
||||
|
||||
export function setColorWithCustomTemplate(theme, color) {
|
||||
return createCustomTheme(theme, color)
|
||||
}
|
||||
|
||||
// 设置自定义字体大小
|
||||
export function setFontSizeWithTemplate(template) {
|
||||
return function (fontSize) {
|
||||
const customTheme = JSON.parse(JSON.stringify(template))
|
||||
customTheme.block.h1[`font-size`] = `${fontSize * 1.14}px`
|
||||
customTheme.block.h2[`font-size`] = `${fontSize * 1.1}px`
|
||||
customTheme.block.h3[`font-size`] = `${fontSize}px`
|
||||
customTheme.block.h4[`font-size`] = `${fontSize}px`
|
||||
return customTheme
|
||||
}
|
||||
}
|
||||
|
||||
export const setColor = setColorWithTemplate(defaultTheme)
|
||||
export const setFontSize = setFontSizeWithTemplate(defaultTheme)
|
||||
|
||||
export function customCssWithTemplate(jsonString, color, theme) {
|
||||
// block
|
||||
const customTheme = createCustomTheme(theme, color)
|
||||
|
||||
customTheme.block.h1 = Object.assign(customTheme.block.h1, jsonString.h1)
|
||||
customTheme.block.h2 = Object.assign(customTheme.block.h2, jsonString.h2)
|
||||
customTheme.block.h3 = Object.assign(customTheme.block.h3, jsonString.h3)
|
||||
customTheme.block.h4 = Object.assign(customTheme.block.h4, jsonString.h4)
|
||||
customTheme.block.code = Object.assign(
|
||||
customTheme.block.code,
|
||||
jsonString.code
|
||||
)
|
||||
customTheme.block.p = Object.assign(customTheme.block.p, jsonString.p)
|
||||
customTheme.block.hr = Object.assign(customTheme.block.hr, jsonString.hr)
|
||||
customTheme.block.blockquote = Object.assign(
|
||||
customTheme.block.blockquote,
|
||||
jsonString.blockquote
|
||||
)
|
||||
customTheme.block.blockquote_p = Object.assign(
|
||||
customTheme.block.blockquote_p,
|
||||
jsonString.blockquote_p
|
||||
)
|
||||
customTheme.block.image = Object.assign(
|
||||
customTheme.block.image,
|
||||
jsonString.image
|
||||
)
|
||||
|
||||
// inline
|
||||
customTheme.inline.strong = Object.assign(
|
||||
customTheme.inline.strong,
|
||||
jsonString.strong
|
||||
)
|
||||
customTheme.inline.codespan = Object.assign(
|
||||
customTheme.inline.codespan,
|
||||
jsonString.codespan
|
||||
)
|
||||
customTheme.inline.link = Object.assign(
|
||||
customTheme.inline.link,
|
||||
jsonString.link
|
||||
)
|
||||
customTheme.inline.wx_link = Object.assign(
|
||||
customTheme.inline.wx_link,
|
||||
jsonString.wx_link
|
||||
)
|
||||
customTheme.block.ul = Object.assign(customTheme.block.ul, jsonString.ul)
|
||||
customTheme.block.ol = Object.assign(customTheme.block.ol, jsonString.ol)
|
||||
customTheme.inline.listitem = Object.assign(
|
||||
customTheme.inline.listitem,
|
||||
jsonString.li
|
||||
)
|
||||
return customTheme
|
||||
}
|
||||
|
||||
/**
|
||||
* 将CSS形式的字符串转换为JSON
|
||||
*
|
||||
* @param {string} css - css字符串
|
||||
*/
|
||||
export function css2json(css) {
|
||||
// 移除CSS所有注释
|
||||
let open, close
|
||||
while (
|
||||
(open = css.indexOf(`/*`)) !== -1 &&
|
||||
(close = css.indexOf(`*/`)) !== -1
|
||||
) {
|
||||
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
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function toObject(array) {
|
||||
let ret = {}
|
||||
array.forEach((e) => {
|
||||
const index = e.indexOf(`:`)
|
||||
const property = e.substring(0, index).trim()
|
||||
ret[property] = e.substring(index + 1).trim()
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// 切割声明块并移除空白符,然后放入数组中
|
||||
let declarations = css
|
||||
.substring(lbracket + 1, rbracket)
|
||||
.split(`;`)
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e.length > 0) // 移除所有""空值
|
||||
|
||||
// 转为Object对象
|
||||
declarations = toObject(declarations)
|
||||
|
||||
// 第二步:选择器处理,每个选择器会与它对应的声明相关联,如:
|
||||
// `h1, p#bar {color: red}`
|
||||
// ==>
|
||||
// {"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
|
||||
}
|
||||
|
||||
/**
|
||||
* 将编辑器内容保存到 LocalStorage
|
||||
* @param {*} editor
|
||||
* @param {*} name
|
||||
*/
|
||||
export function saveEditorContent(editor, name) {
|
||||
const content = editor.getValue(0)
|
||||
if (content) {
|
||||
localStorage.setItem(name, content)
|
||||
} else {
|
||||
localStorage.removeItem(name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文档
|
||||
* @param {string} content - 文档内容
|
||||
*/
|
||||
export function formatDoc(content) {
|
||||
return prettier.format(content, {
|
||||
parser: `markdown`,
|
||||
plugins: [prettierMarkdown],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化css
|
||||
* @param {string} content - css内容
|
||||
*/
|
||||
export function formatCss(content) {
|
||||
return prettier.format(content, {
|
||||
parser: `css`,
|
||||
plugins: [prettierCss],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出原始 Markdown 文档
|
||||
* @param {string} doc - 文档内容
|
||||
*/
|
||||
export function downloadMD(doc) {
|
||||
const downLink = document.createElement(`a`)
|
||||
|
||||
downLink.download = `content.md`
|
||||
downLink.style.display = `none`
|
||||
const blob = new Blob([doc])
|
||||
|
||||
downLink.href = URL.createObjectURL(blob)
|
||||
document.body.appendChild(downLink)
|
||||
downLink.click()
|
||||
document.body.removeChild(downLink)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 HTML 生成内容
|
||||
*/
|
||||
export function exportHTML() {
|
||||
const element = document.querySelector(`#output`)
|
||||
setStyles(element)
|
||||
const htmlStr = element.innerHTML
|
||||
|
||||
const downLink = document.createElement(`a`)
|
||||
|
||||
downLink.download = `content.html`
|
||||
downLink.style.display = `none`
|
||||
let blob = new Blob([
|
||||
`<html><head><meta charset="utf-8" /></head><body><div style="width: 750px; margin: auto;">${htmlStr}</div></body></html>`,
|
||||
])
|
||||
|
||||
downLink.href = URL.createObjectURL(blob)
|
||||
document.body.appendChild(downLink)
|
||||
downLink.click()
|
||||
document.body.removeChild(downLink)
|
||||
|
||||
function setStyles(element) {
|
||||
/**
|
||||
* 获取一个 DOM 元素的所有样式,
|
||||
* @param {DOM 元素} element DOM 元素
|
||||
* @param {排除的属性} excludes 如果某些属性对结果有不良影响,可以使用这个参数来排除
|
||||
* @returns 行内样式拼接结果
|
||||
*/
|
||||
function getElementStyles(element, excludes = [`width`, `height`]) {
|
||||
const styles = getComputedStyle(element, null)
|
||||
return Object.entries(styles)
|
||||
.filter(
|
||||
([key]) => styles.getPropertyValue(key) && !excludes.includes(key)
|
||||
)
|
||||
.map(([key, value]) => `${key}:${value};`)
|
||||
.join(``)
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case isPre(element):
|
||||
case isCode(element):
|
||||
case isSpan(element):
|
||||
element.setAttribute(`style`, getElementStyles(element))
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
default:
|
||||
}
|
||||
if (element.children.length) {
|
||||
Array.from(element.children).forEach((child) => setStyles(child))
|
||||
}
|
||||
|
||||
// 判断是否是包裹代码块的 pre 元素
|
||||
function isPre(element) {
|
||||
return (
|
||||
element.tagName === `PRE` &&
|
||||
Array.from(element.classList).includes(`code__pre`)
|
||||
)
|
||||
}
|
||||
|
||||
// 判断是否是包裹代码块的 code 元素
|
||||
function isCode(element) {
|
||||
return (
|
||||
element.tagName === `CODE` &&
|
||||
Array.from(element.classList).includes(`prettyprint`)
|
||||
)
|
||||
}
|
||||
|
||||
// 判断是否是包裹代码字符的 span 元素
|
||||
function isSpan(element) {
|
||||
return (
|
||||
element.tagName === `SPAN` &&
|
||||
(isCode(element.parentElement) ||
|
||||
isCode(element.parentElement.parentElement))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成列表字符串
|
||||
* @param {*} data 对应内容集合
|
||||
* @param {*} rows 行
|
||||
* @param {*} cols 列
|
||||
*/
|
||||
export function createTable({ data, rows, cols }) {
|
||||
let table = ``
|
||||
for (let i = 0; i < rows + 2; ++i) {
|
||||
table += `| `
|
||||
const currRow = []
|
||||
for (let j = 0; j < cols; ++j) {
|
||||
const rowIdx = i > 1 ? i - 1 : i
|
||||
currRow.push(i === 1 ? `---` : data[`k_${rowIdx}_${j}`] || ` `)
|
||||
}
|
||||
table += currRow.join(` | `)
|
||||
table += ` |\n`
|
||||
}
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
export function toBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result.split(`,`).pop())
|
||||
reader.onerror = (error) => reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
export function checkImage(file) {
|
||||
// check filename suffix
|
||||
const isValidSuffix = /\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name)
|
||||
if (!isValidSuffix) {
|
||||
return {
|
||||
ok: false,
|
||||
msg: `请上传 JPG/PNG/GIF 格式的图片`,
|
||||
}
|
||||
}
|
||||
|
||||
// check file size
|
||||
const maxSize = 10
|
||||
const valid = file.size / 1024 / 1024 <= maxSize
|
||||
if (!valid) {
|
||||
return {
|
||||
ok: false,
|
||||
msg: `由于公众号限制,图片大小不能超过 ${maxSize}M`,
|
||||
}
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除左边多余空格
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
export function removeLeft(str) {
|
||||
const lines = str.split(`\n`)
|
||||
// 获取应该删除的空白符数量
|
||||
const minSpaceNum = lines
|
||||
.filter((item) => item.trim())
|
||||
.map((item) => item.match(/(^\s+)?/)[0].length)
|
||||
.sort((a, b) => a - b)[0]
|
||||
// 删除空白符
|
||||
return lines.map((item) => item.slice(minSpaceNum)).join(`\n`)
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="关于"
|
||||
class="about__dialog"
|
||||
:visible="visible"
|
||||
@close="$emit('close')"
|
||||
width="30%"
|
||||
center
|
||||
>
|
||||
<div style="text-align: center">
|
||||
<h3>一款高度简洁的微信 Markdown 编辑器</h3>
|
||||
<p>扫码关注公众号 Doocs,原创技术文章第一时间推送!</p>
|
||||
<img
|
||||
src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png"
|
||||
style="width: 40%"
|
||||
/>
|
||||
</div>
|
||||
<template slot="footer">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="onRedirect('https://github.com/doocs/md')"
|
||||
plain
|
||||
>
|
||||
GitHub 仓库
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="onRedirect('https://gitee.com/doocs/md')"
|
||||
plain
|
||||
>
|
||||
Gitee 仓库
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onRedirect(url) {
|
||||
window.open(url)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/deep/ .el-dialog {
|
||||
min-width: 420px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<transition enter-active-class="bounceInRight">
|
||||
<el-col :span="12" v-show="showCssEditor" class="cssEditor-wrapper">
|
||||
<textarea
|
||||
id="cssEditor"
|
||||
type="textarea"
|
||||
placeholder="Your custom css here."
|
||||
>
|
||||
</textarea>
|
||||
</el-col>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: `CssEditor`,
|
||||
props: {
|
||||
showCssEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.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>
|
|
@ -0,0 +1,56 @@
|
|||
<template>
|
||||
<el-dialog title="发布" :visible.sync="form.dialogVisible">
|
||||
<el-alert
|
||||
style="margin-bottom: 1em"
|
||||
title="注:此功能由第三方浏览器插件支持,本平台不保证安全性。"
|
||||
type="info"
|
||||
show-icon
|
||||
>
|
||||
</el-alert>
|
||||
<el-form
|
||||
class="postInfo"
|
||||
label-position="right"
|
||||
label-width="50px"
|
||||
:model="form"
|
||||
>
|
||||
<el-form-item label="封面">
|
||||
<el-input
|
||||
v-model="form.thumb"
|
||||
placeholder="自动提取第一张图"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题">
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
placeholder="自动提取第一个标题"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="描述">
|
||||
<el-input
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
v-model="form.desc"
|
||||
placeholder="自动提取第一个段落"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template slot="footer" class="dialog-footer">
|
||||
<el-button @click="$emit('close')">取 消</el-button>
|
||||
<el-button type="primary" @click="$emit('post')">确 定</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: `PostInfoDialog`,
|
||||
props: {
|
||||
form: {
|
||||
type: Object,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="提示"
|
||||
class="reset__dialog"
|
||||
:visible="showResetConfirm"
|
||||
@close="$emit('close')"
|
||||
center
|
||||
>
|
||||
<div style="text-align: center">此操作将丢失本地自定义样式,是否继续?</div>
|
||||
<template slot="footer">
|
||||
<el-button :type="btnType" @click="$emit('close')" plain>
|
||||
取 消
|
||||
</el-button>
|
||||
<el-button :type="btnType" @click="$emit('confirm')" plain>
|
||||
确 定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'pinia'
|
||||
import { useStore } from '@/stores'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
showResetConfirm: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
btnType() {
|
||||
return this.nightMode ? `default` : `primary`
|
||||
},
|
||||
...mapState(useStore, {
|
||||
nightMode: (state) => state.nightMode,
|
||||
}),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/deep/ .el-dialog {
|
||||
min-width: 440px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<el-dropdown placement="right" class="style-option-menu">
|
||||
<div class="el-dropdown-link">
|
||||
{{ label }}
|
||||
<i class="el-icon-arrow-right el-icon--right"></i>
|
||||
</div>
|
||||
<el-dropdown-menu slot="dropdown" style="width: 200px">
|
||||
<el-dropdown-item
|
||||
v-for="{ value, label, desc } in options"
|
||||
:key="value"
|
||||
:label="label"
|
||||
:value="value"
|
||||
@click.native="charge(value)"
|
||||
>
|
||||
<i
|
||||
class="el-icon-check"
|
||||
:style="{ opacity: current === value ? 1 : 0 }"
|
||||
></i>
|
||||
{{ label }}
|
||||
<span class="select-item-right">{{ desc }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: `StyleOptionMenu`,
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
current: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
charge: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.style-option-menu.el-dropdown {
|
||||
margin: 0;
|
||||
width: 150px;
|
||||
|
||||
.el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.select-item-right {
|
||||
float: right;
|
||||
color: #8492a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,496 @@
|
|||
<template>
|
||||
<el-container class="header-container is-dark">
|
||||
<div class="dropdowns">
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
文件<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="refClick">
|
||||
<i class="el-icon-upload2"></i>
|
||||
导入 .md
|
||||
<input hidden type="file" ref="fileInput" accept=".md" />
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="$emit('download')">
|
||||
<i class="el-icon-download"></i>
|
||||
导出 .md
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="$emit('export')">
|
||||
<i class="el-icon-document"></i>
|
||||
导出 .html
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click.native="themeChanged">
|
||||
<i
|
||||
class="el-icon-check"
|
||||
:style="{ opacity: nightMode ? 1 : 0 }"
|
||||
></i>
|
||||
暗黑模式
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
格式<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
class="format-item"
|
||||
v-for="{ label, kbd, emitArgs } in formatItems"
|
||||
:key="kbd"
|
||||
@click.native="$emit(...emitArgs)"
|
||||
>
|
||||
{{ label }}
|
||||
<kbd>{{ kbd }}</kbd>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click.native="statusChanged">
|
||||
<i
|
||||
class="el-icon-check"
|
||||
:style="{ opacity: citeStatus ? 1 : 0 }"
|
||||
></i>
|
||||
微信外链转底部引用
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
编辑<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="$emit('show-dialog-upload-img')">
|
||||
<i class="el-icon-upload"></i>
|
||||
上传图片
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="$emit('show-dialog-form')">
|
||||
<i class="el-icon-s-grid"></i>
|
||||
插入表格
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
样式<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item class="padding-left-3">
|
||||
<style-option-menu
|
||||
label="字体"
|
||||
:options="config.builtinFonts"
|
||||
:current="selectFont"
|
||||
:charge="fontChanged"
|
||||
></style-option-menu>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="padding-left-3">
|
||||
<style-option-menu
|
||||
label="字号"
|
||||
:options="config.sizeOption"
|
||||
:current="selectSize"
|
||||
:charge="sizeChanged"
|
||||
></style-option-menu>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="padding-left-3">
|
||||
<style-option-menu
|
||||
label="颜色"
|
||||
:options="config.colorOption"
|
||||
:current="selectColor"
|
||||
:charge="colorChanged"
|
||||
></style-option-menu>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="padding-left-3">
|
||||
<style-option-menu
|
||||
label="代码主题"
|
||||
:options="config.codeThemeOption"
|
||||
:current="selectCodeTheme"
|
||||
:charge="codeThemeChanged"
|
||||
></style-option-menu>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
divided
|
||||
class="padding-left-3"
|
||||
@click.native="showPicker()"
|
||||
>
|
||||
自定义颜色
|
||||
<el-color-picker
|
||||
show-alpha
|
||||
ref="colorPicker"
|
||||
size="mini"
|
||||
style="float: right; margin-top: 3px"
|
||||
v-model="selectColor"
|
||||
@change="colorChanged"
|
||||
></el-color-picker>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="padding-left-3" @click.native="customStyle">
|
||||
自定义 CSS
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click.native="codeBlockChanged">
|
||||
<i
|
||||
class="el-icon-check"
|
||||
:style="{ opacity: isMacCodeBlock ? 1 : 0 }"
|
||||
></i>
|
||||
Mac 代码块
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
divided
|
||||
class="padding-left-3"
|
||||
@click.native="showResetConfirm = true"
|
||||
>
|
||||
重置
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
帮助<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="$emit('show-about-dialog')">
|
||||
关于
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<el-button plain size="medium" :type="btnType" @click="copy">
|
||||
复制
|
||||
</el-button>
|
||||
<!-- <el-button plain size="medium" :type="btnType" @click="prePost">
|
||||
发布
|
||||
</el-button> -->
|
||||
|
||||
<post-info-dialog
|
||||
:form="form"
|
||||
@post="post"
|
||||
@close="form.dialogVisible = false"
|
||||
>
|
||||
</post-info-dialog>
|
||||
<reset-dialog
|
||||
:show-reset-confirm="showResetConfirm"
|
||||
@confirm="confirmReset"
|
||||
@close="cancelReset"
|
||||
></reset-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useStore } from '@/stores'
|
||||
|
||||
import { setFontSize, setColorWithCustomTemplate } from '@/assets/scripts/util'
|
||||
import { solveWeChatImage, mergeCss } from '@/assets/scripts/converter'
|
||||
import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt'
|
||||
import config from '@/assets/scripts/config'
|
||||
import ResetDialog from './ResetDialog'
|
||||
import StyleOptionMenu from './StyleOptionMenu'
|
||||
import PostInfoDialog from './PostInfoDialog'
|
||||
|
||||
export default {
|
||||
name: `editor-header`,
|
||||
data() {
|
||||
return {
|
||||
config,
|
||||
citeStatus: false,
|
||||
isMacCodeBlock: true,
|
||||
showResetConfirm: false,
|
||||
selectFont: ``,
|
||||
selectSize: ``,
|
||||
selectColor: ``,
|
||||
selectCodeTheme: config.codeThemeOption[2].value,
|
||||
form: {
|
||||
dialogVisible: false,
|
||||
title: ``,
|
||||
desc: ``,
|
||||
thumb: ``,
|
||||
content: ``,
|
||||
},
|
||||
formatItems: [
|
||||
{
|
||||
label: `加粗`,
|
||||
kbd: `Ctrl + B`,
|
||||
emitArgs: [`addFormat`, `**`],
|
||||
},
|
||||
{
|
||||
label: `斜体`,
|
||||
kbd: `Ctrl + I`,
|
||||
emitArgs: [`addFormat`, `*`],
|
||||
},
|
||||
{
|
||||
label: `删除线`,
|
||||
kbd: `Alt + Shift + U`,
|
||||
emitArgs: [`addFormat`, `~~`],
|
||||
},
|
||||
{
|
||||
label: `超链接`,
|
||||
kbd: `Alt + Shift + K`,
|
||||
emitArgs: [`addFormat`, `[`, `]()`],
|
||||
},
|
||||
{
|
||||
label: `格式化`,
|
||||
kbd: `Alt + Shift + L`,
|
||||
emitArgs: [`formatContent`],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
components: {
|
||||
PostInfoDialog,
|
||||
StyleOptionMenu,
|
||||
ResetDialog,
|
||||
},
|
||||
computed: {
|
||||
btnType() {
|
||||
return this.nightMode ? `default` : `primary`
|
||||
},
|
||||
...mapState(useStore, {
|
||||
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,
|
||||
currentCiteStatus: (state) => state.citeStatus,
|
||||
currentIsMacCodeBlock: (state) => state.isMacCodeBlock,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
refClick() {
|
||||
this.$refs.fileInput.click()
|
||||
},
|
||||
showPicker() {
|
||||
this.$refs.colorPicker.showPicker = true
|
||||
},
|
||||
prePost() {
|
||||
let auto = {}
|
||||
try {
|
||||
auto = {
|
||||
thumb: document.querySelector(`#output img`).src,
|
||||
title: [1, 2, 3, 4, 5, 6]
|
||||
.map((h) => document.querySelector(`#output h${h}`))
|
||||
.filter((h) => h)[0].innerText,
|
||||
desc: document.querySelector(`#output p`).innerText,
|
||||
content: this.output,
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`error`, error)
|
||||
}
|
||||
this.form = {
|
||||
dialogVisible: true,
|
||||
...auto,
|
||||
auto,
|
||||
}
|
||||
},
|
||||
post() {
|
||||
this.form.dialogVisible = false
|
||||
// 使用 window.$syncer 可以检测是否安装插件
|
||||
window.syncPost({
|
||||
title: this.form.title || this.form.auto.title,
|
||||
desc: this.form.desc || this.form.auto.desc,
|
||||
content: this.form.content || this.form.auto.content,
|
||||
thumb: this.form.thumb || this.form.auto.thumb,
|
||||
})
|
||||
},
|
||||
fontChanged(fonts) {
|
||||
this.setWxRendererOptions({
|
||||
fonts: fonts,
|
||||
})
|
||||
this.setCurrentFont(fonts)
|
||||
this.selectFont = fonts
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
sizeChanged(size) {
|
||||
let theme = setFontSize(size.replace(`px`, ``))
|
||||
theme = setColorWithCustomTemplate(theme, this.currentColor)
|
||||
this.setWxRendererOptions({
|
||||
size: size,
|
||||
theme: theme,
|
||||
})
|
||||
this.setCurrentSize(size)
|
||||
this.selectSize = size
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
colorChanged(color) {
|
||||
let theme = setFontSize(this.currentSize.replace(`px`, ``))
|
||||
|
||||
theme = setColorWithCustomTemplate(theme, color)
|
||||
this.setWxRendererOptions({
|
||||
theme: theme,
|
||||
})
|
||||
this.setCurrentColor(color)
|
||||
this.selectColor = color
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
codeThemeChanged(theme) {
|
||||
this.setCurrentCodeTheme(theme)
|
||||
this.selectCodeTheme = theme
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
statusChanged() {
|
||||
this.citeStatus = !this.citeStatus
|
||||
this.setCiteStatus(this.citeStatus)
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
codeBlockChanged() {
|
||||
this.isMacCodeBlock = !this.isMacCodeBlock
|
||||
this.setIsMacCodeBlock(this.isMacCodeBlock)
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
// 复制到微信公众号
|
||||
copy() {
|
||||
this.$emit(`startCopy`)
|
||||
setTimeout(() => {
|
||||
solveWeChatImage()
|
||||
|
||||
const clipboardDiv = document.getElementById(`output`)
|
||||
clipboardDiv.innerHTML = mergeCss(clipboardDiv.innerHTML)
|
||||
if (this.isMacCodeBlock) {
|
||||
clipboardDiv.innerHTML = clipboardDiv.innerHTML.replaceAll(
|
||||
/(<code class="prettyprint[^>]*)(style=")/g,
|
||||
`$1style="font-family: Menlo, 'Operator Mono', Consolas, Monaco, monospace;`
|
||||
)
|
||||
}
|
||||
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()
|
||||
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 = localStorage.getItem(`__css_content`)
|
||||
if (!flag) {
|
||||
this.setCssEditorValue(DEFAULT_CSS_CONTENT)
|
||||
}
|
||||
},
|
||||
// 重置样式
|
||||
confirmReset() {
|
||||
this.showResetConfirm = false
|
||||
localStorage.clear()
|
||||
this.cssEditor.setValue(DEFAULT_CSS_CONTENT)
|
||||
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.codeThemeChanged(this.config.codeThemeOption[2].value)
|
||||
this.$emit(`cssChanged`)
|
||||
this.selectFont = this.currentFont
|
||||
this.selectSize = this.currentSize
|
||||
this.selectColor = this.currentColor
|
||||
this.selectCodeTheme = this.codeTheme
|
||||
|
||||
this.isMacCodeBlock = false
|
||||
this.codeBlockChanged()
|
||||
},
|
||||
cancelReset() {
|
||||
this.showResetConfirm = false
|
||||
this.editor.focus()
|
||||
},
|
||||
...mapActions(useStore, [
|
||||
`setCurrentColor`,
|
||||
`setCiteStatus`,
|
||||
`themeChanged`,
|
||||
`setCurrentFont`,
|
||||
`setCurrentSize`,
|
||||
`setCssEditorValue`,
|
||||
`setCurrentCodeTheme`,
|
||||
`setWxRendererOptions`,
|
||||
`setIsMacCodeBlock`,
|
||||
]),
|
||||
},
|
||||
mounted() {
|
||||
this.selectFont = this.currentFont
|
||||
this.selectSize = this.currentSize
|
||||
this.selectColor = this.currentColor
|
||||
this.selectCodeTheme = this.codeTheme
|
||||
this.citeStatus = this.currentCiteStatus
|
||||
this.isMacCodeBlock = this.currentIsMacCodeBlock
|
||||
|
||||
const fileInput = this.$refs.fileInput
|
||||
fileInput.onchange = () => {
|
||||
const file = fileInput.files[0]
|
||||
if (file == null) {
|
||||
return
|
||||
}
|
||||
const read = new FileReader()
|
||||
read.readAsText(file)
|
||||
read.onload = () => {
|
||||
this.$emit(`import-md`, read.result)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-container {
|
||||
padding: 10px 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdowns {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.el-dropdown {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.padding-left-3 {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
// 添加边距影响了 divided 行的移入效果,此处做一个兼容处理
|
||||
.el-dropdown-menu__item--divided.padding-left-3 {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 3em;
|
||||
height: 6px;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.format-item {
|
||||
.padding-left-3;
|
||||
width: 180px;
|
||||
|
||||
kbd {
|
||||
font-size: 0.75em;
|
||||
float: right;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="插入表格"
|
||||
class="insert__dialog"
|
||||
:visible="visible"
|
||||
@close="$emit('close')"
|
||||
border
|
||||
>
|
||||
<el-row class="tb-options" type="flex" align="middle" :gutter="10">
|
||||
<el-col>
|
||||
行数:
|
||||
<el-input-number
|
||||
v-model="rowNum"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="100"
|
||||
size="small"
|
||||
></el-input-number>
|
||||
</el-col>
|
||||
<el-col>
|
||||
列数:
|
||||
<el-input-number
|
||||
v-model="colNum"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="100"
|
||||
size="small"
|
||||
></el-input-number>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<table style="border-collapse: collapse" class="input-table">
|
||||
<tr
|
||||
:class="{ 'head-style': row === 1 }"
|
||||
v-for="row in rowNum + 1"
|
||||
:key="row"
|
||||
>
|
||||
<td v-for="col in colNum" :key="col">
|
||||
<el-input
|
||||
align="center"
|
||||
v-model="tableData[`k_${row - 1}_${col - 1}`]"
|
||||
:placeholder="row === 1 ? '表头' : ''"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :type="btnType" @click="$emit('close')" plain>
|
||||
取 消
|
||||
</el-button>
|
||||
<el-button :type="btnType" @click="insertTable" plain> 确 定 </el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useStore } from '@/stores'
|
||||
|
||||
import config from '@/assets/scripts/config'
|
||||
import { createTable } from '@/assets/scripts/util'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
config: config,
|
||||
rowNum: 3,
|
||||
colNum: 3,
|
||||
tableData: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
btnType() {
|
||||
return this.nightMode ? `default` : `primary`
|
||||
},
|
||||
...mapState(useStore, {
|
||||
nightMode: (state) => state.nightMode,
|
||||
editor: (state) => state.editor,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
// 插入表格
|
||||
insertTable() {
|
||||
const cursor = this.editor.getCursor()
|
||||
const table = createTable({
|
||||
data: this.tableData,
|
||||
rows: this.rowNum,
|
||||
cols: this.colNum,
|
||||
})
|
||||
|
||||
this.tableData = {}
|
||||
this.rowNum = 3
|
||||
this.colNum = 3
|
||||
this.editor.replaceSelection(`\n${table}\n`, `end`)
|
||||
this.$emit(`close`)
|
||||
this.editorRefresh()
|
||||
},
|
||||
...mapActions(useStore, [`editorRefresh`]),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/deep/ .el-dialog {
|
||||
width: 55%;
|
||||
min-height: 375px;
|
||||
min-width: 440px;
|
||||
}
|
||||
|
||||
.tb-options {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-table ::v-deep .el-input__inner {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.head-style /deep/ .el-input__inner {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,126 @@
|
|||
<template>
|
||||
<div
|
||||
id="menu"
|
||||
class="menu"
|
||||
v-show="visible"
|
||||
:style="`left: ${left}px;top: ${top}px;`"
|
||||
>
|
||||
<ul class="menu__group" v-for="(menuItem, index) in menu" :key="index">
|
||||
<li
|
||||
class="menu_item"
|
||||
v-for="{ key, text } in menuItem"
|
||||
:key="key"
|
||||
@mousedown="onMouseDown(key)"
|
||||
>
|
||||
{{ text }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
left: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menu: [
|
||||
[
|
||||
{
|
||||
text: `上传图片`,
|
||||
key: `insertPic`,
|
||||
},
|
||||
{
|
||||
text: `插入表格`,
|
||||
key: `insertTable`,
|
||||
},
|
||||
{
|
||||
text: `恢复默认样式`,
|
||||
key: `resetStyle`,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: `导入 .md 文档`,
|
||||
key: `importMarkdown`,
|
||||
},
|
||||
{
|
||||
text: `导出 .md 文档`,
|
||||
key: `download`,
|
||||
},
|
||||
{
|
||||
text: `导出 .html`,
|
||||
key: `export`,
|
||||
},
|
||||
{
|
||||
text: `格式化`,
|
||||
key: `formatMarkdown`,
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onMouseDown(key) {
|
||||
this.$emit(`menuTick`, key)
|
||||
this.$emit(`closeMenu`)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.menu {
|
||||
position: absolute;
|
||||
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);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.menu__group {
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu_item {
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 0 4px 24px;
|
||||
margin-top: 10px;
|
||||
min-width: 200px;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
::v-deep .el-upload {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,733 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="本地上传"
|
||||
class="upload__dialog"
|
||||
:visible="visible"
|
||||
@close="$emit('close')"
|
||||
>
|
||||
<el-tabs type="activeName" v-model="activeName">
|
||||
<el-tab-pane class="upload-panel" label="选择上传" name="upload">
|
||||
<el-select
|
||||
v-model="imgHost"
|
||||
@change="changeImgHost"
|
||||
placeholder="请选择"
|
||||
size="small"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-upload
|
||||
drag
|
||||
action=""
|
||||
:headers="{ 'Content-Type': 'multipart/form-data' }"
|
||||
:show-file-list="false"
|
||||
:multiple="true"
|
||||
accept=".jpg, .jpeg, .png, .gif"
|
||||
name="file"
|
||||
:before-upload="beforeImageUpload"
|
||||
:http-request="uploadImage"
|
||||
>
|
||||
<i class="el-icon-upload"></i>
|
||||
<div class="el-upload__text">
|
||||
将图片拖到此处,或
|
||||
<em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-tab-pane>
|
||||
<!-- <el-tab-pane class="github-panel" label="Gitee 图床" name="gitee">
|
||||
<el-form
|
||||
class="setting-form"
|
||||
:model="formGitee"
|
||||
label-position="right"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="Gitee 仓库" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formGitee.repo"
|
||||
placeholder="如:gitee.com/yanglbme/resource"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分支">
|
||||
<el-input
|
||||
v-model.trim="formGitee.branch"
|
||||
placeholder="如:release,可不填,默认 master"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="私人令牌" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formGitee.accessToken"
|
||||
show-password
|
||||
placeholder="如:cc1d0c1426d0fd0902bd2d7184b14da61b8abc46"
|
||||
></el-input>
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://gitee.com/profile/personal_access_tokens"
|
||||
target="_blank"
|
||||
>请在 Gitee「设置->安全设置->私人令牌」中生成</el-link
|
||||
>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveGiteeConfiguration"
|
||||
>保存配置</el-button
|
||||
>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane> -->
|
||||
<el-tab-pane class="github-panel" label="GitHub 图床" name="github">
|
||||
<el-form
|
||||
class="setting-form"
|
||||
:model="formGitHub"
|
||||
label-position="right"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="GitHub 仓库" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formGitHub.repo"
|
||||
placeholder="如:github.com/yanglbme/resource"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="分支">
|
||||
<el-input
|
||||
v-model.trim="formGitHub.branch"
|
||||
placeholder="如:release,可不填,默认 master"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Token" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formGitHub.accessToken"
|
||||
show-password
|
||||
placeholder="如:cc1d0c1426d0fd0902bd2d7184b14da61b8abc46"
|
||||
></el-input>
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token"
|
||||
target="_blank"
|
||||
>如何获取 GitHub Token?
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveGitHubConfiguration"
|
||||
>保存配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane class="github-panel" label="阿里云 OSS" name="aliOSS">
|
||||
<el-form
|
||||
class="setting-form"
|
||||
:model="formAliOSS"
|
||||
label-position="right"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="AccessKey ID" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formAliOSS.accessKeyId"
|
||||
placeholder="如:LTAI4GdoocsmdoxUf13ylbaNHk"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="AccessKey Secret" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formAliOSS.accessKeySecret"
|
||||
show-password
|
||||
placeholder="如:cc1d0c142doocs0902bd2d7md4b14da6ylbabc46"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formAliOSS.bucket"
|
||||
placeholder="如:doocs"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket 所在区域" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formAliOSS.region"
|
||||
placeholder="如:oss-cn-shenzhen"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="自定义 CDN 域名" :required="false">
|
||||
<el-input
|
||||
v-model.trim="formAliOSS.cdnHost"
|
||||
placeholder="如:https://imagecdn.alidaodao.com,可不填"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储路径">
|
||||
<el-input
|
||||
v-model.trim="formAliOSS.path"
|
||||
placeholder="如:img,可不填,默认为根目录"
|
||||
></el-input>
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://help.aliyun.com/document_detail/31883.html"
|
||||
target="_blank"
|
||||
>如何使用阿里云 OSS?
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveAliOSSConfiguration">
|
||||
保存配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane class="github-panel" label="腾讯云 COS" name="txCOS">
|
||||
<el-form
|
||||
class="setting-form"
|
||||
:model="formTxCOS"
|
||||
label-position="right"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="SecretId" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formTxCOS.secretId"
|
||||
placeholder="如:AKIDnQp1w3DOOCSs8F5MDp9tdoocsmdUPonW3"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="SecretKey" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formTxCOS.secretKey"
|
||||
show-password
|
||||
placeholder="如:ukLmdtEJ9271f3DOocsMDsCXdS3YlbW0"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formTxCOS.bucket"
|
||||
placeholder="如:doocs-3212520134"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket 所在区域" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formTxCOS.region"
|
||||
placeholder="如:ap-guangzhou"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="自定义 CDN 域名" :required="false">
|
||||
<el-input
|
||||
v-model.trim="formTxCOS.cdnHost"
|
||||
placeholder="如:https://imagecdn.alidaodao.com,可不填"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储路径">
|
||||
<el-input
|
||||
v-model.trim="formTxCOS.path"
|
||||
placeholder="如:img,可不填,默认根目录"
|
||||
></el-input>
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://cloud.tencent.com/document/product/436/38484"
|
||||
target="_blank"
|
||||
>如何使用腾讯云 COS?
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveTxCOSConfiguration">
|
||||
保存配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane class="github-panel" label="七牛云 Kodo" name="qiniu">
|
||||
<el-form
|
||||
class="setting-form"
|
||||
:model="formQiniu"
|
||||
label-position="right"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="AccessKey" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formQiniu.accessKey"
|
||||
placeholder="如:6DD3VaLJ_SQgOdoocsyTV_YWaDmdnL2n8EGx7kG"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="SecretKey" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formQiniu.secretKey"
|
||||
show-password
|
||||
placeholder="如:qgZa5qrvDOOcsmdKStD1oCjZ9nB7MDvJUs_34SIm"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formQiniu.bucket"
|
||||
placeholder="如:md"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket 对应域名" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formQiniu.domain"
|
||||
placeholder="如:https://images.123ylb.cn"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储区域" :required="true">
|
||||
<el-input
|
||||
v-model.trim="formQiniu.region"
|
||||
placeholder="如:z2"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="存储路径" :required="false">
|
||||
<el-input
|
||||
v-model.trim="formQiniu.path"
|
||||
placeholder="如:img,可不填,默认为根目录"
|
||||
></el-input>
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://developer.qiniu.com/kodo"
|
||||
target="_blank"
|
||||
>如何使用七牛云 Kodo?
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveQiniuConfiguration">
|
||||
保存配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane class="github-panel" label="MinIO" name="minio">
|
||||
<el-form
|
||||
class="setting-form"
|
||||
:model="minioOSS"
|
||||
label-position="right"
|
||||
label-width="140px"
|
||||
>
|
||||
<el-form-item label="Endpoint" :required="true">
|
||||
<el-input
|
||||
v-model.trim="minioOSS.endpoint"
|
||||
placeholder="如:play.min.io"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="Port" :required="false">
|
||||
<el-input
|
||||
type="number"
|
||||
v-model.trim="minioOSS.port"
|
||||
placeholder="如:9000,可不填,http 默认为 80,https 默认为 443"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="UseSSL" :required="true">
|
||||
<el-switch
|
||||
v-model="minioOSS.useSSL"
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
>
|
||||
</el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item label="Bucket" :required="true">
|
||||
<el-input
|
||||
v-model.trim="minioOSS.bucket"
|
||||
placeholder="如:doocs"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="AccessKey" :required="true">
|
||||
<el-input
|
||||
v-model.trim="minioOSS.accessKey"
|
||||
placeholder="如:zhangsan"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="SecretKey" :required="true">
|
||||
<el-input
|
||||
v-model.trim="minioOSS.secretKey"
|
||||
placeholder="如:asdasdasd"
|
||||
></el-input>
|
||||
<el-link
|
||||
type="primary"
|
||||
href="http://docs.minio.org.cn/docs/master/minio-client-complete-guide"
|
||||
target="_blank"
|
||||
>如何使用 MinIO?
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="saveMinioOSSConfiguration">
|
||||
保存配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane
|
||||
class="github-panel formCustom"
|
||||
label="自定义代码"
|
||||
name="formCustom"
|
||||
>
|
||||
<el-form
|
||||
class="setting-form"
|
||||
:model="formCustom"
|
||||
label-position="right"
|
||||
>
|
||||
<el-form-item label="" :required="true">
|
||||
<el-input
|
||||
class="formCustomElInput"
|
||||
ref="formCustomElInput"
|
||||
type="textarea"
|
||||
resize="none"
|
||||
placeholder="Your custom code here."
|
||||
v-model="formCustom.code"
|
||||
>
|
||||
</el-input>
|
||||
<el-link
|
||||
type="primary"
|
||||
href="https://github.com/doocs/md#自定义上传逻辑"
|
||||
target="_blank"
|
||||
>参数详情?
|
||||
</el-link>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="formCustomSave">
|
||||
保存配置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { checkImage, removeLeft } from '@/assets/scripts/util'
|
||||
import CodeMirror from 'codemirror/lib/codemirror'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
activeName: `upload`,
|
||||
|
||||
formGitHub: {
|
||||
repo: ``,
|
||||
branch: ``,
|
||||
accessToken: ``,
|
||||
},
|
||||
// formGitee: {
|
||||
// repo: ``,
|
||||
// branch: ``,
|
||||
// accessToken: ``,
|
||||
// },
|
||||
formAliOSS: {
|
||||
accessKeyId: ``,
|
||||
accessKeySecret: ``,
|
||||
bucket: ``,
|
||||
region: ``,
|
||||
path: ``,
|
||||
cdnHost: ``,
|
||||
},
|
||||
minioOSS: {
|
||||
endpoint: ``,
|
||||
port: ``,
|
||||
useSSL: true,
|
||||
bucket: ``,
|
||||
accessKey: ``,
|
||||
secretKey: ``,
|
||||
},
|
||||
formTxCOS: {
|
||||
secretId: ``,
|
||||
secretKey: ``,
|
||||
bucket: ``,
|
||||
region: ``,
|
||||
path: ``,
|
||||
cdnHost: ``,
|
||||
},
|
||||
formQiniu: {
|
||||
accessKey: ``,
|
||||
secretKey: ``,
|
||||
bucket: ``,
|
||||
domain: ``,
|
||||
region: ``,
|
||||
},
|
||||
formCustom: {
|
||||
code:
|
||||
localStorage.getItem(`formCustomConfig`) ||
|
||||
removeLeft(`
|
||||
const {file, util, okCb, errCb} = CUSTOM_ARG
|
||||
const param = new FormData()
|
||||
param.append('file', file)
|
||||
util.axios.post('${window.location.origin}/upload', param, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
}).then(res => {
|
||||
okCb(res.url)
|
||||
}).catch(err => {
|
||||
errCb(err)
|
||||
})
|
||||
`).trim(),
|
||||
editor: undefined,
|
||||
},
|
||||
options: [
|
||||
{
|
||||
value: `default`,
|
||||
label: `默认`,
|
||||
},
|
||||
// {
|
||||
// value: `gitee`,
|
||||
// label: `Gitee`,
|
||||
// },
|
||||
{
|
||||
value: `github`,
|
||||
label: `GitHub`,
|
||||
},
|
||||
{
|
||||
value: `aliOSS`,
|
||||
label: `阿里云`,
|
||||
},
|
||||
{
|
||||
value: `txCOS`,
|
||||
label: `腾讯云`,
|
||||
},
|
||||
{
|
||||
value: `qiniu`,
|
||||
label: `七牛云`,
|
||||
},
|
||||
{
|
||||
value: `minio`,
|
||||
label: `MinIO`,
|
||||
},
|
||||
{
|
||||
value: `formCustom`,
|
||||
label: `自定义代码`,
|
||||
},
|
||||
],
|
||||
imgHost: `default`,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
if (localStorage.getItem(`githubConfig`)) {
|
||||
this.formGitHub = JSON.parse(localStorage.getItem(`githubConfig`))
|
||||
}
|
||||
// if (localStorage.getItem(`giteeConfig`)) {
|
||||
// this.formGitee = JSON.parse(localStorage.getItem(`giteeConfig`))
|
||||
// }
|
||||
if (localStorage.getItem(`aliOSSConfig`)) {
|
||||
this.formAliOSS = JSON.parse(localStorage.getItem(`aliOSSConfig`))
|
||||
}
|
||||
if (localStorage.getItem(`minioConfig`)) {
|
||||
this.minioOSS = JSON.parse(localStorage.getItem(`minioConfig`))
|
||||
}
|
||||
if (localStorage.getItem(`txCOSConfig`)) {
|
||||
this.formTxCOS = JSON.parse(localStorage.getItem(`txCOSConfig`))
|
||||
}
|
||||
if (localStorage.getItem(`imgHost`)) {
|
||||
this.imgHost = localStorage.getItem(`imgHost`)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeImgHost() {
|
||||
localStorage.setItem(`imgHost`, this.imgHost)
|
||||
this.$message.success(`已成功切换图床`)
|
||||
},
|
||||
saveGitHubConfiguration() {
|
||||
if (!(this.formGitHub.repo && this.formGitHub.accessToken)) {
|
||||
const blankElement = this.formGitHub.repo ? `token` : `GitHub 仓库`
|
||||
this.$message.error(`参数「${blankElement}」不能为空`)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(`githubConfig`, JSON.stringify(this.formGitHub))
|
||||
this.$message.success(`保存成功`)
|
||||
},
|
||||
// saveGiteeConfiguration() {
|
||||
// if (!(this.formGitee.repo && this.formGitee.accessToken)) {
|
||||
// const blankElement = this.formGitee.repo ? `私人令牌` : `Gitee 仓库`
|
||||
// this.$message.error(`参数「${blankElement}」不能为空`)
|
||||
// return
|
||||
// }
|
||||
// localStorage.setItem(`giteeConfig`, JSON.stringify(this.formGitee))
|
||||
// this.$message.success(`保存成功`)
|
||||
// },
|
||||
saveAliOSSConfiguration() {
|
||||
if (
|
||||
!(
|
||||
this.formAliOSS.accessKeyId &&
|
||||
this.formAliOSS.accessKeySecret &&
|
||||
this.formAliOSS.bucket &&
|
||||
this.formAliOSS.region
|
||||
)
|
||||
) {
|
||||
this.$message.error(`阿里云 OSS 参数配置不全`)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(`aliOSSConfig`, JSON.stringify(this.formAliOSS))
|
||||
this.$message.success(`保存成功`)
|
||||
},
|
||||
saveMinioOSSConfiguration() {
|
||||
if (
|
||||
!(
|
||||
this.minioOSS.endpoint &&
|
||||
this.minioOSS.bucket &&
|
||||
this.minioOSS.accessKey &&
|
||||
this.minioOSS.secretKey
|
||||
)
|
||||
) {
|
||||
this.$message.error(`MinIO 参数配置不全`)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(`minioConfig`, JSON.stringify(this.minioOSS))
|
||||
this.$message.success(`保存成功`)
|
||||
},
|
||||
saveTxCOSConfiguration() {
|
||||
if (
|
||||
!(
|
||||
this.formTxCOS.secretId &&
|
||||
this.formTxCOS.secretKey &&
|
||||
this.formTxCOS.bucket &&
|
||||
this.formTxCOS.region
|
||||
)
|
||||
) {
|
||||
this.$message.error(`腾讯云 COS 参数配置不全`)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(`txCOSConfig`, JSON.stringify(this.formTxCOS))
|
||||
this.$message.success(`保存成功`)
|
||||
},
|
||||
|
||||
saveQiniuConfiguration() {
|
||||
if (
|
||||
!(
|
||||
this.formQiniu.accessKey &&
|
||||
this.formQiniu.secretKey &&
|
||||
this.formQiniu.bucket &&
|
||||
this.formQiniu.domain &&
|
||||
this.formQiniu.region
|
||||
)
|
||||
) {
|
||||
this.$message.error(`七牛云 Kodo 参数配置不全`)
|
||||
return
|
||||
}
|
||||
localStorage.setItem(`qiniuConfig`, JSON.stringify(this.formQiniu))
|
||||
this.$message.success(`保存成功`)
|
||||
},
|
||||
formCustomSave() {
|
||||
const str = this.formCustom.editor.getValue()
|
||||
localStorage.setItem(`formCustomConfig`, str)
|
||||
this.$message.success(`保存成功`)
|
||||
},
|
||||
|
||||
beforeImageUpload(file) {
|
||||
// check 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(params) {
|
||||
this.$emit(`uploadImage`, params.file)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeName: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val === `formCustom`) {
|
||||
this.$nextTick(() => {
|
||||
const textarea =
|
||||
this.$refs.formCustomElInput.$el.querySelector(`textarea`)
|
||||
this.formCustom.editor =
|
||||
this.formCustom.editor ||
|
||||
CodeMirror.fromTextArea(textarea, {
|
||||
mode: `javascript`,
|
||||
})
|
||||
this.formCustom.editor.setValue(this.formCustom.code)
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.upload__dialog {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
/deep/ .el-dialog {
|
||||
width: 55%;
|
||||
min-width: 640px;
|
||||
min-height: 615px;
|
||||
// 消除固定的行内样式,配合 flex 布局居中元素
|
||||
margin: auto !important;
|
||||
}
|
||||
|
||||
/deep/ .el-upload-dragger {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
justify-content: center;
|
||||
width: 500px;
|
||||
height: 360px;
|
||||
|
||||
.el-icon-upload {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .el-dialog__body {
|
||||
padding-bottom: 50px;
|
||||
}
|
||||
|
||||
.upload-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
|
||||
.el-select {
|
||||
align-self: flex-end;
|
||||
margin: 0 67.75px 20px;
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.github-panel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
&.formCustom {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.formCustomElInput {
|
||||
/deep/ .CodeMirror {
|
||||
border: 1px solid #eee;
|
||||
height: 300px !important;
|
||||
font-family: 'Fira Mono', 'DejaVu Sans Mono', Menlo, Consolas,
|
||||
'Liberation Mono', Monaco, 'Lucida Console', monospace !important;
|
||||
line-height: 20px;
|
||||
|
||||
.CodeMirror-scroll {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.setting-form {
|
||||
width: 100%;
|
||||
|
||||
.el-form-item {
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.el-form-item:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<transition name="fade" v-if="loading">
|
||||
<div
|
||||
class="loading"
|
||||
:class="{
|
||||
loading_night: nightMode,
|
||||
}"
|
||||
>
|
||||
<strong>致力于让 Markdown 编辑更简单</strong>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'pinia'
|
||||
import { useStore } from '@/stores'
|
||||
|
||||
export default {
|
||||
name: `RunLoading`,
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 100)
|
||||
},
|
||||
computed: {
|
||||
...mapState(useStore, {
|
||||
nightMode: ({ nightMode }) => nightMode,
|
||||
}),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@light-color: #303133;
|
||||
@light-background-color: #f2f2f2;
|
||||
@night-color: #bbbbbb;
|
||||
@night-background-color: #303133;
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
font-size: 18px;
|
||||
color: @light-color;
|
||||
background-color: @light-background-color;
|
||||
|
||||
&::before {
|
||||
content: url('../assets/images/favicon.png');
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading_night {
|
||||
color: @night-color;
|
||||
background-color: @night-background-color;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-to,
|
||||
.fade-leave {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 1s;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||
import Vue from 'vue'
|
||||
import ElementUI from 'element-ui'
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
||||
|
||||
import 'element-ui/lib/theme-chalk/index.css'
|
||||
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/theme/xq-light.css'
|
||||
|
||||
import 'codemirror/mode/css/css'
|
||||
import 'codemirror/mode/markdown/markdown'
|
||||
import 'codemirror/addon/edit/closebrackets'
|
||||
import 'codemirror/addon/edit/matchbrackets'
|
||||
import 'codemirror/addon/selection/active-line'
|
||||
import 'codemirror/addon/hint/show-hint'
|
||||
import 'codemirror/addon/hint/css-hint'
|
||||
|
||||
import './plugins/element'
|
||||
import App from './App'
|
||||
|
||||
Vue.use(ElementUI).use(PiniaVuePlugin)
|
||||
|
||||
Vue.config.productionTip = false
|
||||
|
||||
App.mpType = `app`
|
||||
|
||||
new Vue({
|
||||
...App,
|
||||
pinia: createPinia(),
|
||||
}).$mount(`#app`)
|
|
@ -0,0 +1,7 @@
|
|||
import Vue from 'vue'
|
||||
import { Loading, Message } from 'element-ui'
|
||||
|
||||
Vue.component(Message.name, Message)
|
||||
|
||||
Vue.prototype.$loading = Loading.service
|
||||
Vue.prototype.$message = Message
|
|
@ -0,0 +1,190 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { marked } from 'marked'
|
||||
import CodeMirror from 'codemirror/lib/codemirror'
|
||||
|
||||
import config from '../assets/scripts/config'
|
||||
import WxRenderer from '../assets/scripts/renderers/wx-renderer'
|
||||
import DEFAULT_CONTENT from '@/assets/example/markdown.md'
|
||||
import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt'
|
||||
import { setColor, formatDoc, formatCss } from '@/assets/scripts/util'
|
||||
|
||||
export const useStore = defineStore(`store`, {
|
||||
state: () => ({
|
||||
wxRenderer: null,
|
||||
output: ``,
|
||||
html: ``,
|
||||
editor: null,
|
||||
cssEditor: null,
|
||||
currentFont: ``,
|
||||
currentSize: ``,
|
||||
currentColor: ``,
|
||||
citeStatus: false,
|
||||
nightMode: false,
|
||||
codeTheme: config.codeThemeOption[2].value,
|
||||
isMacCodeBlock: true,
|
||||
}),
|
||||
actions: {
|
||||
setEditorValue(data) {
|
||||
this.editor.setValue(data)
|
||||
},
|
||||
setCssEditorValue(data) {
|
||||
this.cssEditor.setValue(data)
|
||||
},
|
||||
setWxRendererOptions(data) {
|
||||
this.wxRenderer.setOptions(data)
|
||||
},
|
||||
setCiteStatus(data) {
|
||||
this.citeStatus = data
|
||||
localStorage.setItem(`citeStatus`, data)
|
||||
},
|
||||
setCurrentFont(data) {
|
||||
this.currentFont = data
|
||||
localStorage.setItem(`fonts`, data)
|
||||
},
|
||||
setCurrentSize(data) {
|
||||
this.currentSize = data
|
||||
localStorage.setItem(`size`, data)
|
||||
},
|
||||
setCurrentColor(data) {
|
||||
this.currentColor = data
|
||||
localStorage.setItem(`color`, data)
|
||||
},
|
||||
setCurrentCodeTheme(data) {
|
||||
this.codeTheme = data
|
||||
localStorage.setItem(`codeTheme`, data)
|
||||
},
|
||||
setIsMacCodeBlock(data) {
|
||||
this.isMacCodeBlock = data
|
||||
localStorage.setItem(`isMacCodeBlock`, data)
|
||||
},
|
||||
themeChanged() {
|
||||
this.nightMode = !this.nightMode
|
||||
localStorage.setItem(`nightMode`, this.nightMode)
|
||||
},
|
||||
initEditorState() {
|
||||
this.currentFont =
|
||||
localStorage.getItem(`fonts`) || config.builtinFonts[0].value
|
||||
this.currentColor =
|
||||
localStorage.getItem(`color`) || config.colorOption[0].value
|
||||
this.currentSize =
|
||||
localStorage.getItem(`size`) || config.sizeOption[2].value
|
||||
this.codeTheme =
|
||||
localStorage.getItem(`codeTheme`) || config.codeThemeOption[2].value
|
||||
this.citeStatus = localStorage.getItem(`citeStatus`) === `true`
|
||||
this.nightMode = localStorage.getItem(`nightMode`) === `true`
|
||||
this.isMacCodeBlock = !(
|
||||
localStorage.getItem(`isMacCodeBlock`) === `false`
|
||||
)
|
||||
this.wxRenderer = new WxRenderer({
|
||||
theme: setColor(this.currentColor),
|
||||
fonts: this.currentFont,
|
||||
size: this.currentSize,
|
||||
})
|
||||
},
|
||||
initEditorEntity() {
|
||||
const editorDom = document.getElementById(`editor`)
|
||||
|
||||
if (!editorDom.value) {
|
||||
editorDom.value =
|
||||
localStorage.getItem(`__editor_content`) || formatDoc(DEFAULT_CONTENT)
|
||||
}
|
||||
this.editor = CodeMirror.fromTextArea(editorDom, {
|
||||
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) {},
|
||||
'Ctrl-B': function bold(editor) {
|
||||
const selected = editor.getSelection()
|
||||
editor.replaceSelection(`**${selected}**`)
|
||||
},
|
||||
'Ctrl-D': function del(editor) {
|
||||
const selected = editor.getSelection()
|
||||
editor.replaceSelection(`~~${selected}~~`)
|
||||
},
|
||||
'Ctrl-I': function italic(editor) {
|
||||
const selected = editor.getSelection()
|
||||
editor.replaceSelection(`*${selected}*`)
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
initCssEditorEntity() {
|
||||
const cssEditorDom = document.getElementById(`cssEditor`)
|
||||
|
||||
if (!cssEditorDom.value) {
|
||||
cssEditorDom.value =
|
||||
localStorage.getItem(`__css_content`) || DEFAULT_CSS_CONTENT
|
||||
}
|
||||
this.cssEditor = CodeMirror.fromTextArea(cssEditorDom, {
|
||||
mode: `css`,
|
||||
theme: `style-mirror`,
|
||||
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() {
|
||||
const renderer = this.wxRenderer.getRenderer(this.citeStatus)
|
||||
marked.setOptions({ renderer })
|
||||
let output = marked.parse(this.editor.getValue(0))
|
||||
|
||||
// 去除第一行的 margin-top
|
||||
output = output.replace(/(style=".*?)"/, `$1;margin-top: 0"`)
|
||||
if (this.citeStatus) {
|
||||
// 引用脚注
|
||||
output += this.wxRenderer.buildFootnotes()
|
||||
// 附加的一些 style
|
||||
output += this.wxRenderer.buildAddition()
|
||||
}
|
||||
|
||||
if (this.isMacCodeBlock) {
|
||||
output += `
|
||||
<style>
|
||||
.hljs.code__pre::before {
|
||||
position: initial;
|
||||
padding: initial;
|
||||
content: '';
|
||||
display: block;
|
||||
height: 25px;
|
||||
background-color: transparent;
|
||||
background-image: url("https://doocs.oss-cn-shenzhen.aliyuncs.com/img/123.svg");
|
||||
background-position: 14px 10px;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 40px;
|
||||
}
|
||||
|
||||
.hljs.code__pre {
|
||||
padding: 0!important;
|
||||
}
|
||||
|
||||
.hljs.code__pre code {
|
||||
display: -webkit-box;
|
||||
padding: 0.5em 1em 1em;
|
||||
overflow-x: auto;
|
||||
text-indent: 0;
|
||||
}
|
||||
</style>
|
||||
`
|
||||
}
|
||||
this.output = output
|
||||
},
|
||||
},
|
||||
})
|
|
@ -0,0 +1,705 @@
|
|||
<template>
|
||||
<div
|
||||
class="container"
|
||||
:class="{ container_night: nightMode }"
|
||||
@keydown.alt.shift.k="addFormat('[', ']()')"
|
||||
@keydown.alt.shift.u="addFormat('~~')"
|
||||
@keydown.ctrl.alt.l="formatContent()"
|
||||
>
|
||||
<el-container>
|
||||
<el-header class="editor__header">
|
||||
<editor-header
|
||||
ref="header"
|
||||
@addFormat="addFormat"
|
||||
@formatContent="formatContent"
|
||||
@refresh="onEditorRefresh"
|
||||
@cssChanged="cssChanged"
|
||||
@import-md="importMD"
|
||||
@download="downloadEditorContent"
|
||||
@export="exportEditorContent"
|
||||
@showCssEditor="showCssEditor = !showCssEditor"
|
||||
@show-about-dialog="aboutDialogVisible = true"
|
||||
@show-dialog-form="insertFormDialogVisible = true"
|
||||
@show-dialog-upload-img="dialogUploadImgVisible = true"
|
||||
@startCopy=";(isCoping = true), (backLight = true)"
|
||||
@endCopy="endCopy"
|
||||
></editor-header>
|
||||
</el-header>
|
||||
<el-main class="main-body">
|
||||
<el-row class="main-section">
|
||||
<el-col
|
||||
:span="12"
|
||||
class="codeMirror-wrapper"
|
||||
ref="codeMirrorWrapper"
|
||||
@contextmenu.prevent.native="openMenu"
|
||||
>
|
||||
<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>
|
||||
<css-editor :show-css-editor="showCssEditor"></css-editor>
|
||||
</el-row>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
<upload-img-dialog
|
||||
:visible="dialogUploadImgVisible"
|
||||
@close="dialogUploadImgVisible = false"
|
||||
@beforeUpload="beforeUpload"
|
||||
@uploadImage="uploadImage"
|
||||
@uploaded="uploaded"
|
||||
></upload-img-dialog>
|
||||
<about-dialog
|
||||
:visible="aboutDialogVisible"
|
||||
@close="aboutDialogVisible = false"
|
||||
></about-dialog>
|
||||
<insert-form-dialog
|
||||
:visible="insertFormDialogVisible"
|
||||
@close="insertFormDialogVisible = false"
|
||||
></insert-form-dialog>
|
||||
|
||||
<right-click-menu
|
||||
:visible="rightClickMenuVisible"
|
||||
:left="mouseLeft"
|
||||
:top="mouseTop"
|
||||
@menuTick="onMenuEvent"
|
||||
@closeMenu="rightClickMenuVisible = false"
|
||||
></right-click-menu>
|
||||
<run-loading></run-loading>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useStore } from '@/stores'
|
||||
|
||||
import EditorHeader from '@/components/CodemirrorEditor/EditorHeader/index'
|
||||
import AboutDialog from '@/components/CodemirrorEditor/AboutDialog'
|
||||
import InsertFormDialog from '@/components/CodemirrorEditor/InsertFormDialog'
|
||||
import RightClickMenu from '@/components/CodemirrorEditor/RightClickMenu'
|
||||
import UploadImgDialog from '@/components/CodemirrorEditor/UploadImgDialog'
|
||||
import CssEditor from '@/components/CodemirrorEditor/CssEditor'
|
||||
import RunLoading from '@/components/RunLoading'
|
||||
|
||||
import {
|
||||
css2json,
|
||||
downloadMD,
|
||||
exportHTML,
|
||||
formatDoc,
|
||||
setFontSize,
|
||||
saveEditorContent,
|
||||
customCssWithTemplate,
|
||||
checkImage,
|
||||
toBase64,
|
||||
} from '@/assets/scripts/util'
|
||||
import fileApi from '../api/file'
|
||||
|
||||
require(`codemirror/mode/javascript/javascript`)
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
showCssEditor: false,
|
||||
aboutDialogVisible: false,
|
||||
dialogUploadImgVisible: false,
|
||||
insertFormDialogVisible: false,
|
||||
isCoping: false,
|
||||
isImgLoading: false,
|
||||
backLight: false,
|
||||
timeout: null,
|
||||
changeTimer: null,
|
||||
source: ``,
|
||||
mouseLeft: 0,
|
||||
mouseTop: 0,
|
||||
rightClickMenuVisible: false,
|
||||
}
|
||||
},
|
||||
components: {
|
||||
CssEditor,
|
||||
RunLoading,
|
||||
EditorHeader,
|
||||
AboutDialog,
|
||||
InsertFormDialog,
|
||||
RightClickMenu,
|
||||
UploadImgDialog,
|
||||
},
|
||||
computed: {
|
||||
...mapState(useStore, {
|
||||
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,
|
||||
codeTheme: (state) => state.codeTheme,
|
||||
}),
|
||||
},
|
||||
created() {
|
||||
this.initEditorState()
|
||||
this.$nextTick(() => {
|
||||
this.initEditor()
|
||||
this.initCssEditor()
|
||||
this.onEditorRefresh()
|
||||
this.mdLocalToRemote()
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
// 转换 markdown 中的本地图片为线上图片
|
||||
// todo 处理事件覆盖
|
||||
mdLocalToRemote() {
|
||||
const vm = this
|
||||
const dom = this.$refs.codeMirrorWrapper.$el
|
||||
|
||||
dom.ondragover = (evt) => evt.preventDefault()
|
||||
dom.ondrop = async (evt) => {
|
||||
evt.preventDefault()
|
||||
for (const item of evt.dataTransfer.items) {
|
||||
item.getAsFileSystemHandle().then(async (handle) => {
|
||||
if (handle.kind === `directory`) {
|
||||
const list = await showFileStructure(handle)
|
||||
const md = await getMd({ list })
|
||||
uploadMdImg({ md, list })
|
||||
} else {
|
||||
const file = await handle.getFile()
|
||||
console.log(`file`, file)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 从文件列表中查找一个 md 文件并解析
|
||||
async function getMd({ list }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { path, file } = list.find((item) => item.path.match(/\.md$/))
|
||||
const reader = new FileReader()
|
||||
reader.readAsText(file, `UTF-8`)
|
||||
reader.onload = (evt) => {
|
||||
resolve({
|
||||
str: evt.target.result,
|
||||
file,
|
||||
path,
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 上传 md 中的图片
|
||||
async function uploadMdImg({ md, list }) {
|
||||
const mdImgList = [
|
||||
...(md.str.matchAll(/!\[(.*?)\]\((.*?)\)/gm) || []),
|
||||
].filter((item) => {
|
||||
return item // 获取所有相对地址的图片
|
||||
})
|
||||
const root = md.path.match(/.+?\//)[0]
|
||||
const resList = await Promise.all(
|
||||
mdImgList.map((item) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let [, , matchStr] = item
|
||||
matchStr = matchStr.replace(/^.\//, ``) // 处理 ./img/ 为 img/ 统一相对路径风格
|
||||
const { file } =
|
||||
list.find((f) => f.path === `${root}${matchStr}`) || {}
|
||||
vm.uploadImage(file, (url) => {
|
||||
resolve({ matchStr, url })
|
||||
})
|
||||
})
|
||||
})
|
||||
)
|
||||
resList.forEach((item) => {
|
||||
md.str = md.str
|
||||
.replace(`](./${item.matchStr})`, `](${item.url})`)
|
||||
.replace(`](${item.matchStr})`, `](${item.url})`)
|
||||
})
|
||||
vm.editor.setValue(md.str)
|
||||
console.log(`resList`, resList, md.str)
|
||||
}
|
||||
|
||||
// 转换文件系统句柄中的文件为文件列表
|
||||
async function showFileStructure(root) {
|
||||
const result = []
|
||||
let cwd = ``
|
||||
try {
|
||||
const dirs = [root]
|
||||
for (const dir of dirs) {
|
||||
cwd += dir.name + `/`
|
||||
for await (const [, handle] of dir) {
|
||||
if (handle.kind === `file`) {
|
||||
result.push({
|
||||
path: cwd + handle.name,
|
||||
file: await handle.getFile(),
|
||||
})
|
||||
} else {
|
||||
result.push({
|
||||
path: cwd + handle.name + `/`,
|
||||
})
|
||||
dirs.push(handle)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
},
|
||||
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.rightClickMenuVisible = false
|
||||
})
|
||||
this.editor.on(`blur`, () => {
|
||||
//!影响到右键菜单的点击事件,右键菜单的点击事件在组件内通过mousedown触发
|
||||
this.rightClickMenuVisible = false
|
||||
})
|
||||
this.editor.on(`scroll`, () => {
|
||||
this.rightClickMenuVisible = 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()
|
||||
},
|
||||
// 切换 highlight.js 代码主题
|
||||
codeThemeChanged() {
|
||||
let cssUrl = this.codeTheme
|
||||
let el = document.getElementById(`hljs`)
|
||||
if (el != undefined) {
|
||||
el.setAttribute(`href`, cssUrl)
|
||||
} else {
|
||||
const link = document.createElement(`link`)
|
||||
link.setAttribute(`type`, `text/css`)
|
||||
link.setAttribute(`rel`, `stylesheet`)
|
||||
link.setAttribute(`href`, cssUrl)
|
||||
link.setAttribute(`id`, `hljs`)
|
||||
document.head.appendChild(link)
|
||||
}
|
||||
},
|
||||
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, cb) {
|
||||
this.isImgLoading = true
|
||||
toBase64(file)
|
||||
.then((base64Content) => {
|
||||
fileApi
|
||||
.fileUpload(base64Content, file)
|
||||
.then((url) => {
|
||||
console.log(url)
|
||||
cb ? cb(url) : this.uploaded(url)
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$message.error(err.message)
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
this.$message.error(err.message)
|
||||
})
|
||||
this.isImgLoading = false
|
||||
},
|
||||
// 图片上传结束
|
||||
uploaded(response) {
|
||||
console.log(`图片上传之后: `, response)
|
||||
if (!response) {
|
||||
this.$message.error(`上传图片未知异常`)
|
||||
return
|
||||
}
|
||||
this.dialogUploadImgVisible = false
|
||||
// 上传成功,获取光标
|
||||
const cursor = this.editor.getCursor()
|
||||
const imageUrl = response
|
||||
const markdownImage = ``
|
||||
// 将 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.codeThemeChanged(this.codeTheme)
|
||||
this.editorRefresh()
|
||||
setTimeout(() => window.PR.prettyPrint(), 0)
|
||||
},
|
||||
// 复制结束
|
||||
endCopy() {
|
||||
this.backLight = false
|
||||
setTimeout(() => {
|
||||
this.isCoping = false
|
||||
}, 800)
|
||||
},
|
||||
// 工具函数,添加格式
|
||||
addFormat(before, after = before) {
|
||||
const { head, anchor } = this.editor.doc.sel.ranges[0]
|
||||
let start
|
||||
let end
|
||||
// 确定起始关系
|
||||
if (head.line === anchor.line) {
|
||||
if (head.ch < anchor.ch) {
|
||||
start = head
|
||||
end = anchor
|
||||
} else {
|
||||
start = anchor
|
||||
end = head
|
||||
}
|
||||
} else {
|
||||
if (head.line < anchor.line) {
|
||||
start = head
|
||||
end = anchor
|
||||
} else {
|
||||
start = anchor
|
||||
end = head
|
||||
}
|
||||
}
|
||||
|
||||
const rows = []
|
||||
let row = ``
|
||||
for (const c of this.editor.getValue()) {
|
||||
if (c === `\n`) {
|
||||
rows.push(row)
|
||||
row = ``
|
||||
} else {
|
||||
row += c
|
||||
}
|
||||
}
|
||||
rows.push(row)
|
||||
|
||||
let txt = ``
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i]
|
||||
for (let j = 0; j < row.length; j++) {
|
||||
if (i === start.line && j === start.ch) {
|
||||
txt += before
|
||||
}
|
||||
if (i === end.line && j === end.ch) {
|
||||
txt += after
|
||||
}
|
||||
txt += row[j]
|
||||
}
|
||||
//* 特殊情况,结束光标在末尾,无法遍历到
|
||||
if (i === end.line && row.length === end.ch) {
|
||||
txt += after
|
||||
}
|
||||
txt += `\n`
|
||||
}
|
||||
this.editor.setValue(txt)
|
||||
},
|
||||
importMD(md) {
|
||||
this.editor.setValue(md)
|
||||
this.onEditorRefresh()
|
||||
},
|
||||
// 导出编辑器内容到本地
|
||||
downloadEditorContent() {
|
||||
downloadMD(this.editor.getValue(0))
|
||||
},
|
||||
// 导出编辑器内容为 HTML,并且下载到本地
|
||||
exportEditorContent() {
|
||||
this.$nextTick(() => {
|
||||
exportHTML()
|
||||
document.getElementById(`output`).innerHTML = this.output
|
||||
})
|
||||
},
|
||||
// 导入 Markdown 文档
|
||||
importMarkdownContent() {
|
||||
let menu = document.getElementById(`menu`)
|
||||
let input = document.createElement(`input`)
|
||||
input.type = `file`
|
||||
input.name = `filename`
|
||||
input.accept = `.txt,.md`
|
||||
menu.appendChild(input)
|
||||
input.onchange = () => {
|
||||
if (!input.files) {
|
||||
return
|
||||
}
|
||||
const file = input.files[0]
|
||||
if (!/\.(txt|TXT|MD|md)$/.test(file.name)) {
|
||||
this.$message.error(`不支持的文档格式`)
|
||||
return
|
||||
}
|
||||
const reader = new FileReader()
|
||||
reader.readAsText(file)
|
||||
reader.onload = (event) => {
|
||||
let txt = event.target.result
|
||||
txt = formatDoc(txt)
|
||||
if (txt) {
|
||||
localStorage.setItem(`__editor_content`, txt)
|
||||
this.editor.setValue(txt)
|
||||
this.$message.success(`文档导入成功`)
|
||||
}
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
menu.removeChild(input)
|
||||
},
|
||||
// 格式化文档
|
||||
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.rightClickMenuVisible = true
|
||||
},
|
||||
onMenuEvent(type) {
|
||||
switch (type) {
|
||||
case `resetStyle`:
|
||||
this.$refs.header.showResetConfirm = true
|
||||
break
|
||||
case `insertPic`:
|
||||
this.dialogUploadImgVisible = true
|
||||
break
|
||||
case `download`:
|
||||
this.downloadEditorContent()
|
||||
break
|
||||
case `export`:
|
||||
this.exportEditorContent()
|
||||
break
|
||||
case `insertTable`:
|
||||
this.insertFormDialogVisible = true
|
||||
break
|
||||
case `importMarkdown`:
|
||||
this.importMarkdownContent()
|
||||
break
|
||||
case `formatMarkdown`:
|
||||
this.formatContent()
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
...mapActions(useStore, [
|
||||
`initEditorState`,
|
||||
`initEditorEntity`,
|
||||
`setWxRendererOptions`,
|
||||
`editorRefresh`,
|
||||
`initCssEditorEntity`,
|
||||
]),
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.leftAndRightScroll()
|
||||
window.PR.prettyPrint()
|
||||
}, 300)
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.editor__header {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<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%);
|
||||
}
|
||||
}
|
||||
|
||||
/deep/ .preview-table {
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.codeMirror-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import url('../assets/less/app.less');
|
||||
</style>
|
|
@ -0,0 +1,35 @@
|
|||
const isProd = process.env.NODE_ENV === `production`
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* md4 algorithm is not available anymore in NodeJS 17+ (because of lib SSL 3).
|
||||
* In that case, silently replace md4 by md5 algorithm.
|
||||
*/
|
||||
try {
|
||||
crypto.createHash('md4');
|
||||
} catch (e) {
|
||||
const origCreateHash = crypto.createHash;
|
||||
crypto.createHash = (alg, opts) => {
|
||||
return origCreateHash(alg === 'md4' ? 'md5' : alg, opts);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
lintOnSave: true,
|
||||
publicPath: process.env.SERVER_ENV === `NETLIFY` ? `/` : `/md/`, // 基本路径, 建议以绝对路径跟随访问目录
|
||||
configureWebpack: (config) => {
|
||||
config.module.rules.push({
|
||||
test: /\.(txt|md)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: `raw-loader`,
|
||||
},
|
||||
],
|
||||
})
|
||||
},
|
||||
productionSourceMap: !isProd,
|
||||
css: {
|
||||
sourceMap: !isProd,
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue