mirror of
https://github.com/doocs/md.git
synced 2024-11-24 19:10:34 +08:00
refactor: upgrade to vue3 (#309)
This commit is contained in:
parent
9ca58d5f32
commit
fc85253489
25
.eslintrc.js
25
.eslintrc.js
@ -1,25 +0,0 @@
|
|||||||
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`,
|
|
||||||
},
|
|
||||||
}
|
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -23,7 +23,6 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
dist
|
dist
|
||||||
lib
|
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
@ -46,3 +45,8 @@ httpData
|
|||||||
public/upload/**
|
public/upload/**
|
||||||
!public/upload/*.gitkeep
|
!public/upload/*.gitkeep
|
||||||
.history
|
.history
|
||||||
|
|
||||||
|
# Package manager lock file
|
||||||
|
# package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
|
||||||
|
|
||||||
npm run lint
|
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
48
.vscode/settings.json
vendored
48
.vscode/settings.json
vendored
@ -1,5 +1,49 @@
|
|||||||
{
|
{
|
||||||
|
// Disable the default formatter, use eslint instead
|
||||||
|
"prettier.enable": false,
|
||||||
|
"editor.formatOnSave": false,
|
||||||
|
|
||||||
|
// Auto fix
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll": "explicit"
|
"source.fixAll.eslint": "explicit",
|
||||||
}
|
"source.organizeImports": "never"
|
||||||
|
},
|
||||||
|
|
||||||
|
// Silent the stylistic rules in you IDE, but still auto fix them
|
||||||
|
"eslint.rules.customizations": [
|
||||||
|
{ "rule": "style/*", "severity": "off" },
|
||||||
|
{ "rule": "format/*", "severity": "off" },
|
||||||
|
{ "rule": "*-indent", "severity": "off" },
|
||||||
|
{ "rule": "*-spacing", "severity": "off" },
|
||||||
|
{ "rule": "*-spaces", "severity": "off" },
|
||||||
|
{ "rule": "*-order", "severity": "off" },
|
||||||
|
{ "rule": "*-dangle", "severity": "off" },
|
||||||
|
{ "rule": "*-newline", "severity": "off" },
|
||||||
|
{ "rule": "*quotes", "severity": "off" },
|
||||||
|
{ "rule": "*semi", "severity": "off" }
|
||||||
|
],
|
||||||
|
|
||||||
|
// Enable eslint for all supported languages
|
||||||
|
"eslint.validate": [
|
||||||
|
"javascript",
|
||||||
|
"javascriptreact",
|
||||||
|
"typescript",
|
||||||
|
"typescriptreact",
|
||||||
|
"vue",
|
||||||
|
"html",
|
||||||
|
"markdown",
|
||||||
|
"json",
|
||||||
|
"jsonc",
|
||||||
|
"yaml",
|
||||||
|
"toml",
|
||||||
|
"xml",
|
||||||
|
"gql",
|
||||||
|
"graphql",
|
||||||
|
"astro",
|
||||||
|
"css",
|
||||||
|
"less",
|
||||||
|
"scss",
|
||||||
|
"pcss",
|
||||||
|
"postcss"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
16
README.md
16
README.md
@ -76,19 +76,19 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章
|
|||||||
示例代码:
|
示例代码:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const { file, util, okCb, errCb } = CUSTOM_ARG;
|
const { file, util, okCb, errCb } = CUSTOM_ARG
|
||||||
const param = new FormData();
|
const param = new FormData()
|
||||||
param.append("file", file);
|
param.append(`file`, file)
|
||||||
util.axios
|
util.axios
|
||||||
.post("http://127.0.0.1:9000/upload", param, {
|
.post(`http://127.0.0.1:9000/upload`, param, {
|
||||||
headers: { "Content-Type": "multipart/form-data" },
|
headers: { 'Content-Type': `multipart/form-data` },
|
||||||
})
|
})
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
okCb(res.url);
|
okCb(res.url)
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
errCb(err);
|
errCb(err)
|
||||||
});
|
})
|
||||||
|
|
||||||
// 提供的可用参数:
|
// 提供的可用参数:
|
||||||
// CUSTOM_ARG = {
|
// CUSTOM_ARG = {
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [`@vue/cli-plugin-babel/preset`],
|
|
||||||
}
|
|
17
components.json
Normal file
17
components.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://shadcn-vue.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"typescript": true,
|
||||||
|
"tsConfigPath": "./tsconfig.json",
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/assets/index.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true
|
||||||
|
},
|
||||||
|
"framework": "vite",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils"
|
||||||
|
}
|
||||||
|
}
|
17
eslint.config.mjs
Normal file
17
eslint.config.mjs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import antfu from '@antfu/eslint-config'
|
||||||
|
|
||||||
|
export default antfu({
|
||||||
|
vue: true,
|
||||||
|
unocss: true,
|
||||||
|
typescript: true,
|
||||||
|
formatters: true,
|
||||||
|
ignores: [`.github`, `bin`, `md-cli`, `src/assets`],
|
||||||
|
}, {
|
||||||
|
rules: {
|
||||||
|
'semi': [`error`, `never`],
|
||||||
|
'quotes': [`error`, `backtick`],
|
||||||
|
'no-unused-vars': `off`,
|
||||||
|
'no-console': `off`,
|
||||||
|
'no-debugger': `off`,
|
||||||
|
},
|
||||||
|
})
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
@ -24,19 +24,23 @@
|
|||||||
rel="apple-touch-icon-precomposed"
|
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"
|
href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png"
|
||||||
/>
|
/>
|
||||||
<link
|
<!-- <link
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/wechatsync/article-syncjs@latest/dist/styles.css"
|
href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/wechatsync/article-syncjs@latest/dist/styles.css"
|
||||||
/>
|
/> -->
|
||||||
<!-- KaTeX CSS -->
|
<!-- KaTeX CSS -->
|
||||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV" crossorigin="anonymous">
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"
|
||||||
|
integrity="sha384-n8MVd4RsNIU0tAv4ct0nTaAbDJwPJzDEaqSD1odI+WdtXRGWt2kTvGFasHpSy3SV"
|
||||||
|
crossorigin="anonymous"
|
||||||
|
/>
|
||||||
<style>
|
<style>
|
||||||
/**
|
/**
|
||||||
解决公众号复制字体问题
|
解决公众号复制字体问题
|
||||||
*/
|
*/
|
||||||
.katex .mathnormal {
|
.katex .mathnormal {
|
||||||
font-family: "Times New Roman" !important;
|
font-family: 'Times New Roman' !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -46,14 +50,14 @@
|
|||||||
<strong>Please enable JavaScript to continue.</strong>
|
<strong>Please enable JavaScript to continue.</strong>
|
||||||
</noscript>
|
</noscript>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
<script src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/wechatsync/article-syncjs@latest/dist/main.js"></script>
|
<script src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/wechatsync/article-syncjs@latest/dist/main.js"></script>
|
||||||
<!-- 支持一下 mermaid -->
|
<!-- 支持一下 mermaid -->
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs'
|
||||||
mermaid.initialize({ startOnLoad: true });
|
mermaid.initialize({ startOnLoad: true })
|
||||||
window.mermaid = mermaid;
|
window.mermaid = mermaid
|
||||||
</script>
|
</script>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "./",
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
19582
package-lock.json
generated
19582
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
74
package.json
74
package.json
@ -1,62 +1,78 @@
|
|||||||
{
|
{
|
||||||
"name": "md",
|
"name": "md",
|
||||||
|
"type": "module",
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "npm run dev",
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "run-p type-check \"build:only {@}\" --",
|
||||||
|
"build:only": "vite build",
|
||||||
|
"build:h5-netlify": "cross-env SERVER_ENV=NETLIFY vite build",
|
||||||
|
"build:cli": "npm run build && npm run shx rm -rf md-cli/dist && npm run shx rm -rf dist/**/*.map && npm run shx cp -r dist md-cli/ && cd md-cli && npm run pack",
|
||||||
|
"preview": "npm run build && vite preview",
|
||||||
"release:cli": "node ./bin/release.js",
|
"release:cli": "node ./bin/release.js",
|
||||||
"prepare": "husky install",
|
"lint": "eslint . --fix",
|
||||||
"lint": "vue-cli-service lint src",
|
"type-check": "vue-tsc --build --force",
|
||||||
"start": "npm run lint -- --fix && run-p serve",
|
"postinstall": "simple-git-hooks"
|
||||||
"serve": "vue-cli-service serve",
|
|
||||||
"build:h5-netlify": "cross-env SERVER_ENV=NETLIFY vue-cli-service build",
|
|
||||||
"build": "vue-cli-service build",
|
|
||||||
"build-cli": "npm run build && npx shx rm -rf md-cli/dist && npx shx rm -rf dist/**/*.map && npx shx cp -r dist md-cli/ && cd md-cli && npm pack"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@element-plus/icons-vue": "^2.3.1",
|
||||||
|
"@vueuse/core": "^10.11.0",
|
||||||
"ali-oss": "^6.17.1",
|
"ali-oss": "^6.17.1",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
"buffer-from": "^1.1.2",
|
"buffer-from": "^1.1.2",
|
||||||
|
"class-variance-authority": "^0.7.0",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"codemirror": "^5.65.7",
|
"codemirror": "^5.65.7",
|
||||||
"core-js": "^3.34.0",
|
"core-js": "^3.34.0",
|
||||||
"cos-js-sdk-v5": "^1.3.9",
|
"cos-js-sdk-v5": "^1.3.9",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"element-ui": "^2.15.9",
|
"element-plus": "^2.7.6",
|
||||||
"form-data": "4.0.0",
|
"form-data": "4.0.0",
|
||||||
"highlight.js": "^11.6.0",
|
"highlight.js": "^11.6.0",
|
||||||
"juice": "^8.0.0",
|
"juice": "^8.0.0",
|
||||||
"katex": "^0.16.10",
|
"katex": "^0.16.10",
|
||||||
|
"lucide-vue-next": "^0.427.0",
|
||||||
"marked": "^4.0.18",
|
"marked": "^4.0.18",
|
||||||
"marked-katex-extension": "^5.1.0",
|
"marked-katex-extension": "^5.1.0",
|
||||||
"minio": "7.0.33",
|
"minio": "7.0.33",
|
||||||
"node-fetch": "^3.2.10",
|
"node-fetch": "^3.2.10",
|
||||||
"pinia": "^2.1.6",
|
"pinia": "^2.2.2",
|
||||||
"qiniu-js": "^3.4.1",
|
"qiniu-js": "^3.4.1",
|
||||||
|
"radix-vue": "^1.9.4",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"vue": "^2.7.14"
|
"vue": "^3.4.29"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.5.19",
|
"@antfu/eslint-config": "2.21.3",
|
||||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
"@types/codemirror": "^5.60.15",
|
||||||
"@vue/cli-service": "^4.5.15",
|
"@types/marked": "^4.0.0",
|
||||||
"@vue/eslint-config-prettier": "^6.0.0",
|
"@types/node": "^20.14.10",
|
||||||
"async-validator": "^4.0.7",
|
"@unocss/eslint-plugin": "^0.61.2",
|
||||||
"babel-eslint": "^10.1.0",
|
"@vitejs/plugin-vue": "^5.0.5",
|
||||||
"babel-plugin-import": "^1.13.3",
|
"autoprefixer": "^10.4.20",
|
||||||
"cache-loader": "^4.1.0",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-prettier": "^3.4.1",
|
"eslint-plugin-format": "^0.1.2",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"less": "^4.2.0",
|
||||||
"husky": "^8.0.3",
|
|
||||||
"less": "^4.1.2",
|
|
||||||
"less-loader": "^7.3.0",
|
|
||||||
"mini-types": "*",
|
|
||||||
"miniprogram-api-typings": "*",
|
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"postcss-comment": "^2.0.0",
|
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^2.8.8",
|
||||||
"raw-loader": "^4.0.2",
|
|
||||||
"shx": "^0.3.4",
|
"shx": "^0.3.4",
|
||||||
"vue-template-compiler": "^2.7.14"
|
"simple-git-hooks": "^2.11.1",
|
||||||
|
"tailwindcss": "^3.4.10",
|
||||||
|
"typescript": "^5.2.2",
|
||||||
|
"unocss": "^0.61.2",
|
||||||
|
"vite": "^5.4.0",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
|
"vue-tsc": "^2.0.21"
|
||||||
|
},
|
||||||
|
"simple-git-hooks": {
|
||||||
|
"pre-commit": "npx lint-staged"
|
||||||
|
},
|
||||||
|
"lint-staged": {
|
||||||
|
"*": "npm run lint"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
33
src/App.vue
33
src/App.vue
@ -1,20 +1,18 @@
|
|||||||
<template>
|
|
||||||
<div id="app">
|
|
||||||
<codemirror-editor />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import CodemirrorEditor from '@/views/CodemirrorEditor.vue'
|
import CodemirrorEditor from '@/views/CodemirrorEditor.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CodemirrorEditor />
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less">
|
<style lang="less">
|
||||||
// 仿 uniapp 外层全屏
|
// 仿 uniapp 外层全屏
|
||||||
html,
|
html,
|
||||||
body,
|
body,
|
||||||
#app {
|
#app {
|
||||||
width: 100%;
|
width: 100vw;
|
||||||
height: 100%;
|
height: 100vh;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@ -50,7 +48,9 @@ body,
|
|||||||
|
|
||||||
color: #333333;
|
color: #333333;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
box-shadow:
|
||||||
|
0 4px 8px 0 rgba(0, 0, 0, 0.12),
|
||||||
|
0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.CodeMirror-hint {
|
.CodeMirror-hint {
|
||||||
@ -68,4 +68,19 @@ body,
|
|||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 修复分栏线负数 margin 导致的轴向滚动条
|
||||||
|
.el-dropdown-menu__item--divided:before {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修复颜色选择器下拉箭头位置
|
||||||
|
.el-icon.el-color-picker__icon.is-icon-arrow-down {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 参见:https://github.com/element-plus/element-plus/issues/11662
|
||||||
|
.el-dropdown-link:focus-visible {
|
||||||
|
outline: unset;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
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 }
|
|
@ -60,10 +60,8 @@ Google 拥有专门设计的系统,可以自动捕获不适当的预测结果
|
|||||||
|
|
||||||
通常来说,许多词汇都以相同的前缀开头,比如 `need`、`nested` 都以 `ne` 开头,`seed`、`speed` 都以 `s` 开头。要是为每个单词分别存储公共前缀似乎很浪费。
|
通常来说,许多词汇都以相同的前缀开头,比如 `need`、`nested` 都以 `ne` 开头,`seed`、`speed` 都以 `s` 开头。要是为每个单词分别存储公共前缀似乎很浪费。
|
||||||
|
|
||||||
|
|
||||||
![](https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303128008-93cf798d-2662-4eec-8f80-2e07436aebfe.png)
|
![](https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303128008-93cf798d-2662-4eec-8f80-2e07436aebfe.png)
|
||||||
|
|
||||||
|
|
||||||
前缀树是一种利用公共前缀来加速补全速度的数据结构。前缀树在节点树中排列一组单词,单词沿着从根节点到叶子节点的路径存储,树的层次对应于前缀的字母位置。
|
前缀树是一种利用公共前缀来加速补全速度的数据结构。前缀树在节点树中排列一组单词,单词沿着从根节点到叶子节点的路径存储,树的层次对应于前缀的字母位置。
|
||||||
|
|
||||||
前缀的补全是顺着前缀定义的路径来查找的。例如,在上图的前缀树中,前缀 `ne` 对应于从子节点取左边缘 `N` 和唯一边缘 `E` 的路径。然后可以通过继续遍历从 `E` 节点可以达到的所有叶节点来生成补全列表。在图中,`ne` 的补全可以是两个分支:`-ed` 和 `-sted`。如果在数中找不到由前缀定义的路径,则说明词汇表中不包含以该前缀开头的单词。
|
前缀的补全是顺着前缀定义的路径来查找的。例如,在上图的前缀树中,前缀 `ne` 对应于从子节点取左边缘 `N` 和唯一边缘 `E` 的路径。然后可以通过继续遍历从 `E` 节点可以达到的所有叶节点来生成补全列表。在图中,`ne` 的补全可以是两个分支:`-ed` 和 `-sted`。如果在数中找不到由前缀定义的路径,则说明词汇表中不包含以该前缀开头的单词。
|
||||||
|
78
src/assets/index.css
Normal file
78
src/assets/index.css
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,13 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
input,
|
input,
|
||||||
button,
|
button,
|
||||||
textarea {
|
textarea {
|
||||||
@ -23,23 +30,15 @@ em {
|
|||||||
font-style: normal !important;
|
font-style: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
section {
|
||||||
body {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: "PingFang SC", BlinkMacSystemFont, Roboto, "Helvetica Neue",
|
|
||||||
sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.el-message__icon {
|
.el-message__icon {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.web-title {
|
.web-title {
|
||||||
margin: 0 15px 0 5px;
|
margin: 0 15px 0 5px;
|
||||||
}
|
}
|
||||||
@ -51,21 +50,11 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#editor {
|
#editor {
|
||||||
height: 100%;
|
|
||||||
display: block;
|
display: block;
|
||||||
border: none;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
border: none;
|
||||||
|
|
||||||
section {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctrl {
|
.ctrl {
|
||||||
@ -77,18 +66,12 @@ section {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.preview-wrapper {
|
.preview-wrapper {
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
display: flex;
|
||||||
padding: 0;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
display: flex;
|
padding: 0;
|
||||||
//word-break: break-all;
|
|
||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
.main-section {
|
|
||||||
display: flex;
|
|
||||||
height: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
@ -104,7 +87,8 @@ section {
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 60px rgba(0, 0, 0, 0.1);
|
color: var(--el-text-color-regular);
|
||||||
|
box-shadow: var(--el-box-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview table {
|
.preview table {
|
||||||
|
@ -11,7 +11,8 @@
|
|||||||
@nightButtonHoverColor: #84868b;
|
@nightButtonHoverColor: #84868b;
|
||||||
@nightLineColor: #84868b;
|
@nightLineColor: #84868b;
|
||||||
|
|
||||||
.container_night {
|
.dark {
|
||||||
|
.container {
|
||||||
background-color: @nightBgColor;
|
background-color: @nightBgColor;
|
||||||
|
|
||||||
.el-main {
|
.el-main {
|
||||||
@ -63,80 +64,79 @@
|
|||||||
color: @nightLinkTextColor;
|
color: @nightLinkTextColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor__header {
|
// .el-button {
|
||||||
background-color: @nightHeaderColor;
|
// color: @nightWhiteColor;
|
||||||
}
|
// background-color: @nightCodeMirrorColor;
|
||||||
|
// border: 1px solid transparent;
|
||||||
|
// }
|
||||||
|
|
||||||
.el-button {
|
// .el-button.is-plain:focus,
|
||||||
color: @nightWhiteColor;
|
// .el-button.is-plain:hover {
|
||||||
background-color: @nightCodeMirrorColor;
|
// background: @nightButtonBg;
|
||||||
border: 1px solid transparent;
|
// color: @nightWhiteColor;
|
||||||
}
|
// border: 1px solid @nightWhiteColor;
|
||||||
|
|
||||||
.el-button.is-plain:focus,
|
// i {
|
||||||
.el-button.is-plain:hover {
|
// color: @nightWhiteColor;
|
||||||
background: @nightButtonBg;
|
// }
|
||||||
color: @nightWhiteColor;
|
// }
|
||||||
border: 1px solid @nightWhiteColor;
|
|
||||||
|
|
||||||
i {
|
// .insert__dialog,
|
||||||
color: @nightWhiteColor;
|
// .about__dialog,
|
||||||
}
|
// .reset__dialog,
|
||||||
}
|
// .upload__dialog {
|
||||||
|
// .el-dialog {
|
||||||
|
// background-color: @nightBgColor;
|
||||||
|
// }
|
||||||
|
|
||||||
.insert__dialog,
|
// .el-dialog__body {
|
||||||
.about__dialog,
|
// color: @nightWhiteColor;
|
||||||
.reset__dialog,
|
// }
|
||||||
.upload__dialog {
|
|
||||||
.el-dialog {
|
|
||||||
background-color: @nightBgColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dialog__body {
|
// .el-dialog__title,
|
||||||
color: @nightWhiteColor;
|
// .el-form-item__label {
|
||||||
}
|
// color: @nightWhiteColor;
|
||||||
|
// }
|
||||||
|
|
||||||
.el-dialog__title,
|
// .el-tabs__item {
|
||||||
.el-form-item__label {
|
// color: @nightActiveCodeMirrorColor;
|
||||||
color: @nightWhiteColor;
|
// }
|
||||||
}
|
|
||||||
|
|
||||||
.el-tabs__item {
|
// .el-tabs__nav-wrap::after {
|
||||||
color: @nightActiveCodeMirrorColor;
|
// background-color: @nightLineColor;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.el-tabs__nav-wrap::after {
|
// .is-active {
|
||||||
background-color: @nightLineColor;
|
// color: @nightWhiteColor;
|
||||||
}
|
// }
|
||||||
|
|
||||||
.is-active {
|
// .el-upload-dragger {
|
||||||
color: @nightWhiteColor;
|
// background-color: @nightButtonBg;
|
||||||
}
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
.el-upload-dragger {
|
// :deep(
|
||||||
background-color: @nightButtonBg;
|
// .el-icon-upload,
|
||||||
}
|
// .el-icon-download,
|
||||||
}
|
// .el-icon-refresh,
|
||||||
|
// .el-icon-s-grid,
|
||||||
::v-deep .el-icon-upload,
|
// .el-icon-document
|
||||||
.el-icon-download,
|
// ) {
|
||||||
.el-icon-refresh,
|
// color: @nightWhiteColor;
|
||||||
.el-icon-s-grid,
|
// }
|
||||||
.el-icon-document {
|
|
||||||
color: @nightWhiteColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
background-color: @nightCodeMirrorColor;
|
background-color: @nightCodeMirrorColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.CodeMirror {
|
.CodeMirror {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-family: "PingFang SC", BlinkMacSystemFont, Roboto, "Helvetica Neue",
|
font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue',
|
||||||
sans-serif !important;
|
sans-serif !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,122 +0,0 @@
|
|||||||
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: `字体123Abc`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `衬线`,
|
|
||||||
value: `Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif`,
|
|
||||||
desc: `字体123Abc`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `等宽`,
|
|
||||||
value: `Menlo, Monaco, 'Courier New', monospace`,
|
|
||||||
desc: `字体123Abc`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
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: `热情活泼`,
|
|
||||||
},
|
|
||||||
// { label: `微信绿`, value: `rgb(26, 173, 25,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`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
legendOption: [
|
|
||||||
{
|
|
||||||
label: `title 优先`,
|
|
||||||
value: `title-alt`,
|
|
||||||
desc: ``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `alt 优先`,
|
|
||||||
value: `alt-title`,
|
|
||||||
desc: ``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `只显示 title`,
|
|
||||||
value: `title`,
|
|
||||||
desc: ``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `只显示 alt`,
|
|
||||||
value: `alt`,
|
|
||||||
desc: ``,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: `不显示`,
|
|
||||||
value: `none`,
|
|
||||||
desc: ``,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
form: {
|
|
||||||
rows: 1,
|
|
||||||
cols: 1,
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -1,229 +0,0 @@
|
|||||||
import marked, { Renderer } from "marked";
|
|
||||||
import hljs from "highlight.js";
|
|
||||||
import markedKatex from "marked-katex-extension";
|
|
||||||
|
|
||||||
marked.use(markedKatex({
|
|
||||||
throwOnError: false,
|
|
||||||
output: `html`,
|
|
||||||
nonStandard: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
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 = "") => {
|
|
||||||
if (lang.startsWith("mermaid")) {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.mermaid?.run();
|
|
||||||
}, 0);
|
|
||||||
return `<center><pre class="mermaid">${text}</pre></center>`;
|
|
||||||
}
|
|
||||||
lang = lang.split(" ")[0];
|
|
||||||
lang = hljs.getLanguage(lang) ? lang : "plaintext";
|
|
||||||
text = hljs.highlight(text, { language: lang }).value;
|
|
||||||
text = text
|
|
||||||
.replace(/\r\n/g, "<br/>")
|
|
||||||
.replace(/\n/g, "<br/>")
|
|
||||||
.replace(/(>[^<]+)|(^[^<]+)/g, function (str) {
|
|
||||||
return str.replace(/\s/g, " ");
|
|
||||||
});
|
|
||||||
|
|
||||||
return `<pre class="hljs code__pre" ${getStyles(
|
|
||||||
"code_pre"
|
|
||||||
)}><code class="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) => {
|
|
||||||
const createSubText = (s) => {
|
|
||||||
if (!s) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `<figcaption ${getStyles("figcaption")}>${s}</figcaption>`;
|
|
||||||
};
|
|
||||||
const transform = (title, alt) => {
|
|
||||||
const legend = localStorage.getItem("legend");
|
|
||||||
switch (legend) {
|
|
||||||
case "alt":
|
|
||||||
return alt;
|
|
||||||
case "title":
|
|
||||||
return title;
|
|
||||||
case "alt-title":
|
|
||||||
return alt || title;
|
|
||||||
case "title-alt":
|
|
||||||
return title || alt;
|
|
||||||
default:
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const subText = createSubText(transform(title, text));
|
|
||||||
const figureStyles = getStyles("figure");
|
|
||||||
const 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;
|
|
@ -1,193 +0,0 @@
|
|||||||
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,
|
|
||||||
'text-align': `justify`,
|
|
||||||
},
|
|
||||||
|
|
||||||
// 引用样式
|
|
||||||
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`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
@ -1,28 +1,69 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
import { useStore } from '@/stores'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
function handleTabsEdit(targetName, action) {
|
||||||
|
if (action === `add`) {
|
||||||
|
ElMessageBox.prompt(`请输入方案名称`, `新建自定义 CSS`, {
|
||||||
|
confirmButtonText: `确认`,
|
||||||
|
cancelButtonText: `取消`,
|
||||||
|
inputErrorMessage: `不能与现有方案重名`,
|
||||||
|
inputValidator: store.validatorTabName,
|
||||||
|
})
|
||||||
|
.then(({ value }) => {
|
||||||
|
store.addCssContentTab(value)
|
||||||
|
ElMessage.success(`新建成功~`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else if (action === `remove`) {
|
||||||
|
const tabs = store.cssContentConfig.tabs
|
||||||
|
let activeName = store.cssContentConfig.active
|
||||||
|
if (activeName === targetName) {
|
||||||
|
tabs.forEach((tab, index) => {
|
||||||
|
if (tab.name === targetName) {
|
||||||
|
const nextTab = tabs[index + 1] || tabs[index - 1]
|
||||||
|
if (nextTab) {
|
||||||
|
activeName = nextTab.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
store.cssContentConfig.active = activeName
|
||||||
|
store.cssContentConfig.tabs = tabs.filter(tab => tab.name !== targetName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<transition enter-active-class="bounceInRight">
|
<transition enter-active-class="bounceInRight">
|
||||||
<el-col :span="12" v-show="showCssEditor" class="cssEditor-wrapper">
|
<el-col v-show="store.isShowCssEditor" :span="8" class="cssEditor-wrapper order-1 h-full flex flex-col">
|
||||||
|
<el-tabs
|
||||||
|
v-model="store.cssContentConfig.active"
|
||||||
|
type="card"
|
||||||
|
editable
|
||||||
|
@edit="handleTabsEdit"
|
||||||
|
@tab-change="store.tabChanged"
|
||||||
|
>
|
||||||
|
<el-tab-pane
|
||||||
|
v-for="item in store.cssContentConfig.tabs"
|
||||||
|
:key="item.name"
|
||||||
|
:label="item.title"
|
||||||
|
:name="item.name"
|
||||||
|
/>
|
||||||
|
</el-tabs>
|
||||||
<textarea
|
<textarea
|
||||||
id="cssEditor"
|
id="cssEditor"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
placeholder="Your custom css here."
|
placeholder="Your custom css here."
|
||||||
>
|
/>
|
||||||
</textarea>
|
|
||||||
</el-col>
|
</el-col>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: `CssEditor`,
|
|
||||||
props: {
|
|
||||||
showCssEditor: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.bounceInRight {
|
.bounceInRight {
|
||||||
animation-name: bounceInRight;
|
animation-name: bounceInRight;
|
||||||
@ -61,4 +102,16 @@ export default {
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cssEditor-wrapper {
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__header) {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-tabs__new-tab) {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,57 +1,51 @@
|
|||||||
|
<script setup>
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
defineEmits([`close`])
|
||||||
|
|
||||||
|
function onRedirect(url) {
|
||||||
|
window.open(url)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
title="关于"
|
title="关于"
|
||||||
class="about__dialog"
|
class="about__dialog"
|
||||||
:visible="visible"
|
:model-value="props.visible"
|
||||||
@close="$emit('close')"
|
width="380"
|
||||||
width="30%"
|
|
||||||
center
|
center
|
||||||
|
@close="$emit('close')"
|
||||||
>
|
>
|
||||||
<div style="text-align: center">
|
<div class="text-center">
|
||||||
<h3>一款高度简洁的微信 Markdown 编辑器</h3>
|
<h3>一款高度简洁的微信 Markdown 编辑器</h3>
|
||||||
<p>扫码关注公众号 Doocs,原创技术文章第一时间推送!</p>
|
<p>扫码关注公众号 Doocs,原创技术文章第一时间推送!</p>
|
||||||
<img
|
<img
|
||||||
|
class="mx-auto my-5"
|
||||||
src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png"
|
src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png"
|
||||||
style="width: 40%"
|
style="width: 40%"
|
||||||
/>
|
>
|
||||||
</div>
|
</div>
|
||||||
<template slot="footer">
|
<template #footer>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="onRedirect('https://github.com/doocs/md')"
|
|
||||||
plain
|
plain
|
||||||
|
@click="onRedirect('https://github.com/doocs/md')"
|
||||||
>
|
>
|
||||||
GitHub 仓库
|
GitHub 仓库
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
type="primary"
|
type="primary"
|
||||||
@click="onRedirect('https://gitee.com/doocs/md')"
|
|
||||||
plain
|
plain
|
||||||
|
@click="onRedirect('https://gitee.com/doocs/md')"
|
||||||
>
|
>
|
||||||
Gitee 仓库
|
Gitee 仓库
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onRedirect(url) {
|
|
||||||
window.open(url)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="less" scoped>
|
|
||||||
/deep/ .el-dialog {
|
|
||||||
min-width: 420px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -0,0 +1,42 @@
|
|||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
|
import { useStore } from '@/stores'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
toggleShowInsertFormDialog,
|
||||||
|
toggleShowUploadImgDialog,
|
||||||
|
} = store
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
编辑
|
||||||
|
<el-icon class="ml-2">
|
||||||
|
<ElIconArrowDown />
|
||||||
|
</el-icon>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem @click="toggleShowUploadImgDialog()">
|
||||||
|
<el-icon class="mr-2 h-4 w-4">
|
||||||
|
<ElIconUpload />
|
||||||
|
</el-icon>
|
||||||
|
上传图片
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="toggleShowInsertFormDialog()">
|
||||||
|
<el-icon class="mr-2 h-4 w-4">
|
||||||
|
<ElIconGrid />
|
||||||
|
</el-icon>
|
||||||
|
插入表格
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</template>
|
@ -0,0 +1,73 @@
|
|||||||
|
<script setup>
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
|
import { useStore } from '@/stores'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
isDark,
|
||||||
|
isEditOnLeft,
|
||||||
|
} = storeToRefs(store)
|
||||||
|
|
||||||
|
const {
|
||||||
|
toggleDark,
|
||||||
|
toggleEditOnLeft,
|
||||||
|
exportEditorContent2HTML,
|
||||||
|
exportEditorContent2MD,
|
||||||
|
importMarkdownContent,
|
||||||
|
} = store
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
文件
|
||||||
|
<el-icon class="ml-2">
|
||||||
|
<ElIconArrowDown />
|
||||||
|
</el-icon>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem @click="importMarkdownContent()">
|
||||||
|
<el-icon class="mr-2 h-4 w-4">
|
||||||
|
<ElIconUpload />
|
||||||
|
</el-icon>
|
||||||
|
导入 .md
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="exportEditorContent2MD()">
|
||||||
|
<el-icon class="mr-2 h-4 w-4">
|
||||||
|
<ElIconDownload />
|
||||||
|
</el-icon>
|
||||||
|
导出 .md
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="exportEditorContent2HTML()">
|
||||||
|
<el-icon class="mr-2 h-4 w-4">
|
||||||
|
<ElIconDocument />
|
||||||
|
</el-icon>
|
||||||
|
导出 .html
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem @click="toggleDark()">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isDark }">
|
||||||
|
<ElIconCheck />
|
||||||
|
</el-icon>
|
||||||
|
深色模式
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem @click="toggleEditOnLeft()">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isEditOnLeft }">
|
||||||
|
<ElIconCheck />
|
||||||
|
</el-icon>
|
||||||
|
左侧编辑
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</template>
|
@ -0,0 +1,36 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
import AboutDialog from './AboutDialog.vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
|
const aboutDialogVisible = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger class="flex items-center">
|
||||||
|
帮助
|
||||||
|
<el-icon class="ml-2">
|
||||||
|
<ElIconArrowDown />
|
||||||
|
</el-icon>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent>
|
||||||
|
<DropdownMenuItem @click="aboutDialogVisible = true">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" />
|
||||||
|
<span>关于</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
|
||||||
|
<AboutDialog
|
||||||
|
:visible="aboutDialogVisible"
|
||||||
|
@close="aboutDialogVisible = false"
|
||||||
|
/>
|
||||||
|
</template>
|
105
src/components/CodemirrorEditor/EditorHeader/PostInfo.vue
Normal file
105
src/components/CodemirrorEditor/EditorHeader/PostInfo.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
import { useStore } from '@/stores'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
const { output } = storeToRefs(store)
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
title: ``,
|
||||||
|
desc: ``,
|
||||||
|
thumb: ``,
|
||||||
|
content: ``,
|
||||||
|
})
|
||||||
|
|
||||||
|
function 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].textContent,
|
||||||
|
desc: document.querySelector(`#output p`).textContent,
|
||||||
|
content: output.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
console.log(`error`, error)
|
||||||
|
}
|
||||||
|
form.value = {
|
||||||
|
...auto,
|
||||||
|
auto,
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function post() {
|
||||||
|
dialogVisible.value = false
|
||||||
|
// 使用 window.$syncer 可以检测是否安装插件
|
||||||
|
window.syncPost({
|
||||||
|
thumb: form.value.thumb || form.value.auto.thumb,
|
||||||
|
title: form.value.title || form.value.auto.title,
|
||||||
|
desc: form.value.desc || form.value.auto.desc,
|
||||||
|
content: form.value.content || form.value.auto.content,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<el-button plain type="primary" @click="prePost">
|
||||||
|
发布
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<el-dialog
|
||||||
|
title="发布"
|
||||||
|
:model-value="dialogVisible"
|
||||||
|
@close="dialogVisible = false"
|
||||||
|
>
|
||||||
|
<el-alert
|
||||||
|
class="mb-4"
|
||||||
|
title="注:此功能由第三方浏览器插件支持,本平台不保证安全性。"
|
||||||
|
type="info"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
|
<el-form
|
||||||
|
class="postInfo"
|
||||||
|
label-width="50"
|
||||||
|
:model="form"
|
||||||
|
>
|
||||||
|
<el-form-item label="封面">
|
||||||
|
<el-input
|
||||||
|
v-model="form.thumb"
|
||||||
|
placeholder="自动提取第一张图"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标题">
|
||||||
|
<el-input
|
||||||
|
v-model="form.title"
|
||||||
|
placeholder="自动提取第一个标题"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input
|
||||||
|
v-model="form.desc"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="自动提取第一个段落"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">
|
||||||
|
取 消
|
||||||
|
</el-button>
|
||||||
|
<el-button type="primary" @click="post">
|
||||||
|
确 定
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
@ -1,56 +0,0 @@
|
|||||||
<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>
|
|
@ -1,47 +0,0 @@
|
|||||||
<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>
|
|
142
src/components/CodemirrorEditor/EditorHeader/StyleDropdown.vue
Normal file
142
src/components/CodemirrorEditor/EditorHeader/StyleDropdown.vue
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
<script setup>
|
||||||
|
import { nextTick, ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
|
||||||
|
import StyleOptionMenu from './StyleOptionMenu.vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@/components/ui/hover-card'
|
||||||
|
|
||||||
|
import { codeBlockThemeOptions, colorOptions, fontFamilyOptions, fontSizeOptions, githubConfig, legendOptions } from '@/config'
|
||||||
|
import { useStore } from '@/stores'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontColor,
|
||||||
|
codeBlockTheme,
|
||||||
|
legend,
|
||||||
|
isMacCodeBlock,
|
||||||
|
cssEditor,
|
||||||
|
} = storeToRefs(store)
|
||||||
|
|
||||||
|
const {
|
||||||
|
resetStyleConfirm,
|
||||||
|
fontChanged,
|
||||||
|
sizeChanged,
|
||||||
|
colorChanged,
|
||||||
|
codeBlockThemeChanged,
|
||||||
|
legendChanged,
|
||||||
|
macCodeBlockChanged,
|
||||||
|
toggleShowCssEditor,
|
||||||
|
} = store
|
||||||
|
|
||||||
|
const colorPicker = ref(null)
|
||||||
|
|
||||||
|
function showPicker() {
|
||||||
|
colorPicker.value.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义CSS样式
|
||||||
|
function customStyle() {
|
||||||
|
toggleShowCssEditor()
|
||||||
|
nextTick(() => {
|
||||||
|
if (!cssEditor.value) {
|
||||||
|
cssEditor.value.refresh()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
cssEditor.value.refresh()
|
||||||
|
}, 50)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
样式
|
||||||
|
<el-icon class="ml-2">
|
||||||
|
<ElIconArrowDown />
|
||||||
|
</el-icon>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent class="w-56">
|
||||||
|
<StyleOptionMenu title="字体" :options="fontFamilyOptions" :current="fontFamily" :change="fontChanged" />
|
||||||
|
<StyleOptionMenu title="字号" :options="fontSizeOptions" :current="fontSize" :change="sizeChanged" />
|
||||||
|
<StyleOptionMenu
|
||||||
|
title="主题色"
|
||||||
|
:options="colorOptions"
|
||||||
|
:current="fontColor"
|
||||||
|
:change="colorChanged"
|
||||||
|
/>
|
||||||
|
<StyleOptionMenu
|
||||||
|
title="代码块主题"
|
||||||
|
:options="codeBlockThemeOptions"
|
||||||
|
:current="codeBlockTheme"
|
||||||
|
:change="codeBlockThemeChanged"
|
||||||
|
/>
|
||||||
|
<StyleOptionMenu title="图注格式" :options="legendOptions" :current="legend" :change="legendChanged" />
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem @click.self.prevent="showPicker">
|
||||||
|
<HoverCard :open-delay="100">
|
||||||
|
<HoverCardTrigger class="w-full flex">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" />
|
||||||
|
自定义主题色
|
||||||
|
</HoverCardTrigger>
|
||||||
|
<HoverCardContent side="right" class="w-min">
|
||||||
|
<el-color-picker
|
||||||
|
ref="colorPicker"
|
||||||
|
v-model="fontColor"
|
||||||
|
:teleported="false"
|
||||||
|
show-alpha
|
||||||
|
class="ml-auto"
|
||||||
|
style="height: 2em"
|
||||||
|
@change="colorChanged"
|
||||||
|
@click="showPicker"
|
||||||
|
/>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
|
<!-- <el-icon class="mr-2 h-4 w-4" />
|
||||||
|
自定义主题色
|
||||||
|
<el-color-picker
|
||||||
|
ref="colorPicker"
|
||||||
|
v-model="fontColor"
|
||||||
|
:teleported="false"
|
||||||
|
show-alpha
|
||||||
|
class="ml-auto"
|
||||||
|
style="height: 2em"
|
||||||
|
@change="colorChanged"
|
||||||
|
@click="showPicker"
|
||||||
|
/> -->
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem @click="customStyle">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" />
|
||||||
|
自定义 CSS
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem @click="macCodeBlockChanged">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isMacCodeBlock }">
|
||||||
|
<ElIconCheck />
|
||||||
|
</el-icon>
|
||||||
|
Mac 代码块
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem divided class="leading-8" @click="resetStyleConfirm">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" />
|
||||||
|
重置
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</template>
|
@ -1,35 +1,15 @@
|
|||||||
<template>
|
<script setup>
|
||||||
<el-dropdown placement="right" class="style-option-menu">
|
import {
|
||||||
<div class="el-dropdown-link">
|
DropdownMenuItem,
|
||||||
{{ label }}
|
DropdownMenuPortal,
|
||||||
<i class="el-icon-arrow-right el-icon--right"></i>
|
DropdownMenuShortcut,
|
||||||
</div>
|
DropdownMenuSub,
|
||||||
<el-dropdown-menu slot="dropdown" style="width: 200px">
|
DropdownMenuSubContent,
|
||||||
<el-dropdown-item
|
DropdownMenuSubTrigger,
|
||||||
v-for="{ value, label, desc } in options"
|
} from '@/components/ui/dropdown-menu'
|
||||||
: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" :style="{ fontFamily: value }">{{
|
|
||||||
desc
|
|
||||||
}}</span>
|
|
||||||
</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</el-dropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
const props = defineProps({
|
||||||
export default {
|
title: {
|
||||||
name: `StyleOptionMenu`,
|
|
||||||
props: {
|
|
||||||
label: {
|
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@ -41,29 +21,51 @@ export default {
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
charge: {
|
change: {
|
||||||
type: Function,
|
type: Function,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
|
|
||||||
|
function setStyle(title, value) {
|
||||||
|
switch (title) {
|
||||||
|
case `字体`:
|
||||||
|
return { fontFamily: value }
|
||||||
|
case `字号`:
|
||||||
|
return { fontSize: value }
|
||||||
|
case `主题色`:
|
||||||
|
return { color: value }
|
||||||
|
default:
|
||||||
|
return {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<template>
|
||||||
.style-option-menu.el-dropdown {
|
<DropdownMenuSub>
|
||||||
margin: 0;
|
<DropdownMenuSubTrigger>
|
||||||
width: 150px;
|
<el-icon class="mr-2 h-4 w-4" />
|
||||||
|
<span>{{ props.title }}</span>
|
||||||
.el-dropdown-link {
|
</DropdownMenuSubTrigger>
|
||||||
display: flex;
|
<DropdownMenuPortal>
|
||||||
align-items: center;
|
<DropdownMenuSubContent>
|
||||||
justify-content: space-between;
|
<DropdownMenuItem
|
||||||
}
|
v-for="{ label, value, desc } in options"
|
||||||
}
|
:key="value"
|
||||||
|
:label="label"
|
||||||
.select-item-right {
|
:model-value="value"
|
||||||
float: right;
|
class="w-50"
|
||||||
color: #8492a6;
|
@click="change(value)"
|
||||||
font-size: 13px;
|
>
|
||||||
}
|
<el-icon class="mr-2 h-4 w-4" :style="{ opacity: +(current === value) }">
|
||||||
</style>
|
<ElIconCheck />
|
||||||
|
</el-icon>
|
||||||
|
{{ label }}
|
||||||
|
<DropdownMenuShortcut :style="setStyle(title, value)">
|
||||||
|
{{ desc }}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</template>
|
||||||
|
@ -1,369 +1,93 @@
|
|||||||
<template>
|
<script setup>
|
||||||
<el-container class="header-container is-dark">
|
import { nextTick, ref } from 'vue'
|
||||||
<div class="dropdowns">
|
import { storeToRefs } from 'pinia'
|
||||||
<el-dropdown>
|
import { ElNotification } from 'element-plus'
|
||||||
<span class="el-dropdown-link">
|
import CodeMirror from 'codemirror'
|
||||||
文件<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-item divided @click.native="isEditOnLeftChanged">
|
|
||||||
<i
|
|
||||||
class="el-icon-check"
|
|
||||||
:style="{ opacity: isEditOnLeft ? 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 class="padding-left-3">
|
|
||||||
<style-option-menu
|
|
||||||
label="图注格式"
|
|
||||||
:options="config.legendOption"
|
|
||||||
:current="selectLegend"
|
|
||||||
:charge="legendChanged"
|
|
||||||
></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
|
import PostInfo from './PostInfo.vue'
|
||||||
:form="form"
|
import FileDropdown from './FileDropdown.vue'
|
||||||
@post="post"
|
import HelpDropdown from './HelpDropdown.vue'
|
||||||
@close="form.dialogVisible = false"
|
import StyleDropdown from './StyleDropdown.vue'
|
||||||
>
|
import EditDropdown from './EditDropdown.vue'
|
||||||
</post-info-dialog>
|
|
||||||
<reset-dialog
|
|
||||||
:show-reset-confirm="showResetConfirm"
|
|
||||||
@confirm="confirmReset"
|
|
||||||
@close="cancelReset"
|
|
||||||
></reset-dialog>
|
|
||||||
</el-container>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
import {
|
||||||
import { mapState, mapActions } from 'pinia'
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuShortcut,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu'
|
||||||
|
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@/components/ui/hover-card'
|
||||||
|
|
||||||
|
import { mergeCss, solveWeChatImage } from '@/utils'
|
||||||
import { useStore } from '@/stores'
|
import { useStore } from '@/stores'
|
||||||
|
|
||||||
import { setFontSize, setColorWithCustomTemplate } from '@/assets/scripts/util'
|
const emit = defineEmits([
|
||||||
import { solveWeChatImage, mergeCss } from '@/assets/scripts/converter'
|
`addFormat`,
|
||||||
import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt'
|
`formatContent`,
|
||||||
import config from '@/assets/scripts/config'
|
`startCopy`,
|
||||||
import ResetDialog from './ResetDialog'
|
`endCopy`,
|
||||||
import StyleOptionMenu from './StyleOptionMenu'
|
])
|
||||||
import PostInfoDialog from './PostInfoDialog'
|
const defaultKeyMap = CodeMirror.keyMap.default
|
||||||
|
const modPrefix
|
||||||
|
= defaultKeyMap === CodeMirror.keyMap.macDefault ? `Cmd` : `Ctrl`
|
||||||
|
|
||||||
export default {
|
const formatItems = [
|
||||||
name: `editor-header`,
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
config,
|
|
||||||
citeStatus: false,
|
|
||||||
isMacCodeBlock: true,
|
|
||||||
isEditOnLeft: true,
|
|
||||||
showResetConfirm: false,
|
|
||||||
selectFont: ``,
|
|
||||||
selectSize: ``,
|
|
||||||
selectColor: ``,
|
|
||||||
selectCodeTheme: config.codeThemeOption[2].value,
|
|
||||||
selectLegend: ``,
|
|
||||||
form: {
|
|
||||||
dialogVisible: false,
|
|
||||||
title: ``,
|
|
||||||
desc: ``,
|
|
||||||
thumb: ``,
|
|
||||||
content: ``,
|
|
||||||
},
|
|
||||||
formatItems: [
|
|
||||||
{
|
{
|
||||||
label: `加粗`,
|
label: `加粗`,
|
||||||
kbd: `Ctrl/Command + B`,
|
kbd: `${modPrefix} + B`,
|
||||||
emitArgs: [`addFormat`, `**`],
|
emitArgs: [`addFormat`, `${modPrefix}-B`],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `斜体`,
|
label: `斜体`,
|
||||||
kbd: `Ctrl/Command + I`,
|
kbd: `${modPrefix} + I`,
|
||||||
emitArgs: [`addFormat`, `*`],
|
emitArgs: [`addFormat`, `${modPrefix}-I`],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `删除线`,
|
label: `删除线`,
|
||||||
kbd: `Ctrl/Command + D`,
|
kbd: `${modPrefix} + D`,
|
||||||
emitArgs: [`addFormat`, `~~`],
|
emitArgs: [`addFormat`, `${modPrefix}-D`],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `超链接`,
|
label: `超链接`,
|
||||||
kbd: `Ctrl/Command + K`,
|
kbd: `${modPrefix} + K`,
|
||||||
emitArgs: [`addFormat`, `[`, `]()`],
|
emitArgs: [`addFormat`, `${modPrefix}-K`],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `行内代码`,
|
||||||
|
kbd: `${modPrefix} + E`,
|
||||||
|
emitArgs: [`addFormat`, `${modPrefix}-E`],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: `格式化`,
|
label: `格式化`,
|
||||||
kbd: `Ctrl/Command + F`,
|
kbd: `${modPrefix} + F`,
|
||||||
emitArgs: [`formatContent`],
|
emitArgs: [`formatContent`],
|
||||||
},
|
},
|
||||||
],
|
]
|
||||||
}
|
|
||||||
},
|
const store = useStore()
|
||||||
components: {
|
|
||||||
PostInfoDialog,
|
const {
|
||||||
StyleOptionMenu,
|
isDark,
|
||||||
ResetDialog,
|
isCiteStatus,
|
||||||
},
|
output,
|
||||||
computed: {
|
} = storeToRefs(store)
|
||||||
btnType() {
|
|
||||||
return this.nightMode ? `default` : `primary`
|
const {
|
||||||
},
|
toggleDark,
|
||||||
...mapState(useStore, {
|
editorRefresh,
|
||||||
output: (state) => state.output,
|
citeStatusChanged,
|
||||||
editor: (state) => state.editor,
|
} = store
|
||||||
cssEditor: (state) => state.cssEditor,
|
|
||||||
currentFont: (state) => state.currentFont,
|
|
||||||
currentSize: (state) => state.currentSize,
|
|
||||||
currentColor: (state) => state.currentColor,
|
|
||||||
codeTheme: (state) => state.codeTheme,
|
|
||||||
legend: (state) => state.legend,
|
|
||||||
nightMode: (state) => state.nightMode,
|
|
||||||
currentCiteStatus: (state) => state.citeStatus,
|
|
||||||
currentIsMacCodeBlock: (state) => state.isMacCodeBlock,
|
|
||||||
currentIsEditOnLeft: (state) => state.isEditOnLeft,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
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`)
|
|
||||||
},
|
|
||||||
legendChanged(legend) {
|
|
||||||
this.setCurrentLegend(legend)
|
|
||||||
this.selectLegend = legend
|
|
||||||
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`)
|
|
||||||
},
|
|
||||||
isEditOnLeftChanged() {
|
|
||||||
this.isEditOnLeft = !this.isEditOnLeft
|
|
||||||
this.setIsEditOnLeft(this.isEditOnLeft)
|
|
||||||
},
|
|
||||||
// 复制到微信公众号
|
// 复制到微信公众号
|
||||||
copy() {
|
function copy() {
|
||||||
this.$emit(`startCopy`)
|
emit(`startCopy`)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
function modifyHtmlStructure(htmlString) {
|
function modifyHtmlStructure(htmlString) {
|
||||||
// 创建一个 div 元素来暂存原始 HTML 字符串
|
// 创建一个 div 元素来暂存原始 HTML 字符串
|
||||||
@ -375,7 +99,7 @@ export default {
|
|||||||
originalItems.forEach((originalItem) => {
|
originalItems.forEach((originalItem) => {
|
||||||
originalItem.parentElement.insertAdjacentElement(
|
originalItem.parentElement.insertAdjacentElement(
|
||||||
`afterend`,
|
`afterend`,
|
||||||
originalItem
|
originalItem,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -383,6 +107,12 @@ export default {
|
|||||||
return tempDiv.innerHTML
|
return tempDiv.innerHTML
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是深色模式,复制之前需要先切换到白天模式
|
||||||
|
const isBeforeDark = isDark.value
|
||||||
|
if (isBeforeDark) {
|
||||||
|
toggleDark()
|
||||||
|
}
|
||||||
|
|
||||||
solveWeChatImage()
|
solveWeChatImage()
|
||||||
|
|
||||||
const clipboardDiv = document.getElementById(`output`)
|
const clipboardDiv = document.getElementById(`output`)
|
||||||
@ -393,159 +123,87 @@ export default {
|
|||||||
clipboardDiv.innerHTML = clipboardDiv.innerHTML
|
clipboardDiv.innerHTML = clipboardDiv.innerHTML
|
||||||
.replace(
|
.replace(
|
||||||
/class="base"( style="display: inline")*/g,
|
/class="base"( style="display: inline")*/g,
|
||||||
`class="base" style="display: inline"`
|
`class="base" style="display: inline"`,
|
||||||
)
|
)
|
||||||
// 公众号不支持 position, 转换为等价的 translateY
|
// 公众号不支持 position, 转换为等价的 translateY
|
||||||
.replace(/top:(.*?)em/g, `transform: translateY($1em)`)
|
.replace(/top:(.*?)em/g, `transform: translateY($1em)`)
|
||||||
|
// 适配主题中的颜色变量
|
||||||
|
.replaceAll(`var(--el-text-color-regular)`, `#3f3f3f`)
|
||||||
clipboardDiv.focus()
|
clipboardDiv.focus()
|
||||||
window.getSelection().removeAllRanges()
|
window.getSelection().removeAllRanges()
|
||||||
let range = document.createRange()
|
const range = document.createRange()
|
||||||
|
|
||||||
range.setStartBefore(clipboardDiv.firstChild)
|
range.setStartBefore(clipboardDiv.firstChild)
|
||||||
range.setEndAfter(clipboardDiv.lastChild)
|
range.setEndAfter(clipboardDiv.lastChild)
|
||||||
window.getSelection().addRange(range)
|
window.getSelection().addRange(range)
|
||||||
document.execCommand(`copy`)
|
document.execCommand(`copy`)
|
||||||
window.getSelection().removeAllRanges()
|
window.getSelection().removeAllRanges()
|
||||||
clipboardDiv.innerHTML = this.output
|
clipboardDiv.innerHTML = output.value
|
||||||
|
|
||||||
|
if (isBeforeDark) {
|
||||||
|
toggleDark()
|
||||||
|
}
|
||||||
|
|
||||||
// 输出提示
|
// 输出提示
|
||||||
this.$notify({
|
ElNotification({
|
||||||
showClose: true,
|
showClose: true,
|
||||||
message: `已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴`,
|
message: `已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴`,
|
||||||
offset: 80,
|
offset: 80,
|
||||||
duration: 1600,
|
duration: 1600,
|
||||||
type: `success`,
|
type: `success`,
|
||||||
})
|
})
|
||||||
this.$emit(`refresh`)
|
|
||||||
this.$emit(`endCopy`)
|
editorRefresh()
|
||||||
|
emit(`endCopy`)
|
||||||
}, 350)
|
}, 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.legendChanged(this.config.legendOption[3].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`,
|
|
||||||
`setCurrentLegend`,
|
|
||||||
`setWxRendererOptions`,
|
|
||||||
`setIsMacCodeBlock`,
|
|
||||||
`setIsEditOnLeft`,
|
|
||||||
]),
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
this.selectFont = this.currentFont
|
|
||||||
this.selectSize = this.currentSize
|
|
||||||
this.selectColor = this.currentColor
|
|
||||||
this.selectCodeTheme = this.codeTheme
|
|
||||||
this.selectLegend = this.legend
|
|
||||||
this.citeStatus = this.currentCiteStatus
|
|
||||||
this.isMacCodeBlock = this.currentIsMacCodeBlock
|
|
||||||
this.isEditOnLeft = this.currentIsEditOnLeft
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="header-container">
|
||||||
|
<el-space class="dropdowns flex-auto" size="large">
|
||||||
|
<FileDropdown />
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger>
|
||||||
|
格式
|
||||||
|
<el-icon class="ml-2">
|
||||||
|
<ElIconArrowDown />
|
||||||
|
</el-icon>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent class="w-60">
|
||||||
|
<DropdownMenuItem v-for="{ label, kbd, emitArgs } in formatItems" :key="kbd" @click="$emit(...emitArgs)">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" />
|
||||||
|
{{ label }}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{{ kbd }}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem @click="citeStatusChanged">
|
||||||
|
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isCiteStatus }">
|
||||||
|
<ElIconCheck />
|
||||||
|
</el-icon>
|
||||||
|
微信外链转底部引用
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
<EditDropdown />
|
||||||
|
<StyleDropdown />
|
||||||
|
<HelpDropdown />
|
||||||
|
</el-space>
|
||||||
|
<el-button plain type="primary" @click="copy">
|
||||||
|
复制
|
||||||
|
</el-button>
|
||||||
|
|
||||||
|
<PostInfo />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.header-container {
|
.header-container {
|
||||||
padding: 10px 20px;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
height: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
.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>
|
</style>
|
||||||
|
@ -1,13 +1,48 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useStore } from '@/stores'
|
||||||
|
import { createTable } from '@/utils'
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const { formatContent, toggleShowInsertFormDialog } = store
|
||||||
|
|
||||||
|
const rowNum = ref(3)
|
||||||
|
const colNum = ref(3)
|
||||||
|
const tableData = ref({})
|
||||||
|
|
||||||
|
function resetVal() {
|
||||||
|
rowNum.value = 3
|
||||||
|
colNum.value = 3
|
||||||
|
tableData.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 插入表格
|
||||||
|
function insertTable() {
|
||||||
|
const table = createTable({
|
||||||
|
rows: rowNum.value,
|
||||||
|
cols: colNum.value,
|
||||||
|
data: tableData.value,
|
||||||
|
})
|
||||||
|
store.editor.operation(() => {
|
||||||
|
store.editor.replaceSelection(`\n${table}\n`, `end`)
|
||||||
|
})
|
||||||
|
// store.editorRefresh()
|
||||||
|
resetVal()
|
||||||
|
// formatContent()
|
||||||
|
toggleShowInsertFormDialog()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
title="插入表格"
|
title="插入表格"
|
||||||
class="insert__dialog"
|
class="insert__dialog"
|
||||||
:visible="visible"
|
:model-value="store.isShowInsertFormDialog"
|
||||||
@close="$emit('close')"
|
@close="toggleShowInsertFormDialog(false)"
|
||||||
border
|
|
||||||
>
|
>
|
||||||
<el-row class="tb-options" type="flex" align="middle" :gutter="10">
|
<el-row class="tb-options" type="flex" align="middle" :gutter="10">
|
||||||
<el-col>
|
<el-col :span="12">
|
||||||
行数:
|
行数:
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="rowNum"
|
v-model="rowNum"
|
||||||
@ -15,9 +50,9 @@
|
|||||||
:min="1"
|
:min="1"
|
||||||
:max="100"
|
:max="100"
|
||||||
size="small"
|
size="small"
|
||||||
></el-input-number>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
<el-col>
|
<el-col :span="12">
|
||||||
列数:
|
列数:
|
||||||
<el-input-number
|
<el-input-number
|
||||||
v-model="colNum"
|
v-model="colNum"
|
||||||
@ -25,88 +60,39 @@
|
|||||||
:min="1"
|
:min="1"
|
||||||
:max="100"
|
:max="100"
|
||||||
size="small"
|
size="small"
|
||||||
></el-input-number>
|
/>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
<table style="border-collapse: collapse" class="input-table">
|
<table style="border-collapse: collapse" class="input-table">
|
||||||
<tr
|
<tr
|
||||||
:class="{ 'head-style': row === 1 }"
|
|
||||||
v-for="row in rowNum + 1"
|
v-for="row in rowNum + 1"
|
||||||
:key="row"
|
:key="row"
|
||||||
|
:class="{ 'head-style': row === 1 }"
|
||||||
>
|
>
|
||||||
<td v-for="col in colNum" :key="col">
|
<td v-for="col in colNum" :key="col">
|
||||||
<el-input
|
<el-input
|
||||||
align="center"
|
|
||||||
v-model="tableData[`k_${row - 1}_${col - 1}`]"
|
v-model="tableData[`k_${row - 1}_${col - 1}`]"
|
||||||
|
align="center"
|
||||||
:placeholder="row === 1 ? '表头' : ''"
|
:placeholder="row === 1 ? '表头' : ''"
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<div slot="footer" class="dialog-footer">
|
<template #footer>
|
||||||
<el-button :type="btnType" @click="$emit('close')" plain>
|
<div class="dialog-footer">
|
||||||
|
<el-button plain @click="toggleShowInsertFormDialog(false)">
|
||||||
取 消
|
取 消
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button :type="btnType" @click="insertTable" plain> 确 定 </el-button>
|
<el-button type="primary" plain @click="insertTable">
|
||||||
|
确 定
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</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>
|
<style lang="less" scoped>
|
||||||
/deep/ .el-dialog {
|
:deep(.el-dialog) {
|
||||||
width: 55%;
|
width: 55%;
|
||||||
min-height: 375px;
|
min-height: 375px;
|
||||||
min-width: 440px;
|
min-width: 440px;
|
||||||
@ -116,11 +102,7 @@ export default {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-table ::v-deep .el-input__inner {
|
.input-table :deep(.el-input__inner) {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.head-style /deep/ .el-input__inner {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,26 +1,5 @@
|
|||||||
<template>
|
<script setup>
|
||||||
<div
|
const props = defineProps({
|
||||||
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: {
|
visible: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@ -33,10 +12,11 @@ export default {
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
},
|
})
|
||||||
data() {
|
|
||||||
return {
|
const emit = defineEmits([`menuTick`, `closeMenu`])
|
||||||
menu: [
|
|
||||||
|
const menu = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
text: `上传图片`,
|
text: `上传图片`,
|
||||||
@ -58,42 +38,61 @@ export default {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: `导出 .md 文档`,
|
text: `导出 .md 文档`,
|
||||||
key: `download`,
|
key: `exportMarkdown`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: `导出 .html`,
|
text: `导出 .html`,
|
||||||
key: `export`,
|
key: `exportHtml`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
text: `格式化`,
|
text: `格式化`,
|
||||||
key: `formatMarkdown`,
|
key: `formatMarkdown`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
],
|
]
|
||||||
}
|
|
||||||
},
|
function onMouseDown(key) {
|
||||||
methods: {
|
emit(`menuTick`, key)
|
||||||
onMouseDown(key) {
|
emit(`closeMenu`)
|
||||||
this.$emit(`menuTick`, key)
|
|
||||||
this.$emit(`closeMenu`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-show="props.visible"
|
||||||
|
id="menu"
|
||||||
|
class="menu"
|
||||||
|
:style="{
|
||||||
|
left: `${props.left}px`,
|
||||||
|
top: `${props.top}px`,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ul v-for="(menuItem, index) in menu" :key="index" class="menu__group">
|
||||||
|
<li
|
||||||
|
v-for="{ key, text } in menuItem"
|
||||||
|
:key="key"
|
||||||
|
class="menu_item"
|
||||||
|
@mousedown="onMouseDown(key)"
|
||||||
|
>
|
||||||
|
{{ text }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.menu {
|
.menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 4px;
|
border-radius: var(--el-border-radius-base);
|
||||||
background-color: #ffffff;
|
background-color: var(--el-bg-color);
|
||||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
box-shadow: var(--el-box-shadow);
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu__group {
|
.menu__group {
|
||||||
margin: 0;
|
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
border-bottom: 1px solid #eeeeee;
|
margin: 0;
|
||||||
|
border-bottom: 1px solid var(--el-border-color);
|
||||||
|
|
||||||
&:last-of-type {
|
&:last-of-type {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
@ -108,7 +107,7 @@ export default {
|
|||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #333333;
|
color: var(--el-text-color-regular);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:first-of-type {
|
&:first-of-type {
|
||||||
@ -116,10 +115,10 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #f0f0f0;
|
background: var(--el-bg-color-page);
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-upload {
|
:deep(.el-upload) {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,38 +1,296 @@
|
|||||||
|
<script setup>
|
||||||
|
import { nextTick, onBeforeMount, ref, watch } from 'vue'
|
||||||
|
import CodeMirror from 'codemirror/lib/codemirror'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { UploadFilled } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
import { checkImage, removeLeft } from '@/utils'
|
||||||
|
import { useStore } from '@/stores'
|
||||||
|
|
||||||
|
const emit = defineEmits([`uploadImage`])
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const formGitHub = ref({
|
||||||
|
repo: ``,
|
||||||
|
branch: ``,
|
||||||
|
accessToken: ``,
|
||||||
|
})
|
||||||
|
|
||||||
|
// const formGitee = ref({
|
||||||
|
// repo: ``,
|
||||||
|
// branch: ``,
|
||||||
|
// accessToken: ``,
|
||||||
|
// })
|
||||||
|
|
||||||
|
const formAliOSS = ref({
|
||||||
|
accessKeyId: ``,
|
||||||
|
accessKeySecret: ``,
|
||||||
|
bucket: ``,
|
||||||
|
region: ``,
|
||||||
|
path: ``,
|
||||||
|
cdnHost: ``,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formTxCOS = ref({
|
||||||
|
secretId: ``,
|
||||||
|
secretKey: ``,
|
||||||
|
bucket: ``,
|
||||||
|
region: ``,
|
||||||
|
path: ``,
|
||||||
|
cdnHost: ``,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formQiniu = ref({
|
||||||
|
accessKey: ``,
|
||||||
|
secretKey: ``,
|
||||||
|
bucket: ``,
|
||||||
|
domain: ``,
|
||||||
|
region: ``,
|
||||||
|
})
|
||||||
|
|
||||||
|
const minioOSS = ref({
|
||||||
|
endpoint: ``,
|
||||||
|
port: ``,
|
||||||
|
useSSL: true,
|
||||||
|
bucket: ``,
|
||||||
|
accessKey: ``,
|
||||||
|
secretKey: ``,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formCustom = ref({
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
const 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: `自定义代码`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const imgHost = ref(`default`)
|
||||||
|
|
||||||
|
const formCustomElInput = ref(null)
|
||||||
|
const activeName = ref(`upload`)
|
||||||
|
|
||||||
|
watch(activeName, async (val) => {
|
||||||
|
if (val === `formCustom`) {
|
||||||
|
nextTick(() => {
|
||||||
|
const textarea
|
||||||
|
= formCustomElInput.value.$el.querySelector(`textarea`)
|
||||||
|
formCustom.value.editor
|
||||||
|
= formCustom.value.editor
|
||||||
|
|| CodeMirror.fromTextArea(textarea, {
|
||||||
|
mode: `javascript`,
|
||||||
|
})
|
||||||
|
// formCustom.value.editor.setValue(formCustom.value.code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
immediate: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
if (localStorage.getItem(`githubConfig`)) {
|
||||||
|
formGitHub.value = JSON.parse(localStorage.getItem(`githubConfig`))
|
||||||
|
}
|
||||||
|
// if (localStorage.getItem(`giteeConfig`)) {
|
||||||
|
// formGitee.value = JSON.parse(localStorage.getItem(`giteeConfig`))
|
||||||
|
// }
|
||||||
|
if (localStorage.getItem(`aliOSSConfig`)) {
|
||||||
|
formAliOSS.value = JSON.parse(localStorage.getItem(`aliOSSConfig`))
|
||||||
|
}
|
||||||
|
if (localStorage.getItem(`txCOSConfig`)) {
|
||||||
|
formTxCOS.value = JSON.parse(localStorage.getItem(`txCOSConfig`))
|
||||||
|
}
|
||||||
|
if (localStorage.getItem(`qiniuConfig`)) {
|
||||||
|
formQiniu.value = JSON.parse(localStorage.getItem(`qiniuConfig`))
|
||||||
|
}
|
||||||
|
if (localStorage.getItem(`minioConfig`)) {
|
||||||
|
minioOSS.value = JSON.parse(localStorage.getItem(`minioConfig`))
|
||||||
|
}
|
||||||
|
if (localStorage.getItem(`imgHost`)) {
|
||||||
|
imgHost.value = localStorage.getItem(`imgHost`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function changeImgHost() {
|
||||||
|
localStorage.setItem(`imgHost`, imgHost.value)
|
||||||
|
ElMessage.success(`已成功切换图床`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveGitHubConfiguration() {
|
||||||
|
if (!(formGitHub.value.repo && formGitHub.value.accessToken)) {
|
||||||
|
const blankElement = formGitHub.value.repo ? `token` : `GitHub 仓库`
|
||||||
|
ElMessage.error(`参数「${blankElement}」不能为空`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem(`githubConfig`, JSON.stringify(formGitHub.value))
|
||||||
|
ElMessage.success(`保存成功`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// const saveGiteeConfiguration = () => {
|
||||||
|
// if (!(formGitee.value.repo && formGitee.value.accessToken)) {
|
||||||
|
// const blankElement = formGitee.value.repo ? `私人令牌` : `Gitee 仓库`
|
||||||
|
// ElMessage.error(`参数「${blankElement}」不能为空`)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
// localStorage.setItem(`giteeConfig`, JSON.stringify(formGitee.value))
|
||||||
|
// ElMessage.success(`保存成功`)
|
||||||
|
// }
|
||||||
|
|
||||||
|
function saveAliOSSConfiguration() {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
formAliOSS.value.accessKeyId
|
||||||
|
&& formAliOSS.value.accessKeySecret
|
||||||
|
&& formAliOSS.value.bucket
|
||||||
|
&& formAliOSS.value.region
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ElMessage.error(`阿里云 OSS 参数配置不全`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localStorage.setItem(`aliOSSConfig`, JSON.stringify(formAliOSS.value))
|
||||||
|
ElMessage.success(`保存成功`)
|
||||||
|
}
|
||||||
|
function saveMinioOSSConfiguration() {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
minioOSS.value.endpoint
|
||||||
|
&& minioOSS.value.bucket
|
||||||
|
&& minioOSS.value.accessKey
|
||||||
|
&& minioOSS.value.secretKey
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ElMessage.error(`MinIO 参数配置不全`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localStorage.setItem(`minioConfig`, JSON.stringify(minioOSS.value))
|
||||||
|
ElMessage.success(`保存成功`)
|
||||||
|
}
|
||||||
|
function saveTxCOSConfiguration() {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
formTxCOS.value.secretId
|
||||||
|
&& formTxCOS.value.secretKey
|
||||||
|
&& formTxCOS.value.bucket
|
||||||
|
&& formTxCOS.value.region
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ElMessage.error(`腾讯云 COS 参数配置不全`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localStorage.setItem(`txCOSConfig`, JSON.stringify(formTxCOS.value))
|
||||||
|
ElMessage.success(`保存成功`)
|
||||||
|
}
|
||||||
|
function saveQiniuConfiguration() {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
formQiniu.value.accessKey
|
||||||
|
&& formQiniu.value.secretKey
|
||||||
|
&& formQiniu.value.bucket
|
||||||
|
&& formQiniu.value.domain
|
||||||
|
&& formQiniu.value.region
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ElMessage.error(`七牛云 Kodo 参数配置不全`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
localStorage.setItem(`qiniuConfig`, JSON.stringify(formQiniu.value))
|
||||||
|
ElMessage.success(`保存成功`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function formCustomSave() {
|
||||||
|
const str = formCustom.value.editor.getValue()
|
||||||
|
localStorage.setItem(`formCustomConfig`, str)
|
||||||
|
ElMessage.success(`保存成功`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function beforeImageUpload(file) {
|
||||||
|
// check image
|
||||||
|
const checkResult = checkImage(file)
|
||||||
|
if (!checkResult.ok) {
|
||||||
|
ElMessage.error(checkResult.msg)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// check image host
|
||||||
|
let imgHost = localStorage.getItem(`imgHost`)
|
||||||
|
imgHost = imgHost || `default`
|
||||||
|
localStorage.setItem(`imgHost`, imgHost)
|
||||||
|
|
||||||
|
const config = localStorage.getItem(`${imgHost}Config`)
|
||||||
|
const isValidHost = imgHost === `default` || config
|
||||||
|
if (!isValidHost) {
|
||||||
|
ElMessage.error(`请先配置 ${imgHost} 图床参数`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
function uploadImage(params) {
|
||||||
|
emit(`uploadImage`, params.file)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog title="本地上传" class="upload__dialog" :model-value="store.isShowUploadImgDialog" @close="store.toggleShowUploadImgDialog(false)">
|
||||||
title="本地上传"
|
<el-tabs v-model="activeName">
|
||||||
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-tab-pane class="upload-panel" label="选择上传" name="upload">
|
||||||
<el-select
|
<el-select v-model="imgHost" placeholder="请选择" size="small" @change="changeImgHost">
|
||||||
v-model="imgHost"
|
<el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
@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-select>
|
||||||
<el-upload
|
<el-upload
|
||||||
drag
|
drag multiple action="" :headers="{ 'Content-Type': 'multipart/form-data' }" :show-file-list="false"
|
||||||
action=""
|
accept=".jpg, .jpeg, .png, .gif" name="file" :before-upload="beforeImageUpload" :http-request="uploadImage"
|
||||||
: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>
|
<el-icon class="el-icon--upload">
|
||||||
|
<UploadFilled />
|
||||||
|
</el-icon>
|
||||||
<div class="el-upload__text">
|
<div class="el-upload__text">
|
||||||
将图片拖到此处,或
|
将图片拖到此处,或
|
||||||
<em>点击上传</em>
|
<em>点击上传</em>
|
||||||
@ -44,7 +302,7 @@
|
|||||||
class="setting-form"
|
class="setting-form"
|
||||||
:model="formGitee"
|
:model="formGitee"
|
||||||
label-position="right"
|
label-position="right"
|
||||||
label-width="140px"
|
label-width="150px"
|
||||||
>
|
>
|
||||||
<el-form-item label="Gitee 仓库" :required="true">
|
<el-form-item label="Gitee 仓库" :required="true">
|
||||||
<el-input
|
<el-input
|
||||||
@ -79,92 +337,57 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane> -->
|
</el-tab-pane> -->
|
||||||
<el-tab-pane class="github-panel" label="GitHub 图床" name="github">
|
<el-tab-pane class="github-panel" label="GitHub 图床" name="github">
|
||||||
<el-form
|
<el-form class="setting-form" :model="formGitHub" label-position="right" label-width="150px">
|
||||||
class="setting-form"
|
|
||||||
:model="formGitHub"
|
|
||||||
label-position="right"
|
|
||||||
label-width="140px"
|
|
||||||
>
|
|
||||||
<el-form-item label="GitHub 仓库" :required="true">
|
<el-form-item label="GitHub 仓库" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formGitHub.repo" placeholder="如:github.com/yanglbme/resource" />
|
||||||
v-model.trim="formGitHub.repo"
|
|
||||||
placeholder="如:github.com/yanglbme/resource"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分支">
|
<el-form-item label="分支">
|
||||||
<el-input
|
<el-input v-model.trim="formGitHub.branch" placeholder="如:release,可不填,默认 master" />
|
||||||
v-model.trim="formGitHub.branch"
|
|
||||||
placeholder="如:release,可不填,默认 master"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Token" :required="true">
|
<el-form-item label="Token" :required="true">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="formGitHub.accessToken"
|
v-model.trim="formGitHub.accessToken" show-password
|
||||||
show-password
|
|
||||||
placeholder="如:cc1d0c1426d0fd0902bd2d7184b14da61b8abc46"
|
placeholder="如:cc1d0c1426d0fd0902bd2d7184b14da61b8abc46"
|
||||||
></el-input>
|
/>
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary"
|
||||||
href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token"
|
href="https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>如何获取 GitHub Token?
|
>
|
||||||
|
如何获取 GitHub Token?
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
<el-button type="primary" @click="saveGitHubConfiguration"
|
<el-button type="primary" @click="saveGitHubConfiguration">
|
||||||
>保存配置
|
保存配置
|
||||||
</el-button>
|
</el-button>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane class="github-panel" label="阿里云 OSS" name="aliOSS">
|
<el-tab-pane class="github-panel" label="阿里云 OSS" name="aliOSS">
|
||||||
<el-form
|
<el-form class="setting-form" :model="formAliOSS" label-position="right" label-width="150px">
|
||||||
class="setting-form"
|
|
||||||
:model="formAliOSS"
|
|
||||||
label-position="right"
|
|
||||||
label-width="140px"
|
|
||||||
>
|
|
||||||
<el-form-item label="AccessKey ID" :required="true">
|
<el-form-item label="AccessKey ID" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formAliOSS.accessKeyId" placeholder="如:LTAI4GdoocsmdoxUf13ylbaNHk" />
|
||||||
v-model.trim="formAliOSS.accessKeyId"
|
|
||||||
placeholder="如:LTAI4GdoocsmdoxUf13ylbaNHk"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="AccessKey Secret" :required="true">
|
<el-form-item label="AccessKey Secret" :required="true">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="formAliOSS.accessKeySecret"
|
v-model.trim="formAliOSS.accessKeySecret" show-password
|
||||||
show-password
|
|
||||||
placeholder="如:cc1d0c142doocs0902bd2d7md4b14da6ylbabc46"
|
placeholder="如:cc1d0c142doocs0902bd2d7md4b14da6ylbabc46"
|
||||||
></el-input>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Bucket" :required="true">
|
<el-form-item label="Bucket" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formAliOSS.bucket" placeholder="如:doocs" />
|
||||||
v-model.trim="formAliOSS.bucket"
|
|
||||||
placeholder="如:doocs"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Bucket 所在区域" :required="true">
|
<el-form-item label="Bucket 所在区域" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formAliOSS.region" placeholder="如:oss-cn-shenzhen" />
|
||||||
v-model.trim="formAliOSS.region"
|
|
||||||
placeholder="如:oss-cn-shenzhen"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="自定义 CDN 域名" :required="false">
|
<el-form-item label="自定义 CDN 域名" :required="false">
|
||||||
<el-input
|
<el-input v-model.trim="formAliOSS.cdnHost" placeholder="如:https://imagecdn.alidaodao.com,可不填" />
|
||||||
v-model.trim="formAliOSS.cdnHost"
|
|
||||||
placeholder="如:https://imagecdn.alidaodao.com,可不填"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="存储路径">
|
<el-form-item label="存储路径">
|
||||||
<el-input
|
<el-input v-model.trim="formAliOSS.path" placeholder="如:img,可不填,默认为根目录" />
|
||||||
v-model.trim="formAliOSS.path"
|
<el-link type="primary" href="https://help.aliyun.com/document_detail/31883.html" target="_blank">
|
||||||
placeholder="如:img,可不填,默认为根目录"
|
如何使用阿里云 OSS?
|
||||||
></el-input>
|
|
||||||
<el-link
|
|
||||||
type="primary"
|
|
||||||
href="https://help.aliyun.com/document_detail/31883.html"
|
|
||||||
target="_blank"
|
|
||||||
>如何使用阿里云 OSS?
|
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@ -175,53 +398,29 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane class="github-panel" label="腾讯云 COS" name="txCOS">
|
<el-tab-pane class="github-panel" label="腾讯云 COS" name="txCOS">
|
||||||
<el-form
|
<el-form class="setting-form" :model="formTxCOS" label-position="right" label-width="150px">
|
||||||
class="setting-form"
|
|
||||||
:model="formTxCOS"
|
|
||||||
label-position="right"
|
|
||||||
label-width="140px"
|
|
||||||
>
|
|
||||||
<el-form-item label="SecretId" :required="true">
|
<el-form-item label="SecretId" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formTxCOS.secretId" placeholder="如:AKIDnQp1w3DOOCSs8F5MDp9tdoocsmdUPonW3" />
|
||||||
v-model.trim="formTxCOS.secretId"
|
|
||||||
placeholder="如:AKIDnQp1w3DOOCSs8F5MDp9tdoocsmdUPonW3"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="SecretKey" :required="true">
|
<el-form-item label="SecretKey" :required="true">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="formTxCOS.secretKey"
|
v-model.trim="formTxCOS.secretKey" show-password
|
||||||
show-password
|
|
||||||
placeholder="如:ukLmdtEJ9271f3DOocsMDsCXdS3YlbW0"
|
placeholder="如:ukLmdtEJ9271f3DOocsMDsCXdS3YlbW0"
|
||||||
></el-input>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Bucket" :required="true">
|
<el-form-item label="Bucket" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formTxCOS.bucket" placeholder="如:doocs-3212520134" />
|
||||||
v-model.trim="formTxCOS.bucket"
|
|
||||||
placeholder="如:doocs-3212520134"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Bucket 所在区域" :required="true">
|
<el-form-item label="Bucket 所在区域" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formTxCOS.region" placeholder="如:ap-guangzhou" />
|
||||||
v-model.trim="formTxCOS.region"
|
|
||||||
placeholder="如:ap-guangzhou"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="自定义 CDN 域名" :required="false">
|
<el-form-item label="自定义 CDN 域名" :required="false">
|
||||||
<el-input
|
<el-input v-model.trim="formTxCOS.cdnHost" placeholder="如:https://imagecdn.alidaodao.com,可不填" />
|
||||||
v-model.trim="formTxCOS.cdnHost"
|
|
||||||
placeholder="如:https://imagecdn.alidaodao.com,可不填"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="存储路径">
|
<el-form-item label="存储路径">
|
||||||
<el-input
|
<el-input v-model.trim="formTxCOS.path" placeholder="如:img,可不填,默认根目录" />
|
||||||
v-model.trim="formTxCOS.path"
|
<el-link type="primary" href="https://cloud.tencent.com/document/product/436/38484" target="_blank">
|
||||||
placeholder="如:img,可不填,默认根目录"
|
如何使用腾讯云 COS?
|
||||||
></el-input>
|
|
||||||
<el-link
|
|
||||||
type="primary"
|
|
||||||
href="https://cloud.tencent.com/document/product/436/38484"
|
|
||||||
target="_blank"
|
|
||||||
>如何使用腾讯云 COS?
|
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@ -232,53 +431,29 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane class="github-panel" label="七牛云 Kodo" name="qiniu">
|
<el-tab-pane class="github-panel" label="七牛云 Kodo" name="qiniu">
|
||||||
<el-form
|
<el-form class="setting-form" :model="formQiniu" label-position="right" label-width="150px">
|
||||||
class="setting-form"
|
|
||||||
:model="formQiniu"
|
|
||||||
label-position="right"
|
|
||||||
label-width="140px"
|
|
||||||
>
|
|
||||||
<el-form-item label="AccessKey" :required="true">
|
<el-form-item label="AccessKey" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formQiniu.accessKey" placeholder="如:6DD3VaLJ_SQgOdoocsyTV_YWaDmdnL2n8EGx7kG" />
|
||||||
v-model.trim="formQiniu.accessKey"
|
|
||||||
placeholder="如:6DD3VaLJ_SQgOdoocsyTV_YWaDmdnL2n8EGx7kG"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="SecretKey" :required="true">
|
<el-form-item label="SecretKey" :required="true">
|
||||||
<el-input
|
<el-input
|
||||||
v-model.trim="formQiniu.secretKey"
|
v-model.trim="formQiniu.secretKey" show-password
|
||||||
show-password
|
|
||||||
placeholder="如:qgZa5qrvDOOcsmdKStD1oCjZ9nB7MDvJUs_34SIm"
|
placeholder="如:qgZa5qrvDOOcsmdKStD1oCjZ9nB7MDvJUs_34SIm"
|
||||||
></el-input>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Bucket" :required="true">
|
<el-form-item label="Bucket" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formQiniu.bucket" placeholder="如:md" />
|
||||||
v-model.trim="formQiniu.bucket"
|
|
||||||
placeholder="如:md"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Bucket 对应域名" :required="true">
|
<el-form-item label="Bucket 对应域名" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formQiniu.domain" placeholder="如:https://images.123ylb.cn" />
|
||||||
v-model.trim="formQiniu.domain"
|
|
||||||
placeholder="如:https://images.123ylb.cn"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="存储区域" :required="true">
|
<el-form-item label="存储区域" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="formQiniu.region" placeholder="如:z2" />
|
||||||
v-model.trim="formQiniu.region"
|
|
||||||
placeholder="如:z2"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="存储路径" :required="false">
|
<el-form-item label="存储路径" :required="false">
|
||||||
<el-input
|
<el-input v-model.trim="formQiniu.path" placeholder="如:img,可不填,默认为根目录" />
|
||||||
v-model.trim="formQiniu.path"
|
<el-link type="primary" href="https://developer.qiniu.com/kodo" target="_blank">
|
||||||
placeholder="如:img,可不填,默认为根目录"
|
如何使用七牛云 Kodo?
|
||||||
></el-input>
|
|
||||||
<el-link
|
|
||||||
type="primary"
|
|
||||||
href="https://developer.qiniu.com/kodo"
|
|
||||||
target="_blank"
|
|
||||||
>如何使用七牛云 Kodo?
|
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@ -289,55 +464,29 @@
|
|||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane class="github-panel" label="MinIO" name="minio">
|
<el-tab-pane class="github-panel" label="MinIO" name="minio">
|
||||||
<el-form
|
<el-form class="setting-form" :model="minioOSS" label-position="right" label-width="150px">
|
||||||
class="setting-form"
|
|
||||||
:model="minioOSS"
|
|
||||||
label-position="right"
|
|
||||||
label-width="140px"
|
|
||||||
>
|
|
||||||
<el-form-item label="Endpoint" :required="true">
|
<el-form-item label="Endpoint" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="minioOSS.endpoint" placeholder="如:play.min.io" />
|
||||||
v-model.trim="minioOSS.endpoint"
|
|
||||||
placeholder="如:play.min.io"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Port" :required="false">
|
<el-form-item label="Port" :required="false">
|
||||||
<el-input
|
<el-input v-model.trim="minioOSS.port" type="number" placeholder="如:9000,可不填,http 默认为 80,https 默认为 443" />
|
||||||
type="number"
|
|
||||||
v-model.trim="minioOSS.port"
|
|
||||||
placeholder="如:9000,可不填,http 默认为 80,https 默认为 443"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="UseSSL" :required="true">
|
<el-form-item label="UseSSL" :required="true">
|
||||||
<el-switch
|
<el-switch v-model="minioOSS.useSSL" active-text="是" inactive-text="否" />
|
||||||
v-model="minioOSS.useSSL"
|
|
||||||
active-text="是"
|
|
||||||
inactive-text="否"
|
|
||||||
>
|
|
||||||
</el-switch>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="Bucket" :required="true">
|
<el-form-item label="Bucket" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="minioOSS.bucket" placeholder="如:doocs" />
|
||||||
v-model.trim="minioOSS.bucket"
|
|
||||||
placeholder="如:doocs"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="AccessKey" :required="true">
|
<el-form-item label="AccessKey" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="minioOSS.accessKey" placeholder="如:zhangsan" />
|
||||||
v-model.trim="minioOSS.accessKey"
|
|
||||||
placeholder="如:zhangsan"
|
|
||||||
></el-input>
|
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="SecretKey" :required="true">
|
<el-form-item label="SecretKey" :required="true">
|
||||||
<el-input
|
<el-input v-model.trim="minioOSS.secretKey" placeholder="如:asdasdasd" />
|
||||||
v-model.trim="minioOSS.secretKey"
|
|
||||||
placeholder="如:asdasdasd"
|
|
||||||
></el-input>
|
|
||||||
<el-link
|
<el-link
|
||||||
type="primary"
|
type="primary" href="http://docs.minio.org.cn/docs/master/minio-client-complete-guide"
|
||||||
href="http://docs.minio.org.cn/docs/master/minio-client-complete-guide"
|
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>如何使用 MinIO?
|
>
|
||||||
|
如何使用 MinIO?
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@ -347,31 +496,15 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane
|
<el-tab-pane class="github-panel formCustom" label="自定义代码" name="formCustom">
|
||||||
class="github-panel formCustom"
|
<el-form class="setting-form" :model="formCustom" label-position="right">
|
||||||
label="自定义代码"
|
|
||||||
name="formCustom"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
class="setting-form"
|
|
||||||
:model="formCustom"
|
|
||||||
label-position="right"
|
|
||||||
>
|
|
||||||
<el-form-item label="" :required="true">
|
<el-form-item label="" :required="true">
|
||||||
<el-input
|
<el-input
|
||||||
class="formCustomElInput"
|
ref="formCustomElInput" v-model="formCustom.code" class="formCustomElInput" type="textarea"
|
||||||
ref="formCustomElInput"
|
resize="none" placeholder="Your custom code here."
|
||||||
type="textarea"
|
/>
|
||||||
resize="none"
|
<el-link type="primary" href="https://github.com/doocs/md#自定义上传逻辑" target="_blank">
|
||||||
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-link>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item>
|
<el-form-item>
|
||||||
@ -385,283 +518,12 @@
|
|||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</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: ``,
|
|
||||||
},
|
|
||||||
formTxCOS: {
|
|
||||||
secretId: ``,
|
|
||||||
secretKey: ``,
|
|
||||||
bucket: ``,
|
|
||||||
region: ``,
|
|
||||||
path: ``,
|
|
||||||
cdnHost: ``,
|
|
||||||
},
|
|
||||||
formQiniu: {
|
|
||||||
accessKey: ``,
|
|
||||||
secretKey: ``,
|
|
||||||
bucket: ``,
|
|
||||||
domain: ``,
|
|
||||||
region: ``,
|
|
||||||
},
|
|
||||||
minioOSS: {
|
|
||||||
endpoint: ``,
|
|
||||||
port: ``,
|
|
||||||
useSSL: true,
|
|
||||||
bucket: ``,
|
|
||||||
accessKey: ``,
|
|
||||||
secretKey: ``,
|
|
||||||
},
|
|
||||||
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(`txCOSConfig`)) {
|
|
||||||
this.formTxCOS = JSON.parse(localStorage.getItem(`txCOSConfig`))
|
|
||||||
}
|
|
||||||
if (localStorage.getItem(`qiniuConfig`)) {
|
|
||||||
this.formQiniu = JSON.parse(localStorage.getItem(`qiniuConfig`))
|
|
||||||
}
|
|
||||||
if (localStorage.getItem(`minioConfig`)) {
|
|
||||||
this.minioOSS = JSON.parse(localStorage.getItem(`minioConfig`))
|
|
||||||
}
|
|
||||||
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>
|
<style lang="less" scoped>
|
||||||
.upload__dialog {
|
.upload__dialog {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/deep/ .el-dialog {
|
:deep(.el-dialog) {
|
||||||
width: 55%;
|
width: 55%;
|
||||||
min-width: 640px;
|
min-width: 640px;
|
||||||
min-height: 615px;
|
min-height: 615px;
|
||||||
@ -669,10 +531,11 @@ export default {
|
|||||||
margin: auto !important;
|
margin: auto !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/deep/ .el-upload-dragger {
|
:deep(.el-upload-dragger) {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column;
|
flex-flow: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
width: 500px;
|
width: 500px;
|
||||||
height: 360px;
|
height: 360px;
|
||||||
|
|
||||||
@ -681,7 +544,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/deep/ .el-dialog__body {
|
:deep(.el-dialog__body) {
|
||||||
padding-bottom: 50px;
|
padding-bottom: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -708,7 +571,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.formCustomElInput {
|
.formCustomElInput {
|
||||||
/deep/ .CodeMirror {
|
:deep(.CodeMirror) {
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
height: 300px !important;
|
height: 300px !important;
|
||||||
font-family: 'Fira Mono', 'DejaVu Sans Mono', Menlo, Consolas,
|
font-family: 'Fira Mono', 'DejaVu Sans Mono', Menlo, Consolas,
|
||||||
|
@ -1,48 +1,33 @@
|
|||||||
|
<script setup>
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
loading.value = false
|
||||||
|
}, 100)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<transition name="fade" v-if="loading">
|
<transition name="fade">
|
||||||
<div
|
<div
|
||||||
|
v-if="loading"
|
||||||
class="loading"
|
class="loading"
|
||||||
:class="{
|
|
||||||
loading_night: nightMode,
|
|
||||||
}"
|
|
||||||
>
|
>
|
||||||
<strong>致力于让 Markdown 编辑更简单</strong>
|
<el-text size="large" tag="strong">
|
||||||
|
致力于让 Markdown 编辑更简单
|
||||||
|
</el-text>
|
||||||
</div>
|
</div>
|
||||||
</transition>
|
</transition>
|
||||||
</template>
|
</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>
|
<style lang="less" scoped>
|
||||||
@light-color: #303133;
|
|
||||||
@light-background-color: #f2f2f2;
|
|
||||||
@night-color: #bbbbbb;
|
|
||||||
@night-background-color: #303133;
|
|
||||||
|
|
||||||
.loading {
|
.loading {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
z-index: 99999;
|
z-index: 99999;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -51,8 +36,7 @@ export default {
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: @light-color;
|
background-color: var(--el-bg-color-page);
|
||||||
background-color: @light-background-color;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: url('../assets/images/favicon.png');
|
content: url('../assets/images/favicon.png');
|
||||||
@ -62,11 +46,6 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading_night {
|
|
||||||
color: @night-color;
|
|
||||||
background-color: @night-background-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-enter,
|
.fade-enter,
|
||||||
.fade-leave-to {
|
.fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
14
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
14
src/components/ui/dropdown-menu/DropdownMenu.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DropdownMenuRoot, type DropdownMenuRootEmits, type DropdownMenuRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRootProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRoot v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRoot>
|
||||||
|
</template>
|
40
src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
40
src/components/ui/dropdown-menu/DropdownMenuCheckboxItem.vue
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
DropdownMenuCheckboxItem,
|
||||||
|
type DropdownMenuCheckboxItemEmits,
|
||||||
|
type DropdownMenuCheckboxItemProps,
|
||||||
|
DropdownMenuItemIndicator,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { Check } from 'lucide-vue-next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuCheckboxItemProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
const emits = defineEmits<DropdownMenuCheckboxItemEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuCheckboxItem
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class=" cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Check class="w-4 h-4" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuCheckboxItem>
|
||||||
|
</template>
|
38
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
38
src/components/ui/dropdown-menu/DropdownMenuContent.vue
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
DropdownMenuContent,
|
||||||
|
type DropdownMenuContentEmits,
|
||||||
|
type DropdownMenuContentProps,
|
||||||
|
DropdownMenuPortal,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<DropdownMenuContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||||
|
{
|
||||||
|
sideOffset: 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const emits = defineEmits<DropdownMenuContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuPortal>
|
||||||
|
<DropdownMenuContent
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenuPortal>
|
||||||
|
</template>
|
11
src/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
11
src/components/ui/dropdown-menu/DropdownMenuGroup.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DropdownMenuGroup, type DropdownMenuGroupProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuGroupProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuGroup v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</template>
|
28
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
28
src/components/ui/dropdown-menu/DropdownMenuItem.vue
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuItemProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuItem
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
inset && 'pl-8',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</template>
|
24
src/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
24
src/components/ui/dropdown-menu/DropdownMenuLabel.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'radix-vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuLabelProps & { class?: HTMLAttributes['class'], inset?: boolean }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuLabel
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuLabel>
|
||||||
|
</template>
|
19
src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
19
src/components/ui/dropdown-menu/DropdownMenuRadioGroup.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
DropdownMenuRadioGroup,
|
||||||
|
type DropdownMenuRadioGroupEmits,
|
||||||
|
type DropdownMenuRadioGroupProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRadioGroupProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuRadioGroupEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRadioGroup v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRadioGroup>
|
||||||
|
</template>
|
41
src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
41
src/components/ui/dropdown-menu/DropdownMenuRadioItem.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
DropdownMenuItemIndicator,
|
||||||
|
DropdownMenuRadioItem,
|
||||||
|
type DropdownMenuRadioItemEmits,
|
||||||
|
type DropdownMenuRadioItemProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { Circle } from 'lucide-vue-next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuRadioItemProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const emits = defineEmits<DropdownMenuRadioItemEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuRadioItem
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn(
|
||||||
|
'relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<DropdownMenuItemIndicator>
|
||||||
|
<Circle class="h-2 w-2 fill-current" />
|
||||||
|
</DropdownMenuItemIndicator>
|
||||||
|
</span>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuRadioItem>
|
||||||
|
</template>
|
22
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
22
src/components/ui/dropdown-menu/DropdownMenuSeparator.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
type DropdownMenuSeparatorProps,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSeparatorProps & {
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-muted', props.class)" />
|
||||||
|
</template>
|
14
src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
14
src/components/ui/dropdown-menu/DropdownMenuShortcut.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<span :class="cn('ml-auto text-xs tracking-widest opacity-60', props.class)">
|
||||||
|
<slot />
|
||||||
|
</span>
|
||||||
|
</template>
|
19
src/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
19
src/components/ui/dropdown-menu/DropdownMenuSub.vue
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
DropdownMenuSub,
|
||||||
|
type DropdownMenuSubEmits,
|
||||||
|
type DropdownMenuSubProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubProps>()
|
||||||
|
const emits = defineEmits<DropdownMenuSubEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSub v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</template>
|
30
src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
30
src/components/ui/dropdown-menu/DropdownMenuSubContent.vue
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
type DropdownMenuSubContentEmits,
|
||||||
|
type DropdownMenuSubContentProps,
|
||||||
|
useForwardPropsEmits,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubContentProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
const emits = defineEmits<DropdownMenuSubContentEmits>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSubContent
|
||||||
|
v-bind="forwarded"
|
||||||
|
:class="cn('z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2', props.class)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</template>
|
33
src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
33
src/components/ui/dropdown-menu/DropdownMenuSubTrigger.vue
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
|
type DropdownMenuSubTriggerProps,
|
||||||
|
useForwardProps,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { ChevronRight } from 'lucide-vue-next'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuSubTriggerProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuSubTrigger
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="cn(
|
||||||
|
'flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent',
|
||||||
|
props.class,
|
||||||
|
)"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
<ChevronRight class="ml-auto h-4 w-4" />
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
</template>
|
13
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
13
src/components/ui/dropdown-menu/DropdownMenuTrigger.vue
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { DropdownMenuTrigger, type DropdownMenuTriggerProps, useForwardProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<DropdownMenuTriggerProps>()
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(props)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<DropdownMenuTrigger class="outline-none" v-bind="forwardedProps">
|
||||||
|
<slot />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</template>
|
16
src/components/ui/dropdown-menu/index.ts
Normal file
16
src/components/ui/dropdown-menu/index.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
export { DropdownMenuPortal } from 'radix-vue'
|
||||||
|
|
||||||
|
export { default as DropdownMenu } from './DropdownMenu.vue'
|
||||||
|
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
|
||||||
|
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
|
||||||
|
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
|
||||||
|
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
|
||||||
|
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
|
||||||
|
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
|
||||||
|
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
|
||||||
|
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
|
||||||
|
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
|
||||||
|
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
|
||||||
|
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
|
||||||
|
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
|
||||||
|
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
|
14
src/components/ui/hover-card/HoverCard.vue
Normal file
14
src/components/ui/hover-card/HoverCard.vue
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { HoverCardRoot, type HoverCardRootEmits, type HoverCardRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<HoverCardRootProps>()
|
||||||
|
const emits = defineEmits<HoverCardRootEmits>()
|
||||||
|
|
||||||
|
const forwarded = useForwardPropsEmits(props, emits)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<HoverCardRoot v-bind="forwarded">
|
||||||
|
<slot />
|
||||||
|
</HoverCardRoot>
|
||||||
|
</template>
|
41
src/components/ui/hover-card/HoverCardContent.vue
Normal file
41
src/components/ui/hover-card/HoverCardContent.vue
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes, computed } from 'vue'
|
||||||
|
import {
|
||||||
|
HoverCardContent,
|
||||||
|
type HoverCardContentProps,
|
||||||
|
HoverCardPortal,
|
||||||
|
useForwardProps,
|
||||||
|
} from 'radix-vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<HoverCardContentProps & { class?: HTMLAttributes['class'] }>(),
|
||||||
|
{
|
||||||
|
sideOffset: 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const delegatedProps = computed(() => {
|
||||||
|
const { class: _, ...delegated } = props
|
||||||
|
|
||||||
|
return delegated
|
||||||
|
})
|
||||||
|
|
||||||
|
const forwardedProps = useForwardProps(delegatedProps)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<HoverCardPortal>
|
||||||
|
<HoverCardContent
|
||||||
|
v-bind="forwardedProps"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||||
|
props.class,
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCardPortal>
|
||||||
|
</template>
|
11
src/components/ui/hover-card/HoverCardTrigger.vue
Normal file
11
src/components/ui/hover-card/HoverCardTrigger.vue
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { HoverCardTrigger, type HoverCardTriggerProps } from 'radix-vue'
|
||||||
|
|
||||||
|
const props = defineProps<HoverCardTriggerProps>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<HoverCardTrigger v-bind="props">
|
||||||
|
<slot />
|
||||||
|
</HoverCardTrigger>
|
||||||
|
</template>
|
3
src/components/ui/hover-card/index.ts
Normal file
3
src/components/ui/hover-card/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export { default as HoverCard } from './HoverCard.vue'
|
||||||
|
export { default as HoverCardTrigger } from './HoverCardTrigger.vue'
|
||||||
|
export { default as HoverCardContent } from './HoverCardContent.vue'
|
370
src/config/index.js
Normal file
370
src/config/index.js
Normal file
@ -0,0 +1,370 @@
|
|||||||
|
export const prefix = `MD`
|
||||||
|
|
||||||
|
export const fontFamilyOptions = [
|
||||||
|
{
|
||||||
|
label: `无衬线`,
|
||||||
|
value: `-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif`,
|
||||||
|
desc: `字体123Abc`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `衬线`,
|
||||||
|
value: `Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif`,
|
||||||
|
desc: `字体123Abc`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `等宽`,
|
||||||
|
value: `Menlo, Monaco, 'Courier New', monospace`,
|
||||||
|
desc: `字体123Abc`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const fontSizeOptions = [
|
||||||
|
{
|
||||||
|
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: `更大`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const colorOptions = [
|
||||||
|
{
|
||||||
|
label: `经典蓝`,
|
||||||
|
value: `rgba(15, 76, 129, 1)`,
|
||||||
|
desc: `最新流行`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `翡翠绿`,
|
||||||
|
value: `rgba(0, 152, 116, 1)`,
|
||||||
|
desc: `优雅清新`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `活力橘`,
|
||||||
|
value: `rgba(250, 81, 81, 1)`,
|
||||||
|
desc: `热情活泼`,
|
||||||
|
},
|
||||||
|
// { label: `微信绿`, value: `rgb(26, 173, 25,1)`, desc: `经典微信绿` },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const codeBlockThemeOptions = [
|
||||||
|
{
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const legendOptions = [
|
||||||
|
{
|
||||||
|
label: `title 优先`,
|
||||||
|
value: `title-alt`,
|
||||||
|
desc: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `alt 优先`,
|
||||||
|
value: `alt-title`,
|
||||||
|
desc: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `只显示 title`,
|
||||||
|
value: `title`,
|
||||||
|
desc: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `只显示 alt`,
|
||||||
|
value: `alt`,
|
||||||
|
desc: ``,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: `不显示`,
|
||||||
|
value: `none`,
|
||||||
|
desc: ``,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export 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`,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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`,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseColor = `#3f3f3f`
|
||||||
|
|
||||||
|
export const defaultTheme = {
|
||||||
|
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': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 二级标题样式
|
||||||
|
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': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 四级标题样式
|
||||||
|
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': `var(--el-text-color-regular)`,
|
||||||
|
'text-align': `justify`,
|
||||||
|
},
|
||||||
|
|
||||||
|
// 引用样式
|
||||||
|
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': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
ul: {
|
||||||
|
'margin-left': `0`,
|
||||||
|
'padding-left': `1em`,
|
||||||
|
'list-style': `circle`,
|
||||||
|
'color': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
footnotes: {
|
||||||
|
'margin': `0.5em 8px`,
|
||||||
|
'font-size': `80%`,
|
||||||
|
'color': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
figure: {
|
||||||
|
margin: `1.5em 8px`,
|
||||||
|
color: `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
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': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
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': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
thead: {
|
||||||
|
'background': `rgba(0, 0, 0, 0.05)`,
|
||||||
|
'font-weight': `bold`,
|
||||||
|
'color': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
td: {
|
||||||
|
border: `1px solid #dfdfdf`,
|
||||||
|
padding: `0.25em 0.5em`,
|
||||||
|
color: baseColor,
|
||||||
|
},
|
||||||
|
|
||||||
|
footnote: {
|
||||||
|
'font-size': `12px`,
|
||||||
|
'color': `var(--el-text-color-regular)`,
|
||||||
|
},
|
||||||
|
|
||||||
|
figcaption: {
|
||||||
|
'text-align': `center`,
|
||||||
|
'color': `#888`,
|
||||||
|
'font-size': `0.8em`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
17
src/element/index.js
Normal file
17
src/element/index.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import ElementPlus, { ElLoading, ElMessage } from 'element-plus'
|
||||||
|
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
install(app) {
|
||||||
|
app.use(ElementPlus, { size: `default` })
|
||||||
|
|
||||||
|
app.config.globalProperties.$loading = ElLoading.service
|
||||||
|
app.config.globalProperties.$message = ElMessage
|
||||||
|
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(`ElIcon${key}`, component)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
30
src/main.js
30
src/main.js
@ -1,15 +1,16 @@
|
|||||||
import Vue from 'vue'
|
import { createApp } from 'vue'
|
||||||
import ElementUI from 'element-ui'
|
import Store from './stores'
|
||||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
import ElementPlus from './element'
|
||||||
|
import App from './App.vue'
|
||||||
import 'element-ui/lib/theme-chalk/index.css'
|
|
||||||
|
|
||||||
|
import 'virtual:uno.css'
|
||||||
import 'codemirror/lib/codemirror.css'
|
import 'codemirror/lib/codemirror.css'
|
||||||
import 'codemirror/theme/xq-light.css'
|
import 'codemirror/theme/xq-light.css'
|
||||||
|
|
||||||
/* 每个页面公共css */
|
/* 每个页面公共css */
|
||||||
import './assets/less/theme.less'
|
import '@/assets/index.css'
|
||||||
import './assets/less/style-mirror.less'
|
import '@/assets/less/theme.less'
|
||||||
|
import '@/assets/less/style-mirror.less'
|
||||||
|
|
||||||
import 'codemirror/mode/css/css'
|
import 'codemirror/mode/css/css'
|
||||||
import 'codemirror/mode/markdown/markdown'
|
import 'codemirror/mode/markdown/markdown'
|
||||||
@ -19,16 +20,9 @@ import 'codemirror/addon/selection/active-line'
|
|||||||
import 'codemirror/addon/hint/show-hint'
|
import 'codemirror/addon/hint/show-hint'
|
||||||
import 'codemirror/addon/hint/css-hint'
|
import 'codemirror/addon/hint/css-hint'
|
||||||
|
|
||||||
import './plugins/element'
|
const app = createApp(App)
|
||||||
import App from './App'
|
|
||||||
|
|
||||||
Vue.use(ElementUI).use(PiniaVuePlugin)
|
app.use(Store)
|
||||||
|
app.use(ElementPlus)
|
||||||
|
|
||||||
Vue.config.productionTip = false
|
app.mount(`#app`)
|
||||||
|
|
||||||
App.mpType = `app`
|
|
||||||
|
|
||||||
new Vue({
|
|
||||||
...App,
|
|
||||||
pinia: createPinia(),
|
|
||||||
}).$mount(`#app`)
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
import { Loading, Message } from 'element-ui'
|
|
||||||
|
|
||||||
Vue.component(Message.name, Message)
|
|
||||||
|
|
||||||
Vue.prototype.$loading = Loading.service
|
|
||||||
Vue.prototype.$message = Message
|
|
@ -1,193 +1,104 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { markRaw, onMounted, ref } from 'vue'
|
||||||
|
import { createPinia, defineStore } from 'pinia'
|
||||||
import { marked } from 'marked'
|
import { marked } from 'marked'
|
||||||
import CodeMirror from 'codemirror/lib/codemirror'
|
import CodeMirror from 'codemirror'
|
||||||
|
import { useDark, useStorage, useToggle } from '@vueuse/core'
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
import config from '../assets/scripts/config'
|
import { codeBlockThemeOptions, colorOptions, fontFamilyOptions, fontSizeOptions, legendOptions } from '@/config'
|
||||||
import WxRenderer from '../assets/scripts/renderers/wx-renderer'
|
import WxRenderer from '@/utils/wx-renderer'
|
||||||
import DEFAULT_CONTENT from '@/assets/example/markdown.md'
|
import DEFAULT_CONTENT from '@/assets/example/markdown.md?raw'
|
||||||
import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt'
|
import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt?raw'
|
||||||
import {
|
import { addPrefix, css2json, customCssWithTemplate, downloadMD, exportHTML, formatCss, formatDoc, setColor, setColorWithCustomTemplate, setFontSize } from '@/utils'
|
||||||
formatDoc,
|
|
||||||
formatCss,
|
|
||||||
setFontSize,
|
|
||||||
setColorWithCustomTemplate,
|
|
||||||
} from '@/assets/scripts/util'
|
|
||||||
|
|
||||||
const defaultKeyMap = CodeMirror.keyMap[`default`]
|
const defaultKeyMap = CodeMirror.keyMap.default
|
||||||
const modPrefix =
|
const modPrefix
|
||||||
defaultKeyMap === CodeMirror.keyMap[`macDefault`] ? `Cmd` : `Ctrl`
|
= defaultKeyMap === CodeMirror.keyMap.macDefault ? `Cmd` : `Ctrl`
|
||||||
|
|
||||||
export const useStore = defineStore(`store`, {
|
export const useStore = defineStore(`store`, () => {
|
||||||
state: () => ({
|
// 是否开启深色模式
|
||||||
wxRenderer: null,
|
const isDark = useDark()
|
||||||
output: ``,
|
const toggleDark = useToggle(isDark)
|
||||||
html: ``,
|
|
||||||
editor: null,
|
|
||||||
cssEditor: null,
|
|
||||||
currentFont: ``,
|
|
||||||
currentSize: ``,
|
|
||||||
currentColor: ``,
|
|
||||||
citeStatus: false,
|
|
||||||
nightMode: false,
|
|
||||||
codeTheme: config.codeThemeOption[2].value,
|
|
||||||
legend: config.legendOption[3].value,
|
|
||||||
isMacCodeBlock: true,
|
|
||||||
isEditOnLeft: 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)
|
|
||||||
},
|
|
||||||
setCurrentLegend(data) {
|
|
||||||
this.legend = data
|
|
||||||
localStorage.setItem(`legend`, data)
|
|
||||||
},
|
|
||||||
setIsMacCodeBlock(data) {
|
|
||||||
this.isMacCodeBlock = data
|
|
||||||
localStorage.setItem(`isMacCodeBlock`, data)
|
|
||||||
},
|
|
||||||
setIsEditOnLeft(data) {
|
|
||||||
this.isEditOnLeft = data
|
|
||||||
localStorage.setItem(`isEditOnLeft`, 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.legend =
|
|
||||||
localStorage.getItem(`legend`) || config.legendOption[3].value
|
|
||||||
this.citeStatus = localStorage.getItem(`citeStatus`) === `true`
|
|
||||||
this.nightMode = localStorage.getItem(`nightMode`) === `true`
|
|
||||||
this.isMacCodeBlock = !(
|
|
||||||
localStorage.getItem(`isMacCodeBlock`) === `false`
|
|
||||||
)
|
|
||||||
this.isEditOnLeft = !(localStorage.getItem(`isEditOnLeft`) === `false`)
|
|
||||||
|
|
||||||
const theme = setFontSize(this.currentSize.replace(`px`, ``))
|
// 是否开启 Mac 代码块
|
||||||
this.wxRenderer = new WxRenderer({
|
const isMacCodeBlock = useStorage(`isMacCodeBlock`, true)
|
||||||
theme: setColorWithCustomTemplate(theme, this.currentColor),
|
const toggleMacCodeBlock = useToggle(isMacCodeBlock)
|
||||||
fonts: this.currentFont,
|
|
||||||
size: this.currentSize,
|
// 是否在左侧编辑
|
||||||
|
const isEditOnLeft = useStorage(`isEditOnLeft`, true)
|
||||||
|
const toggleEditOnLeft = useToggle(isEditOnLeft)
|
||||||
|
|
||||||
|
// 是否开启微信外链接底部引用
|
||||||
|
const isCiteStatus = useStorage(`isCiteStatus`, false)
|
||||||
|
const toggleCiteStatus = useToggle(isCiteStatus)
|
||||||
|
|
||||||
|
const output = ref(``)
|
||||||
|
|
||||||
|
// 文本字体
|
||||||
|
const fontFamily = useStorage(`fonts`, fontFamilyOptions[0].value)
|
||||||
|
// 文本大小
|
||||||
|
const fontSize = useStorage(`size`, fontSizeOptions[2].value)
|
||||||
|
// 文本颜色
|
||||||
|
const fontColor = useStorage(`color`, colorOptions[0].value)
|
||||||
|
// 代码块主题
|
||||||
|
const codeBlockTheme = useStorage(`codeBlockTheme`, codeBlockThemeOptions[2].value)
|
||||||
|
// 图注格式
|
||||||
|
const legend = useStorage(`legend`, legendOptions[3].value)
|
||||||
|
|
||||||
|
const wxRenderer = new WxRenderer({
|
||||||
|
theme: setColor(fontColor.value),
|
||||||
|
fonts: fontFamily.value,
|
||||||
|
size: fontSize.value,
|
||||||
})
|
})
|
||||||
},
|
|
||||||
initEditorEntity() {
|
|
||||||
const editorDom = document.getElementById(`editor`)
|
|
||||||
|
|
||||||
if (!editorDom.value) {
|
// 内容编辑器编辑器
|
||||||
editorDom.value =
|
const editor = ref(null)
|
||||||
localStorage.getItem(`__editor_content`) || formatDoc(DEFAULT_CONTENT)
|
// 编辑区域内容
|
||||||
|
const editorContent = useStorage(`__editor_content`, formatDoc(DEFAULT_CONTENT))
|
||||||
|
|
||||||
|
// 格式化文档
|
||||||
|
const formatContent = () => {
|
||||||
|
const doc = formatDoc(editor.value.getValue())
|
||||||
|
editorContent.value = doc
|
||||||
|
editor.value.setValue(doc)
|
||||||
}
|
}
|
||||||
this.editor = CodeMirror.fromTextArea(editorDom, {
|
|
||||||
mode: `text/x-markdown`,
|
|
||||||
theme: `xq-light`,
|
|
||||||
lineNumbers: false,
|
|
||||||
lineWrapping: true,
|
|
||||||
styleActiveLine: true,
|
|
||||||
autoCloseBrackets: true,
|
|
||||||
extraKeys: {
|
|
||||||
[`${modPrefix}-F`]: function autoFormat(editor) {
|
|
||||||
const doc = formatDoc(editor.getValue(0))
|
|
||||||
localStorage.setItem(`__editor_content`, doc)
|
|
||||||
editor.setValue(doc)
|
|
||||||
},
|
|
||||||
[`${modPrefix}-B`]: function bold(editor) {
|
|
||||||
const selected = editor.getSelection()
|
|
||||||
editor.replaceSelection(`**${selected}**`)
|
|
||||||
},
|
|
||||||
[`${modPrefix}-D`]: function del(editor) {
|
|
||||||
const selected = editor.getSelection()
|
|
||||||
editor.replaceSelection(`~~${selected}~~`)
|
|
||||||
},
|
|
||||||
[`${modPrefix}-I`]: function italic(editor) {
|
|
||||||
const selected = editor.getSelection()
|
|
||||||
editor.replaceSelection(`*${selected}*`)
|
|
||||||
},
|
|
||||||
[`${modPrefix}-K`]: function italic(editor) {
|
|
||||||
const selected = editor.getSelection()
|
|
||||||
editor.replaceSelection(`[${selected}]()`)
|
|
||||||
},
|
|
||||||
[`${modPrefix}-L`]: function code(editor) {
|
|
||||||
const selected = editor.getSelection()
|
|
||||||
editor.replaceSelection(`\`${selected}\``)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
},
|
|
||||||
initCssEditorEntity() {
|
|
||||||
const cssEditorDom = document.getElementById(`cssEditor`)
|
|
||||||
|
|
||||||
if (!cssEditorDom.value) {
|
// 切换 highlight.js 代码主题
|
||||||
cssEditorDom.value =
|
const codeThemeChange = () => {
|
||||||
localStorage.getItem(`__css_content`) || DEFAULT_CSS_CONTENT
|
const cssUrl = codeBlockTheme.value
|
||||||
|
const el = document.querySelector(`#hljs`)
|
||||||
|
if (el) {
|
||||||
|
el.setAttribute(`href`, cssUrl)
|
||||||
}
|
}
|
||||||
this.cssEditor = CodeMirror.fromTextArea(cssEditorDom, {
|
else {
|
||||||
mode: `css`,
|
const link = document.createElement(`link`)
|
||||||
theme: `style-mirror`,
|
link.setAttribute(`type`, `text/css`)
|
||||||
lineNumbers: false,
|
link.setAttribute(`rel`, `stylesheet`)
|
||||||
lineWrapping: true,
|
link.setAttribute(`href`, cssUrl)
|
||||||
matchBrackets: true,
|
link.setAttribute(`id`, `hljs`)
|
||||||
autofocus: true,
|
document.head.appendChild(link)
|
||||||
extraKeys: {
|
}
|
||||||
[`${modPrefix}-F`]: function autoFormat(editor) {
|
}
|
||||||
const doc = formatCss(editor.getValue(0))
|
|
||||||
localStorage.setItem(`__css_content`, doc)
|
// 更新编辑器
|
||||||
editor.setValue(doc)
|
const editorRefresh = () => {
|
||||||
},
|
codeThemeChange()
|
||||||
[`${modPrefix}-S`]: function save(editor) {},
|
|
||||||
},
|
const renderer = wxRenderer.getRenderer(isCiteStatus.value)
|
||||||
})
|
|
||||||
},
|
|
||||||
editorRefresh() {
|
|
||||||
const renderer = this.wxRenderer.getRenderer(this.citeStatus)
|
|
||||||
marked.setOptions({ renderer })
|
marked.setOptions({ renderer })
|
||||||
let output = marked.parse(this.editor.getValue(0))
|
let outputTemp = marked.parse(editor.value.getValue(0))
|
||||||
|
|
||||||
// 去除第一行的 margin-top
|
// 去除第一行的 margin-top
|
||||||
output = output.replace(/(style=".*?)"/, `$1;margin-top: 0"`)
|
outputTemp = outputTemp.replace(/(style=".*?)"/, `$1;margin-top: 0"`)
|
||||||
if (this.citeStatus) {
|
if (isCiteStatus.value) {
|
||||||
// 引用脚注
|
// 引用脚注
|
||||||
output += this.wxRenderer.buildFootnotes()
|
outputTemp += wxRenderer.buildFootnotes()
|
||||||
// 附加的一些 style
|
// 附加的一些 style
|
||||||
output += this.wxRenderer.buildAddition()
|
outputTemp += wxRenderer.buildAddition()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isMacCodeBlock) {
|
if (isMacCodeBlock.value) {
|
||||||
output += `
|
outputTemp += `
|
||||||
<style>
|
<style>
|
||||||
.hljs.code__pre::before {
|
.hljs.code__pre::before {
|
||||||
position: initial;
|
position: initial;
|
||||||
@ -215,7 +126,303 @@ export const useStore = defineStore(`store`, {
|
|||||||
</style>
|
</style>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
this.output = output
|
|
||||||
},
|
output.value = outputTemp
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自义定 CSS 编辑器
|
||||||
|
const cssEditor = ref(null)
|
||||||
|
const setCssEditorValue = (content) => {
|
||||||
|
cssEditor.value.setValue(content)
|
||||||
|
}
|
||||||
|
// 自定义 CSS 内容
|
||||||
|
const cssContent = useStorage(`__css_content`, DEFAULT_CSS_CONTENT)
|
||||||
|
const cssContentConfig = useStorage(addPrefix(`css_content_config`), {
|
||||||
|
active: `方案 1`,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
title: `方案 1`,
|
||||||
|
name: `方案 1`,
|
||||||
|
// 兼容之前的方案
|
||||||
|
content: cssContent.value || DEFAULT_CSS_CONTENT,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
const getCurrentTab = () => cssContentConfig.value.tabs.find((tab) => {
|
||||||
|
return tab.name === cssContentConfig.value.active
|
||||||
|
})
|
||||||
|
const tabChanged = (name) => {
|
||||||
|
cssContentConfig.value.active = name
|
||||||
|
const content = cssContentConfig.value.tabs.find((tab) => {
|
||||||
|
return tab.name === name
|
||||||
|
}).content
|
||||||
|
setCssEditorValue(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addCssContentTab = (name) => {
|
||||||
|
cssContentConfig.value.tabs.push({
|
||||||
|
name,
|
||||||
|
title: name,
|
||||||
|
content: DEFAULT_CSS_CONTENT,
|
||||||
|
})
|
||||||
|
cssContentConfig.value.active = name
|
||||||
|
setCssEditorValue(DEFAULT_CSS_CONTENT)
|
||||||
|
}
|
||||||
|
const validatorTabName = (val) => {
|
||||||
|
return cssContentConfig.value.tabs.every(({ name }) => name !== val)
|
||||||
|
}
|
||||||
|
// 更新 CSS
|
||||||
|
const updateCss = () => {
|
||||||
|
const json = css2json(cssEditor.value.getValue())
|
||||||
|
let theme = setFontSize(fontSize.value.replace(`px`, ``))
|
||||||
|
|
||||||
|
theme = customCssWithTemplate(json, fontColor.value, theme)
|
||||||
|
wxRenderer.setOptions({
|
||||||
|
theme,
|
||||||
|
})
|
||||||
|
editorRefresh()
|
||||||
|
}
|
||||||
|
// 初始化 CSS 编辑器
|
||||||
|
onMounted(() => {
|
||||||
|
const cssEditorDom = document.querySelector(`#cssEditor`)
|
||||||
|
cssEditorDom.value = getCurrentTab().content
|
||||||
|
|
||||||
|
cssEditor.value = markRaw(
|
||||||
|
CodeMirror.fromTextArea(cssEditorDom, {
|
||||||
|
mode: `css`,
|
||||||
|
theme: `style-mirror`,
|
||||||
|
lineNumbers: false,
|
||||||
|
lineWrapping: true,
|
||||||
|
matchBrackets: true,
|
||||||
|
autofocus: true,
|
||||||
|
extraKeys: {
|
||||||
|
[`${modPrefix}-F`]: function autoFormat(editor) {
|
||||||
|
const doc = formatCss(editor.getValue())
|
||||||
|
getCurrentTab().content = doc
|
||||||
|
editor.setValue(doc)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 自动提示
|
||||||
|
cssEditor.value.on(`keyup`, (cm, e) => {
|
||||||
|
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
|
||||||
|
cm.showHint(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 实时保存
|
||||||
|
cssEditor.value.on(`update`, () => {
|
||||||
|
updateCss()
|
||||||
|
getCurrentTab().content = cssEditor.value.getValue()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 重置样式
|
||||||
|
const resetStyle = () => {
|
||||||
|
isCiteStatus.value = false
|
||||||
|
isMacCodeBlock.value = true
|
||||||
|
|
||||||
|
fontFamily.value = fontFamilyOptions[0].value
|
||||||
|
fontSize.value = fontSizeOptions[2].value
|
||||||
|
fontColor.value = colorOptions[0].value
|
||||||
|
codeBlockTheme.value = codeBlockThemeOptions[2].value
|
||||||
|
legend.value = legendOptions[3].value
|
||||||
|
|
||||||
|
cssContentConfig.value = {
|
||||||
|
active: `方案 1`,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
title: `方案 1`,
|
||||||
|
name: `方案 1`,
|
||||||
|
// 兼容之前的方案
|
||||||
|
content: cssContent.value || DEFAULT_CSS_CONTENT,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
cssEditor.value.setValue(DEFAULT_CSS_CONTENT)
|
||||||
|
|
||||||
|
updateCss()
|
||||||
|
editorRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为函数添加刷新编辑器的功能
|
||||||
|
const withAfterRefresh = fn => (...rest) => {
|
||||||
|
fn(...rest)
|
||||||
|
editorRefresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTheme = (size, color) => {
|
||||||
|
const theme = setFontSize(size.replace(`px`, ``))
|
||||||
|
return setColorWithCustomTemplate(theme, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontChanged = withAfterRefresh((fonts) => {
|
||||||
|
wxRenderer.setOptions({
|
||||||
|
fonts,
|
||||||
|
})
|
||||||
|
|
||||||
|
fontFamily.value = fonts
|
||||||
|
})
|
||||||
|
|
||||||
|
const sizeChanged = withAfterRefresh((size) => {
|
||||||
|
const theme = getTheme(size, fontColor.value)
|
||||||
|
wxRenderer.setOptions({
|
||||||
|
size,
|
||||||
|
theme,
|
||||||
|
})
|
||||||
|
|
||||||
|
fontSize.value = size
|
||||||
|
})
|
||||||
|
|
||||||
|
const colorChanged = withAfterRefresh((newColor) => {
|
||||||
|
const theme = getTheme(fontSize.value, newColor)
|
||||||
|
wxRenderer.setOptions({
|
||||||
|
theme,
|
||||||
|
})
|
||||||
|
|
||||||
|
fontColor.value = newColor
|
||||||
|
})
|
||||||
|
|
||||||
|
const codeBlockThemeChanged = withAfterRefresh((newTheme) => {
|
||||||
|
codeBlockTheme.value = newTheme
|
||||||
|
})
|
||||||
|
|
||||||
|
const legendChanged = withAfterRefresh((newVal) => {
|
||||||
|
legend.value = newVal
|
||||||
|
})
|
||||||
|
|
||||||
|
const macCodeBlockChanged = withAfterRefresh(() => {
|
||||||
|
toggleMacCodeBlock()
|
||||||
|
})
|
||||||
|
|
||||||
|
const citeStatusChanged = withAfterRefresh(() => {
|
||||||
|
toggleCiteStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 导出编辑器内容为 HTML,并且下载到本地
|
||||||
|
const exportEditorContent2HTML = () => {
|
||||||
|
exportHTML()
|
||||||
|
document.querySelector(`#output`).innerHTML = output.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出编辑器内容到本地
|
||||||
|
const exportEditorContent2MD = () => {
|
||||||
|
downloadMD(editor.value.getValue())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导入 Markdown 文档
|
||||||
|
const importMarkdownContent = () => {
|
||||||
|
const body = document.body
|
||||||
|
const input = document.createElement(`input`)
|
||||||
|
input.type = `file`
|
||||||
|
input.name = `filename`
|
||||||
|
input.accept = `.md`
|
||||||
|
input.onchange = () => {
|
||||||
|
const file = input.files[0]
|
||||||
|
if (!file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.readAsText(file)
|
||||||
|
reader.onload = (event) => {
|
||||||
|
editor.value.setValue(formatDoc(event.target.result))
|
||||||
|
ElMessage.success(`文档导入成功`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body.appendChild(input)
|
||||||
|
input.click()
|
||||||
|
body.removeChild(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置样式
|
||||||
|
const resetStyleConfirm = () => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`此操作将丢失本地自定义样式,是否继续?`,
|
||||||
|
`提示`,
|
||||||
|
{
|
||||||
|
confirmButtonText: `确定`,
|
||||||
|
cancelButtonText: `取消`,
|
||||||
|
type: `warning`,
|
||||||
|
center: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
resetStyle()
|
||||||
|
ElMessage({
|
||||||
|
type: `success`,
|
||||||
|
message: `样式重置成功~`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
editor.value.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isShowCssEditor = ref(false)
|
||||||
|
const toggleShowCssEditor = useToggle(isShowCssEditor)
|
||||||
|
|
||||||
|
const isShowInsertFormDialog = ref(false)
|
||||||
|
const toggleShowInsertFormDialog = useToggle(isShowInsertFormDialog)
|
||||||
|
|
||||||
|
const isShowUploadImgDialog = ref(false)
|
||||||
|
const toggleShowUploadImgDialog = useToggle(isShowUploadImgDialog)
|
||||||
|
|
||||||
|
return {
|
||||||
|
isShowCssEditor,
|
||||||
|
toggleShowCssEditor,
|
||||||
|
isShowInsertFormDialog,
|
||||||
|
toggleShowInsertFormDialog,
|
||||||
|
isShowUploadImgDialog,
|
||||||
|
toggleShowUploadImgDialog,
|
||||||
|
|
||||||
|
isDark,
|
||||||
|
toggleDark,
|
||||||
|
|
||||||
|
isEditOnLeft,
|
||||||
|
toggleEditOnLeft,
|
||||||
|
|
||||||
|
isMacCodeBlock,
|
||||||
|
isCiteStatus,
|
||||||
|
citeStatusChanged,
|
||||||
|
|
||||||
|
output,
|
||||||
|
editor,
|
||||||
|
cssEditor,
|
||||||
|
fontFamily,
|
||||||
|
fontSize,
|
||||||
|
fontColor,
|
||||||
|
codeBlockTheme,
|
||||||
|
legend,
|
||||||
|
|
||||||
|
editorRefresh,
|
||||||
|
|
||||||
|
fontChanged,
|
||||||
|
sizeChanged,
|
||||||
|
colorChanged,
|
||||||
|
codeBlockThemeChanged,
|
||||||
|
legendChanged,
|
||||||
|
macCodeBlockChanged,
|
||||||
|
|
||||||
|
formatContent,
|
||||||
|
exportEditorContent2HTML,
|
||||||
|
exportEditorContent2MD,
|
||||||
|
|
||||||
|
importMarkdownContent,
|
||||||
|
|
||||||
|
resetStyleConfirm,
|
||||||
|
editorContent,
|
||||||
|
|
||||||
|
cssContentConfig,
|
||||||
|
addCssContentTab,
|
||||||
|
validatorTabName,
|
||||||
|
setCssEditorValue,
|
||||||
|
tabChanged,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default createPinia()
|
||||||
|
@ -8,7 +8,7 @@ const service = axios.create({
|
|||||||
|
|
||||||
service.interceptors.request.use(
|
service.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
if (/^(post)|(put)|(delete)$/i.test(config.method)) {
|
if (/^(?:post|put|delete)$/i.test(config.method)) {
|
||||||
if (config.data && config.data.upload) {
|
if (config.data && config.data.upload) {
|
||||||
config.headers[`Content-Type`] = `multipart/form-data`
|
config.headers[`Content-Type`] = `multipart/form-data`
|
||||||
}
|
}
|
||||||
@ -17,14 +17,14 @@ service.interceptors.request.use(
|
|||||||
},
|
},
|
||||||
(error) => {
|
(error) => {
|
||||||
Promise.reject(error)
|
Promise.reject(error)
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
service.interceptors.response.use(
|
service.interceptors.response.use(
|
||||||
(res) => {
|
(res) => {
|
||||||
return res.data ? res.data : Promise.reject(res)
|
return res.data ? res.data : Promise.reject(res)
|
||||||
},
|
},
|
||||||
(error) => Promise.reject(error)
|
error => Promise.reject(error),
|
||||||
)
|
)
|
||||||
|
|
||||||
export default service
|
export default service
|
@ -1,5 +1,3 @@
|
|||||||
import fetch from './fetch'
|
|
||||||
import { githubConfig, giteeConfig } from './config'
|
|
||||||
import CryptoJS from 'crypto-js'
|
import CryptoJS from 'crypto-js'
|
||||||
import OSS from 'ali-oss'
|
import OSS from 'ali-oss'
|
||||||
import * as Minio from 'minio'
|
import * as Minio from 'minio'
|
||||||
@ -7,8 +5,11 @@ import COS from 'cos-js-sdk-v5'
|
|||||||
import Buffer from 'buffer-from'
|
import Buffer from 'buffer-from'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
import * as qiniu from 'qiniu-js'
|
import * as qiniu from 'qiniu-js'
|
||||||
import { utf16to8, base64encode, safe64 } from '../assets/scripts/tokenTools'
|
|
||||||
import * as tokenTools from '../assets/scripts/tokenTools'
|
import fetch from '@/utils/fetch'
|
||||||
|
import { base64encode, safe64, utf16to8 } from '@/utils/tokenTools'
|
||||||
|
import * as tokenTools from '@/utils/tokenTools'
|
||||||
|
import { giteeConfig, githubConfig } from '@/config'
|
||||||
|
|
||||||
function getConfig(useDefault, platform) {
|
function getConfig(useDefault, platform) {
|
||||||
if (useDefault) {
|
if (useDefault) {
|
||||||
@ -59,7 +60,7 @@ function getDir() {
|
|||||||
/**
|
/**
|
||||||
* 根据文件名获取它以 `时间戳+uuid` 的形式
|
* 根据文件名获取它以 `时间戳+uuid` 的形式
|
||||||
* @param {string} filename 文件名
|
* @param {string} filename 文件名
|
||||||
* @returns
|
* @returns {string} `时间戳+uuid`
|
||||||
*/
|
*/
|
||||||
function getDateFilename(filename) {
|
function getDateFilename(filename) {
|
||||||
const currentTimestamp = new Date().getTime()
|
const currentTimestamp = new Date().getTime()
|
||||||
@ -75,7 +76,7 @@ async function ghFileUpload(content, filename) {
|
|||||||
const useDefault = localStorage.getItem(`imgHost`) === `default`
|
const useDefault = localStorage.getItem(`imgHost`) === `default`
|
||||||
const { username, repo, branch, accessToken } = getConfig(
|
const { username, repo, branch, accessToken } = getConfig(
|
||||||
useDefault,
|
useDefault,
|
||||||
`github`
|
`github`,
|
||||||
)
|
)
|
||||||
const dir = getDir()
|
const dir = getDir()
|
||||||
const url = `https://api.github.com/repos/${username}/${repo}/contents/${dir}/`
|
const url = `https://api.github.com/repos/${username}/${repo}/contents/${dir}/`
|
||||||
@ -138,7 +139,7 @@ function getQiniuToken(accessKey, secretKey, putPolicy) {
|
|||||||
|
|
||||||
async function qiniuUpload(file) {
|
async function qiniuUpload(file) {
|
||||||
const { accessKey, secretKey, bucket, region, path, domain } = JSON.parse(
|
const { accessKey, secretKey, bucket, region, path, domain } = JSON.parse(
|
||||||
localStorage.getItem(`qiniuConfig`)
|
localStorage.getItem(`qiniuConfig`),
|
||||||
)
|
)
|
||||||
const token = getQiniuToken(accessKey, secretKey, {
|
const token = getQiniuToken(accessKey, secretKey, {
|
||||||
scope: bucket,
|
scope: bucket,
|
||||||
@ -168,8 +169,8 @@ async function qiniuUpload(file) {
|
|||||||
|
|
||||||
async function aliOSSFileUpload(content, filename) {
|
async function aliOSSFileUpload(content, filename) {
|
||||||
const dateFilename = getDateFilename(filename)
|
const dateFilename = getDateFilename(filename)
|
||||||
const { region, bucket, accessKeyId, accessKeySecret, cdnHost, path } =
|
const { region, bucket, accessKeyId, accessKeySecret, cdnHost, path }
|
||||||
JSON.parse(localStorage.getItem(`aliOSSConfig`))
|
= JSON.parse(localStorage.getItem(`aliOSSConfig`))
|
||||||
const buffer = Buffer(content, `base64`)
|
const buffer = Buffer(content, `base64`)
|
||||||
const dir = `${path}/${dateFilename}`
|
const dir = `${path}/${dateFilename}`
|
||||||
const client = new OSS({
|
const client = new OSS({
|
||||||
@ -180,9 +181,11 @@ async function aliOSSFileUpload(content, filename) {
|
|||||||
})
|
})
|
||||||
try {
|
try {
|
||||||
const res = await client.put(dir, buffer)
|
const res = await client.put(dir, buffer)
|
||||||
if (cdnHost === ``) return res.url
|
if (cdnHost === ``)
|
||||||
|
return res.url
|
||||||
return `${cdnHost}/${path === `` ? dateFilename : dir}`
|
return `${cdnHost}/${path === `` ? dateFilename : dir}`
|
||||||
} catch (e) {
|
}
|
||||||
|
catch (e) {
|
||||||
return Promise.reject(e)
|
return Promise.reject(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,7 +197,7 @@ async function aliOSSFileUpload(content, filename) {
|
|||||||
async function txCOSFileUpload(file) {
|
async function txCOSFileUpload(file) {
|
||||||
const dateFilename = getDateFilename(file.name)
|
const dateFilename = getDateFilename(file.name)
|
||||||
const { secretId, secretKey, bucket, region, path, cdnHost } = JSON.parse(
|
const { secretId, secretKey, bucket, region, path, cdnHost } = JSON.parse(
|
||||||
localStorage.getItem(`txCOSConfig`)
|
localStorage.getItem(`txCOSConfig`),
|
||||||
)
|
)
|
||||||
const cos = new COS({
|
const cos = new COS({
|
||||||
SecretId: secretId,
|
SecretId: secretId,
|
||||||
@ -208,19 +211,21 @@ async function txCOSFileUpload(file) {
|
|||||||
Key: `${path}/${dateFilename}`,
|
Key: `${path}/${dateFilename}`,
|
||||||
Body: file,
|
Body: file,
|
||||||
},
|
},
|
||||||
function (err, data) {
|
(err, data) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
reject(err)
|
reject(err)
|
||||||
} else if (cdnHost) {
|
}
|
||||||
|
else if (cdnHost) {
|
||||||
resolve(
|
resolve(
|
||||||
path == ``
|
path === ``
|
||||||
? `${cdnHost}/${dateFilename}`
|
? `${cdnHost}/${dateFilename}`
|
||||||
: `${cdnHost}/${path}/${dateFilename}`
|
: `${cdnHost}/${path}/${dateFilename}`,
|
||||||
)
|
)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
resolve(`https://${data.Location}`)
|
resolve(`https://${data.Location}`)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -232,14 +237,14 @@ async function txCOSFileUpload(file) {
|
|||||||
async function minioFileUpload(content, filename) {
|
async function minioFileUpload(content, filename) {
|
||||||
const dateFilename = getDateFilename(filename)
|
const dateFilename = getDateFilename(filename)
|
||||||
const { endpoint, port, useSSL, bucket, accessKey, secretKey } = JSON.parse(
|
const { endpoint, port, useSSL, bucket, accessKey, secretKey } = JSON.parse(
|
||||||
localStorage.getItem(`minioConfig`)
|
localStorage.getItem(`minioConfig`),
|
||||||
)
|
)
|
||||||
const buffer = Buffer(content, `base64`)
|
const buffer = Buffer(content, `base64`)
|
||||||
const conf = {
|
const conf = {
|
||||||
endPoint: endpoint,
|
endPoint: endpoint,
|
||||||
useSSL: useSSL,
|
useSSL,
|
||||||
accessKey: accessKey,
|
accessKey,
|
||||||
secretKey: secretKey,
|
secretKey,
|
||||||
}
|
}
|
||||||
const p = Number(port || 0)
|
const p = Number(port || 0)
|
||||||
const isCustomPort = p > 0 && p !== 80 && p !== 443
|
const isCustomPort = p > 0 && p !== 80 && p !== 443
|
||||||
@ -249,19 +254,20 @@ async function minioFileUpload(content, filename) {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const minioClient = new Minio.Client(conf)
|
const minioClient = new Minio.Client(conf)
|
||||||
try {
|
try {
|
||||||
minioClient.putObject(bucket, dateFilename, buffer, function (e) {
|
minioClient.putObject(bucket, dateFilename, buffer, (e) => {
|
||||||
if (e) {
|
if (e) {
|
||||||
reject(e)
|
reject(e)
|
||||||
}
|
}
|
||||||
const host = `${useSSL ? `https://` : `http://`}${endpoint}${
|
const host = `${useSSL ? `https://` : `http://`}${endpoint}${
|
||||||
isCustomPort ? `:` + port : ``
|
isCustomPort ? `:${port}` : ``
|
||||||
}`
|
}`
|
||||||
const url = `${host}/${bucket}/${dateFilename}`
|
const url = `${host}/${bucket}/${dateFilename}`
|
||||||
// console.log("文件上传成功: ", url)
|
// console.log("文件上传成功: ", url)
|
||||||
resolve(url)
|
resolve(url)
|
||||||
// return `${endpoint}/${bucket}/${dateFilename}`;
|
// return `${endpoint}/${bucket}/${dateFilename}`;
|
||||||
})
|
})
|
||||||
} catch (e) {
|
}
|
||||||
|
catch (e) {
|
||||||
reject(e)
|
reject(e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -296,6 +302,7 @@ async function formCustomUpload(content, file) {
|
|||||||
okCb: resolve, // 重要: 上传成功后给此回调传 url 即可
|
okCb: resolve, // 重要: 上传成功后给此回调传 url 即可
|
||||||
errCb: reject, // 上传失败调用的函数
|
errCb: reject, // 上传失败调用的函数
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line no-eval
|
||||||
eval(str)(exportObj).catch((err) => {
|
eval(str)(exportObj).catch((err) => {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
reject(err)
|
reject(err)
|
@ -1,15 +1,21 @@
|
|||||||
|
import juice from 'juice'
|
||||||
import prettier from 'prettier/standalone'
|
import prettier from 'prettier/standalone'
|
||||||
import prettierCss from 'prettier/parser-postcss'
|
import prettierCss from 'prettier/parser-postcss'
|
||||||
import prettierMarkdown from 'prettier/parser-markdown'
|
import prettierMarkdown from 'prettier/parser-markdown'
|
||||||
import defaultTheme from './themes/default-theme'
|
|
||||||
|
|
||||||
const createCustomTheme = (theme, color) => {
|
import { defaultTheme, prefix } from '@/config'
|
||||||
|
|
||||||
|
export function addPrefix(str) {
|
||||||
|
return `${prefix}__${str}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCustomTheme(theme, color) {
|
||||||
const customTheme = JSON.parse(JSON.stringify(theme))
|
const customTheme = JSON.parse(JSON.stringify(theme))
|
||||||
customTheme.block.h1[`border-bottom`] = `2px solid ${color}`
|
customTheme.block.h1[`border-bottom`] = `2px solid ${color}`
|
||||||
customTheme.block.h2[`background`] = color
|
customTheme.block.h2.background = color
|
||||||
customTheme.block.h3[`border-left`] = `3px solid ${color}`
|
customTheme.block.h3[`border-left`] = `3px solid ${color}`
|
||||||
customTheme.block.h4[`color`] = color
|
customTheme.block.h4.color = color
|
||||||
customTheme.inline.strong[`color`] = color
|
customTheme.inline.strong.color = color
|
||||||
return customTheme
|
return customTheme
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -49,45 +55,45 @@ export function customCssWithTemplate(jsonString, color, theme) {
|
|||||||
customTheme.block.h4 = Object.assign(customTheme.block.h4, jsonString.h4)
|
customTheme.block.h4 = Object.assign(customTheme.block.h4, jsonString.h4)
|
||||||
customTheme.block.code = Object.assign(
|
customTheme.block.code = Object.assign(
|
||||||
customTheme.block.code,
|
customTheme.block.code,
|
||||||
jsonString.code
|
jsonString.code,
|
||||||
)
|
)
|
||||||
customTheme.block.p = Object.assign(customTheme.block.p, jsonString.p)
|
customTheme.block.p = Object.assign(customTheme.block.p, jsonString.p)
|
||||||
customTheme.block.hr = Object.assign(customTheme.block.hr, jsonString.hr)
|
customTheme.block.hr = Object.assign(customTheme.block.hr, jsonString.hr)
|
||||||
customTheme.block.blockquote = Object.assign(
|
customTheme.block.blockquote = Object.assign(
|
||||||
customTheme.block.blockquote,
|
customTheme.block.blockquote,
|
||||||
jsonString.blockquote
|
jsonString.blockquote,
|
||||||
)
|
)
|
||||||
customTheme.block.blockquote_p = Object.assign(
|
customTheme.block.blockquote_p = Object.assign(
|
||||||
customTheme.block.blockquote_p,
|
customTheme.block.blockquote_p,
|
||||||
jsonString.blockquote_p
|
jsonString.blockquote_p,
|
||||||
)
|
)
|
||||||
customTheme.block.image = Object.assign(
|
customTheme.block.image = Object.assign(
|
||||||
customTheme.block.image,
|
customTheme.block.image,
|
||||||
jsonString.image
|
jsonString.image,
|
||||||
)
|
)
|
||||||
|
|
||||||
// inline
|
// inline
|
||||||
customTheme.inline.strong = Object.assign(
|
customTheme.inline.strong = Object.assign(
|
||||||
customTheme.inline.strong,
|
customTheme.inline.strong,
|
||||||
jsonString.strong
|
jsonString.strong,
|
||||||
)
|
)
|
||||||
customTheme.inline.codespan = Object.assign(
|
customTheme.inline.codespan = Object.assign(
|
||||||
customTheme.inline.codespan,
|
customTheme.inline.codespan,
|
||||||
jsonString.codespan
|
jsonString.codespan,
|
||||||
)
|
)
|
||||||
customTheme.inline.link = Object.assign(
|
customTheme.inline.link = Object.assign(
|
||||||
customTheme.inline.link,
|
customTheme.inline.link,
|
||||||
jsonString.link
|
jsonString.link,
|
||||||
)
|
)
|
||||||
customTheme.inline.wx_link = Object.assign(
|
customTheme.inline.wx_link = Object.assign(
|
||||||
customTheme.inline.wx_link,
|
customTheme.inline.wx_link,
|
||||||
jsonString.wx_link
|
jsonString.wx_link,
|
||||||
)
|
)
|
||||||
customTheme.block.ul = Object.assign(customTheme.block.ul, jsonString.ul)
|
customTheme.block.ul = Object.assign(customTheme.block.ul, jsonString.ul)
|
||||||
customTheme.block.ol = Object.assign(customTheme.block.ol, jsonString.ol)
|
customTheme.block.ol = Object.assign(customTheme.block.ol, jsonString.ol)
|
||||||
customTheme.inline.listitem = Object.assign(
|
customTheme.inline.listitem = Object.assign(
|
||||||
customTheme.inline.listitem,
|
customTheme.inline.listitem,
|
||||||
jsonString.li
|
jsonString.li,
|
||||||
)
|
)
|
||||||
return customTheme
|
return customTheme
|
||||||
}
|
}
|
||||||
@ -101,16 +107,16 @@ export function css2json(css) {
|
|||||||
// 移除CSS所有注释
|
// 移除CSS所有注释
|
||||||
let open, close
|
let open, close
|
||||||
while (
|
while (
|
||||||
(open = css.indexOf(`/*`)) !== -1 &&
|
(open = css.indexOf(`/*`)) !== -1
|
||||||
(close = css.indexOf(`*/`)) !== -1
|
&& (close = css.indexOf(`*/`)) !== -1
|
||||||
) {
|
) {
|
||||||
css = css.substring(0, open) + css.substring(close + 2)
|
css = css.substring(0, open) + css.substring(close + 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化返回值
|
// 初始化返回值
|
||||||
let json = {}
|
const json = {}
|
||||||
|
|
||||||
while (css.length > 0 && css.indexOf(`{`) !== -1 && css.indexOf(`}`) !== -1) {
|
while (css.length > 0 && css.includes(`{`) && css.includes(`}`)) {
|
||||||
// 存储第一个左/右花括号的下标
|
// 存储第一个左/右花括号的下标
|
||||||
const lbracket = css.indexOf(`{`)
|
const lbracket = css.indexOf(`{`)
|
||||||
const rbracket = css.indexOf(`}`)
|
const rbracket = css.indexOf(`}`)
|
||||||
@ -121,9 +127,9 @@ export function css2json(css) {
|
|||||||
// `{"font": "'Times New Roman' 1em", "color": "#ff0000", "margin-top": "1em"}`
|
// `{"font": "'Times New Roman' 1em", "color": "#ff0000", "margin-top": "1em"}`
|
||||||
|
|
||||||
// 辅助方法:将array转为object
|
// 辅助方法:将array转为object
|
||||||
// eslint-disable-next-line no-inner-declarations
|
|
||||||
function toObject(array) {
|
function toObject(array) {
|
||||||
let ret = {}
|
const ret = {}
|
||||||
array.forEach((e) => {
|
array.forEach((e) => {
|
||||||
const index = e.indexOf(`:`)
|
const index = e.indexOf(`:`)
|
||||||
const property = e.substring(0, index).trim()
|
const property = e.substring(0, index).trim()
|
||||||
@ -136,8 +142,8 @@ export function css2json(css) {
|
|||||||
let declarations = css
|
let declarations = css
|
||||||
.substring(lbracket + 1, rbracket)
|
.substring(lbracket + 1, rbracket)
|
||||||
.split(`;`)
|
.split(`;`)
|
||||||
.map((e) => e.trim())
|
.map(e => e.trim())
|
||||||
.filter((e) => e.length > 0) // 移除所有""空值
|
.filter(e => e.length > 0) // 移除所有""空值
|
||||||
|
|
||||||
// 转为Object对象
|
// 转为Object对象
|
||||||
declarations = toObject(declarations)
|
declarations = toObject(declarations)
|
||||||
@ -147,16 +153,17 @@ export function css2json(css) {
|
|||||||
// ==>
|
// ==>
|
||||||
// {"h1": {color: red}, "p#bar": {color: red}}
|
// {"h1": {color: red}, "p#bar": {color: red}}
|
||||||
|
|
||||||
let selectors = css
|
const selectors = css
|
||||||
.substring(0, lbracket)
|
.substring(0, lbracket)
|
||||||
// 以,切割,并移除空格:`"h1, p#bar, span.foo"` => ["h1", "p#bar", "span.foo"]
|
// 以,切割,并移除空格:`"h1, p#bar, span.foo"` => ["h1", "p#bar", "span.foo"]
|
||||||
.split(`,`)
|
.split(`,`)
|
||||||
.map((selector) => selector.trim())
|
.map(selector => selector.trim())
|
||||||
|
|
||||||
// 迭代赋值
|
// 迭代赋值
|
||||||
selectors.forEach((selector) => {
|
selectors.forEach((selector) => {
|
||||||
// 若不存在,则先初始化
|
// 若不存在,则先初始化
|
||||||
if (!json[selector]) json[selector] = {}
|
if (!json[selector])
|
||||||
|
json[selector] = {}
|
||||||
// 赋值到JSON
|
// 赋值到JSON
|
||||||
Object.keys(declarations).forEach((key) => {
|
Object.keys(declarations).forEach((key) => {
|
||||||
json[selector][key] = declarations[key]
|
json[selector][key] = declarations[key]
|
||||||
@ -180,7 +187,8 @@ export function saveEditorContent(editor, name) {
|
|||||||
const content = editor.getValue(0)
|
const content = editor.getValue(0)
|
||||||
if (content) {
|
if (content) {
|
||||||
localStorage.setItem(name, content)
|
localStorage.setItem(name, content)
|
||||||
} else {
|
}
|
||||||
|
else {
|
||||||
localStorage.removeItem(name)
|
localStorage.removeItem(name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,7 +244,7 @@ export function exportHTML() {
|
|||||||
|
|
||||||
downLink.download = `content.html`
|
downLink.download = `content.html`
|
||||||
downLink.style.display = `none`
|
downLink.style.display = `none`
|
||||||
let blob = new Blob([
|
const blob = new Blob([
|
||||||
`<html><head><meta charset="utf-8" /></head><body><div style="width: 750px; margin: auto;">${htmlStr}</div></body></html>`,
|
`<html><head><meta charset="utf-8" /></head><body><div style="width: 750px; margin: auto;">${htmlStr}</div></body></html>`,
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -256,7 +264,7 @@ export function exportHTML() {
|
|||||||
const styles = getComputedStyle(element, null)
|
const styles = getComputedStyle(element, null)
|
||||||
return Object.entries(styles)
|
return Object.entries(styles)
|
||||||
.filter(
|
.filter(
|
||||||
([key]) => styles.getPropertyValue(key) && !excludes.includes(key)
|
([key]) => styles.getPropertyValue(key) && !excludes.includes(key),
|
||||||
)
|
)
|
||||||
.map(([key, value]) => `${key}:${value};`)
|
.map(([key, value]) => `${key}:${value};`)
|
||||||
.join(``)
|
.join(``)
|
||||||
@ -271,14 +279,14 @@ export function exportHTML() {
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
if (element.children.length) {
|
if (element.children.length) {
|
||||||
Array.from(element.children).forEach((child) => setStyles(child))
|
Array.from(element.children).forEach(child => setStyles(child))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 判断是否是包裹代码块的 pre 元素
|
// 判断是否是包裹代码块的 pre 元素
|
||||||
function isPre(element) {
|
function isPre(element) {
|
||||||
return (
|
return (
|
||||||
element.tagName === `PRE` &&
|
element.tagName === `PRE`
|
||||||
Array.from(element.classList).includes(`code__pre`)
|
&& Array.from(element.classList).includes(`code__pre`)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -290,9 +298,9 @@ export function exportHTML() {
|
|||||||
// 判断是否是包裹代码字符的 span 元素
|
// 判断是否是包裹代码字符的 span 元素
|
||||||
function isSpan(element) {
|
function isSpan(element) {
|
||||||
return (
|
return (
|
||||||
element.tagName === `SPAN` &&
|
element.tagName === `SPAN`
|
||||||
(isCode(element.parentElement) ||
|
&& (isCode(element.parentElement)
|
||||||
isCode(element.parentElement.parentElement))
|
|| isCode(element.parentElement.parentElement))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,7 +333,7 @@ export function toBase64(file) {
|
|||||||
const reader = new FileReader()
|
const reader = new FileReader()
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(file)
|
||||||
reader.onload = () => resolve(reader.result.split(`,`).pop())
|
reader.onload = () => resolve(reader.result.split(`,`).pop())
|
||||||
reader.onerror = (error) => reject(error)
|
reader.onerror = error => reject(error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -354,15 +362,36 @@ export function checkImage(file) {
|
|||||||
/**
|
/**
|
||||||
* 移除左边多余空格
|
* 移除左边多余空格
|
||||||
* @param {*} str
|
* @param {*} str
|
||||||
* @returns
|
* @returns string
|
||||||
*/
|
*/
|
||||||
export function removeLeft(str) {
|
export function removeLeft(str) {
|
||||||
const lines = str.split(`\n`)
|
const lines = str.split(`\n`)
|
||||||
// 获取应该删除的空白符数量
|
// 获取应该删除的空白符数量
|
||||||
const minSpaceNum = lines
|
const minSpaceNum = lines
|
||||||
.filter((item) => item.trim())
|
.filter(item => item.trim())
|
||||||
.map((item) => item.match(/(^\s+)?/)[0].length)
|
.map(item => item.match(/(^\s+)?/)[0].length)
|
||||||
.sort((a, b) => a - b)[0]
|
.sort((a, b) => a - b)[0]
|
||||||
// 删除空白符
|
// 删除空白符
|
||||||
return lines.map((item) => item.slice(minSpaceNum)).join(`\n`)
|
return lines.map(item => item.slice(minSpaceNum)).join(`\n`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
}
|
}
|
@ -1,26 +1,28 @@
|
|||||||
export function utf16to8(str) {
|
export function utf16to8(str) {
|
||||||
var out, i, len, c
|
let out, i, len, c
|
||||||
out = ``
|
out = ``
|
||||||
len = str.length
|
len = str.length
|
||||||
for (i = 0; i < len; i++) {
|
for (i = 0; i < len; i++) {
|
||||||
c = str.charCodeAt(i)
|
c = str.charCodeAt(i)
|
||||||
if (c >= 0x0001 && c <= 0x007f) {
|
if (c >= 0x0001 && c <= 0x007F) {
|
||||||
out += str.charAt(i)
|
out += str.charAt(i)
|
||||||
} else if (c > 0x07ff) {
|
}
|
||||||
out += String.fromCharCode(0xe0 | ((c >> 12) & 0x0f))
|
else if (c > 0x07FF) {
|
||||||
out += String.fromCharCode(0x80 | ((c >> 6) & 0x3f))
|
out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F))
|
||||||
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f))
|
out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F))
|
||||||
} else {
|
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F))
|
||||||
out += String.fromCharCode(0xc0 | ((c >> 6) & 0x1f))
|
}
|
||||||
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3f))
|
else {
|
||||||
|
out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F))
|
||||||
|
out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
export function utf8to16(str) {
|
export function utf8to16(str) {
|
||||||
var out, i, len, c
|
let out, i, len, c
|
||||||
var char2, char3
|
let char2, char3
|
||||||
out = ``
|
out = ``
|
||||||
len = str.length
|
len = str.length
|
||||||
i = 0
|
i = 0
|
||||||
@ -42,14 +44,14 @@ export function utf8to16(str) {
|
|||||||
case 13:
|
case 13:
|
||||||
// 110x xxxx 10xx xxxx
|
// 110x xxxx 10xx xxxx
|
||||||
char2 = str.charCodeAt(i++)
|
char2 = str.charCodeAt(i++)
|
||||||
out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f))
|
out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F))
|
||||||
break
|
break
|
||||||
case 14:
|
case 14:
|
||||||
// 1110 xxxx 10xx xxxx 10xx xxxx
|
// 1110 xxxx 10xx xxxx 10xx xxxx
|
||||||
char2 = str.charCodeAt(i++)
|
char2 = str.charCodeAt(i++)
|
||||||
char3 = str.charCodeAt(i++)
|
char3 = str.charCodeAt(i++)
|
||||||
out += String.fromCharCode(
|
out += String.fromCharCode(
|
||||||
((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0)
|
((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0),
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -58,7 +60,8 @@ export function utf8to16(str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const base64EncodeChars = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_`
|
const base64EncodeChars = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_`
|
||||||
const base64DecodeChars = new Array(
|
|
||||||
|
const base64DecodeChars = [
|
||||||
-1,
|
-1,
|
||||||
-1,
|
-1,
|
||||||
-1,
|
-1,
|
||||||
@ -186,16 +189,17 @@ const base64DecodeChars = new Array(
|
|||||||
-1,
|
-1,
|
||||||
-1,
|
-1,
|
||||||
-1,
|
-1,
|
||||||
-1
|
-1,
|
||||||
)
|
]
|
||||||
|
|
||||||
export function base64encode(str) {
|
export function base64encode(str) {
|
||||||
var out, i, len
|
let out, i, len
|
||||||
var c1, c2, c3
|
let c1, c2, c3
|
||||||
len = str.length
|
len = str.length
|
||||||
i = 0
|
i = 0
|
||||||
out = ``
|
out = ``
|
||||||
while (i < len) {
|
while (i < len) {
|
||||||
c1 = str.charCodeAt(i++) & 0xff
|
c1 = str.charCodeAt(i++) & 0xFF
|
||||||
if (i == len) {
|
if (i == len) {
|
||||||
out += base64EncodeChars.charAt(c1 >> 2)
|
out += base64EncodeChars.charAt(c1 >> 2)
|
||||||
out += base64EncodeChars.charAt((c1 & 0x3) << 4)
|
out += base64EncodeChars.charAt((c1 & 0x3) << 4)
|
||||||
@ -205,53 +209,59 @@ export function base64encode(str) {
|
|||||||
c2 = str.charCodeAt(i++)
|
c2 = str.charCodeAt(i++)
|
||||||
if (i == len) {
|
if (i == len) {
|
||||||
out += base64EncodeChars.charAt(c1 >> 2)
|
out += base64EncodeChars.charAt(c1 >> 2)
|
||||||
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4))
|
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4))
|
||||||
out += base64EncodeChars.charAt((c2 & 0xf) << 2)
|
out += base64EncodeChars.charAt((c2 & 0xF) << 2)
|
||||||
out += `=`
|
out += `=`
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
c3 = str.charCodeAt(i++)
|
c3 = str.charCodeAt(i++)
|
||||||
out += base64EncodeChars.charAt(c1 >> 2)
|
out += base64EncodeChars.charAt(c1 >> 2)
|
||||||
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xf0) >> 4))
|
out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4))
|
||||||
out += base64EncodeChars.charAt(((c2 & 0xf) << 2) | ((c3 & 0xc0) >> 6))
|
out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6))
|
||||||
out += base64EncodeChars.charAt(c3 & 0x3f)
|
out += base64EncodeChars.charAt(c3 & 0x3F)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
export function base64decode(str) {
|
export function base64decode(str) {
|
||||||
var c1, c2, c3, c4
|
let c1, c2, c3, c4
|
||||||
var i, len, out
|
let i, len, out
|
||||||
len = str.length
|
len = str.length
|
||||||
i = 0
|
i = 0
|
||||||
out = ``
|
out = ``
|
||||||
while (i < len) {
|
while (i < len) {
|
||||||
/* c1 */
|
/* c1 */
|
||||||
do {
|
do {
|
||||||
c1 = base64DecodeChars[str.charCodeAt(i++) & 0xff]
|
c1 = base64DecodeChars[str.charCodeAt(i++) & 0xFF]
|
||||||
} while (i < len && c1 == -1)
|
} while (i < len && c1 == -1)
|
||||||
if (c1 == -1) break
|
if (c1 == -1)
|
||||||
|
break
|
||||||
/* c2 */
|
/* c2 */
|
||||||
do {
|
do {
|
||||||
c2 = base64DecodeChars[str.charCodeAt(i++) & 0xff]
|
c2 = base64DecodeChars[str.charCodeAt(i++) & 0xFF]
|
||||||
} while (i < len && c2 == -1)
|
} while (i < len && c2 == -1)
|
||||||
if (c2 == -1) break
|
if (c2 == -1)
|
||||||
|
break
|
||||||
out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4))
|
out += String.fromCharCode((c1 << 2) | ((c2 & 0x30) >> 4))
|
||||||
/* c3 */
|
/* c3 */
|
||||||
do {
|
do {
|
||||||
c3 = str.charCodeAt(i++) & 0xff
|
c3 = str.charCodeAt(i++) & 0xFF
|
||||||
if (c3 == 61) return out
|
if (c3 == 61)
|
||||||
|
return out
|
||||||
c3 = base64DecodeChars[c3]
|
c3 = base64DecodeChars[c3]
|
||||||
} while (i < len && c3 == -1)
|
} while (i < len && c3 == -1)
|
||||||
if (c3 == -1) break
|
if (c3 == -1)
|
||||||
out += String.fromCharCode(((c2 & 0xf) << 4) | ((c3 & 0x3c) >> 2))
|
break
|
||||||
|
out += String.fromCharCode(((c2 & 0xF) << 4) | ((c3 & 0x3C) >> 2))
|
||||||
/* c4 */
|
/* c4 */
|
||||||
do {
|
do {
|
||||||
c4 = str.charCodeAt(i++) & 0xff
|
c4 = str.charCodeAt(i++) & 0xFF
|
||||||
if (c4 == 61) return out
|
if (c4 == 61)
|
||||||
|
return out
|
||||||
c4 = base64DecodeChars[c4]
|
c4 = base64DecodeChars[c4]
|
||||||
} while (i < len && c4 == -1)
|
} while (i < len && c4 == -1)
|
||||||
if (c4 == -1) break
|
if (c4 == -1)
|
||||||
|
break
|
||||||
out += String.fromCharCode(((c3 & 0x03) << 6) | c4)
|
out += String.fromCharCode(((c3 & 0x03) << 6) | c4)
|
||||||
}
|
}
|
||||||
return out
|
return out
|
231
src/utils/wx-renderer.js
Normal file
231
src/utils/wx-renderer.js
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
import { Renderer, marked } from 'marked'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
import markedKatex from 'marked-katex-extension'
|
||||||
|
|
||||||
|
marked.use(markedKatex({
|
||||||
|
throwOnError: false,
|
||||||
|
output: `html`,
|
||||||
|
nonStandard: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
class WxRenderer {
|
||||||
|
constructor(opts) {
|
||||||
|
this.opts = opts
|
||||||
|
let footnotes = []
|
||||||
|
let footnoteIndex = 0
|
||||||
|
let styleMapping = new Map()
|
||||||
|
|
||||||
|
const merge = (base, extend) => Object.assign({}, base, extend)
|
||||||
|
|
||||||
|
this.buildTheme = (themeTpl) => {
|
||||||
|
const mapping = {}
|
||||||
|
const base = merge(themeTpl.BASE, {
|
||||||
|
'font-family': this.opts.fonts,
|
||||||
|
'font-size': this.opts.size,
|
||||||
|
})
|
||||||
|
for (const ele in themeTpl.inline) {
|
||||||
|
if (themeTpl.inline.hasOwnProperty(ele)) {
|
||||||
|
const style = themeTpl.inline[ele]
|
||||||
|
mapping[ele] = merge(themeTpl.BASE, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const base_block = merge(base, {})
|
||||||
|
for (const ele in themeTpl.block) {
|
||||||
|
if (themeTpl.block.hasOwnProperty(ele)) {
|
||||||
|
const style = themeTpl.block[ele]
|
||||||
|
mapping[ele] = merge(base_block, style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return mapping
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (tokenName, addition) => {
|
||||||
|
const arr = []
|
||||||
|
const dict = styleMapping[tokenName]
|
||||||
|
if (!dict)
|
||||||
|
return ``
|
||||||
|
for (const key in dict) {
|
||||||
|
arr.push(`${key}:${dict[key]}`)
|
||||||
|
}
|
||||||
|
return `style="${arr.join(`;`) + (addition || ``)}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const addFootnote = (title, link) => {
|
||||||
|
footnotes.push([++footnoteIndex, title, link])
|
||||||
|
return footnoteIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buildFootnotes = () => {
|
||||||
|
const 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)
|
||||||
|
const 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.includes(`<figure`) && text.includes(`<img`)) {
|
||||||
|
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 = ``) => {
|
||||||
|
if (lang.startsWith(`mermaid`)) {
|
||||||
|
setTimeout(() => {
|
||||||
|
window.mermaid?.run()
|
||||||
|
}, 0)
|
||||||
|
return `<center><pre class="mermaid">${text}</pre></center>`
|
||||||
|
}
|
||||||
|
lang = lang.split(` `)[0]
|
||||||
|
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, (str) => {
|
||||||
|
return str.replace(/\s/g, ` `)
|
||||||
|
})
|
||||||
|
|
||||||
|
return `<pre class="hljs code__pre" ${getStyles(
|
||||||
|
`code_pre`,
|
||||||
|
)}><code class="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, ``)
|
||||||
|
const 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) => {
|
||||||
|
const createSubText = (s) => {
|
||||||
|
if (!s) {
|
||||||
|
return ``
|
||||||
|
}
|
||||||
|
|
||||||
|
return `<figcaption ${getStyles(`figcaption`)}>${s}</figcaption>`
|
||||||
|
}
|
||||||
|
const transform = (title, alt) => {
|
||||||
|
const legend = localStorage.getItem(`legend`)
|
||||||
|
switch (legend) {
|
||||||
|
case `alt`:
|
||||||
|
return alt
|
||||||
|
case `title`:
|
||||||
|
return title
|
||||||
|
case `alt-title`:
|
||||||
|
return alt || title
|
||||||
|
case `title-alt`:
|
||||||
|
return title || alt
|
||||||
|
default:
|
||||||
|
return ``
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const subText = createSubText(transform(title, text))
|
||||||
|
const figureStyles = getStyles(`figure`)
|
||||||
|
const 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) {
|
||||||
|
const 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
|
File diff suppressed because it is too large
Load Diff
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
89
tailwind.config.js
Normal file
89
tailwind.config.js
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
const animate = require("tailwindcss-animate")
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
safelist: ["dark"],
|
||||||
|
prefix: "",
|
||||||
|
experimental: {
|
||||||
|
optimizeUniversalDefaults: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
content: [
|
||||||
|
'./pages/**/*.{ts,tsx,vue}',
|
||||||
|
'./components/**/*.{ts,tsx,vue}',
|
||||||
|
'./app/**/*.{ts,tsx,vue}',
|
||||||
|
'./src/**/*.{ts,tsx,vue}',
|
||||||
|
],
|
||||||
|
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
DEFAULT: "hsl(var(--secondary))",
|
||||||
|
foreground: "hsl(var(--secondary-foreground))",
|
||||||
|
},
|
||||||
|
destructive: {
|
||||||
|
DEFAULT: "hsl(var(--destructive))",
|
||||||
|
foreground: "hsl(var(--destructive-foreground))",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
DEFAULT: "hsl(var(--accent))",
|
||||||
|
foreground: "hsl(var(--accent-foreground))",
|
||||||
|
},
|
||||||
|
popover: {
|
||||||
|
DEFAULT: "hsl(var(--popover))",
|
||||||
|
foreground: "hsl(var(--popover-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
xl: "calc(var(--radius) + 4px)",
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
keyframes: {
|
||||||
|
"accordion-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
},
|
||||||
|
"accordion-up": {
|
||||||
|
from: { height: "var(--radix-accordion-content-height)" },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
"collapsible-down": {
|
||||||
|
from: { height: 0 },
|
||||||
|
to: { height: 'var(--radix-collapsible-content-height)' },
|
||||||
|
},
|
||||||
|
"collapsible-up": {
|
||||||
|
from: { height: 'var(--radix-collapsible-content-height)' },
|
||||||
|
to: { height: 0 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: {
|
||||||
|
"accordion-down": "accordion-down 0.2s ease-out",
|
||||||
|
"accordion-up": "accordion-up 0.2s ease-out",
|
||||||
|
"collapsible-down": "collapsible-down 0.2s ease-in-out",
|
||||||
|
"collapsible-up": "collapsible-up 0.2s ease-in-out",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [animate],
|
||||||
|
}
|
31
tsconfig.app.json
Normal file
31
tsconfig.app.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"baseUrl": "./",
|
||||||
|
"module": "ESNext",
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
11
tsconfig.json
Normal file
11
tsconfig.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"references": [
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.app.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "./tsconfig.node.json"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"files": []
|
||||||
|
}
|
13
tsconfig.node.json
Normal file
13
tsconfig.node.json
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
9
uno.config.ts
Normal file
9
uno.config.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { defineConfig, presetMini } from 'unocss'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
presets: [
|
||||||
|
presetMini({
|
||||||
|
preflight: false,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
})
|
46
vite.config.ts
Normal file
46
vite.config.ts
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import path from 'node:path'
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import UnoCSS from 'unocss/vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
import { nodePolyfills } from 'vite-plugin-node-polyfills'
|
||||||
|
import tailwind from 'tailwindcss'
|
||||||
|
import autoprefixer from 'autoprefixer'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig(({ mode }) => {
|
||||||
|
const isProd = mode === `production`
|
||||||
|
|
||||||
|
return {
|
||||||
|
base: process.env.SERVER_ENV === `NETLIFY` ? `/` : `/md/`, // 基本路径, 建议以绝对路径跟随访问目录
|
||||||
|
define: {
|
||||||
|
process,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
UnoCSS(),
|
||||||
|
nodePolyfills({
|
||||||
|
include: [`path`, `util`, `timers`, `stream`, `fs`],
|
||||||
|
overrides: {
|
||||||
|
// Since `fs` is not supported in browsers, we can use the `memfs` package to polyfill it.
|
||||||
|
// fs: 'memfs',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, `./src`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
css: {
|
||||||
|
devSourcemap: !isProd,
|
||||||
|
postcss: {
|
||||||
|
plugins: [tailwind(), autoprefixer()],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
sourcemap: !isProd,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
@ -1,35 +0,0 @@
|
|||||||
const isProd = process.env.NODE_ENV === `production`
|
|
||||||
|
|
||||||
const crypto = require(`crypto`)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* md4 algorithm is not available anymore in NodeJS 17+ (because of lib SSL 3).
|
|
||||||
* In that case, silently replace md4 by md5 algorithm.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
crypto.createHash(`md4`)
|
|
||||||
} catch (e) {
|
|
||||||
const origCreateHash = crypto.createHash
|
|
||||||
crypto.createHash = (alg, opts) => {
|
|
||||||
return origCreateHash(alg === `md4` ? `md5` : alg, opts)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
lintOnSave: true,
|
|
||||||
publicPath: process.env.SERVER_ENV === `NETLIFY` ? `/` : `/md/`, // 基本路径, 建议以绝对路径跟随访问目录
|
|
||||||
configureWebpack: (config) => {
|
|
||||||
config.module.rules.push({
|
|
||||||
test: /\.(txt|md)$/i,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: `raw-loader`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
},
|
|
||||||
productionSourceMap: !isProd,
|
|
||||||
css: {
|
|
||||||
sourceMap: !isProd,
|
|
||||||
},
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user