取消同步页面

This commit is contained in:
DLLCNX 2023-08-03 15:00:19 +08:00
commit 935f09a737
51 changed files with 33991 additions and 0 deletions

18
.editorconfig Normal file
View File

@ -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

25
.eslintrc.js Normal file
View File

@ -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`,
},
}

48
.gitignore vendored Normal file
View File

@ -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

4
.husky/pre-commit Normal file
View File

@ -0,0 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm run lint

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": true
}
}

13
LICENSE Normal file
View File

@ -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.

8
README.md Normal file
View File

@ -0,0 +1,8 @@
# 前言
一直想直接写一套文章在博客和公众号同步使用因为习惯markdown而其对公众号的排版有点问题所以找到了这款专门给公众号排版的项目但是做了一些适合自己的需求。
## 修改记录
1. 去除原项目自动同步功能,因为同步插件已经停止维护,并且很多功能已经无法使用,所以去除了自动同步功能,只保留公众号模版复制功能
2. 将项目作为了自己的本地文档的存储库,添加本地文章列表菜单,可以同步切换展示
3. 默认文档底部添加公众号链接

3
babel.config.js Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
presets: [`@vue/cli-plugin-babel/preset`],
}

9
jsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

1
mm/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
httpData/

4
mm/apiWeb.json Normal file
View File

@ -0,0 +1,4 @@
{
"paths": {},
"disable": []
}

65
mm/mm.config.js Normal file
View File

@ -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`,
},
],
}
}

22
mm/readme.md Normal file
View File

@ -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/)

112
mm/util.js Normal file
View File

@ -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,
}

28618
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

60
package.json Normal file
View File

@ -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

43
public/index.html Normal file
View File

@ -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
public/upload/.gitkeep Normal file
View File

77
src/App.vue Normal file
View File

@ -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>

55
src/api/config.js Normal file
View File

@ -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 }

30
src/api/fetch.js Normal file
View File

@ -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

334
src/api/file.js Normal file
View File

@ -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,
}

View File

@ -0,0 +1 @@
# sdfdsf

View File

@ -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

132
src/assets/less/app.less Normal file
View File

@ -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%;
}

View File

@ -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;
}

162
src/assets/less/theme.less Normal file
View File

@ -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;
}

View File

@ -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,
},
}

View File

@ -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,
})
}

View File

@ -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, '&nbsp;')
});
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;

View File

@ -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`,
},
},
}

View File

@ -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
}

371
src/assets/scripts/util.js Normal file
View File

@ -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`)
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 默认为 80https 默认为 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>

View File

@ -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>

30
src/main.js Normal file
View File

@ -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`)

View File

@ -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

190
src/stores/index.js Normal file
View File

@ -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
},
},
})

View File

@ -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 = `![](${imageUrl})`
// Markdown URL
this.editor.replaceSelection(`\n${markdownImage}\n`, cursor)
this.$message.success(`图片上传成功`)
this.onEditorRefresh()
},
//
leftAndRightScroll() {
const scrollCB = (text) => {
let source, target
clearTimeout(this.timeout)
if (text === `preview`) {
source = this.$refs.preview.$el
target = document.getElementsByClassName(`CodeMirror-scroll`)[0]
this.editor.off(`scroll`, editorScrollCB)
this.timeout = setTimeout(() => {
this.editor.on(`scroll`, editorScrollCB)
}, 300)
} else if (text === `editor`) {
source = document.getElementsByClassName(`CodeMirror-scroll`)[0]
target = this.$refs.preview.$el
target.removeEventListener(`scroll`, previewScrollCB, false)
this.timeout = setTimeout(() => {
target.addEventListener(`scroll`, previewScrollCB, false)
}, 300)
}
let percentage =
source.scrollTop / (source.scrollHeight - source.offsetHeight)
let height = percentage * (target.scrollHeight - target.offsetHeight)
target.scrollTo(0, height)
}
const editorScrollCB = () => {
scrollCB(`editor`)
}
const previewScrollCB = () => {
scrollCB(`preview`)
}
this.$refs.preview.$el.addEventListener(`scroll`, previewScrollCB, false)
this.editor.on(`scroll`, editorScrollCB)
},
//
onEditorRefresh() {
this.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>

35
vue.config.js Normal file
View File

@ -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,
},
}