mirror of
https://github.com/woodchen-ink/md-wechat.git
synced 2025-07-18 05:32:02 +08:00
Merge branch 'main' of https://github.com/doocs/md into doocs-main
This commit is contained in:
commit
06c5fab003
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`,
|
||||
},
|
||||
}
|
2
.github/secret_scanning.yml
vendored
Normal file
2
.github/secret_scanning.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
paths-ignore:
|
||||
- "src/**"
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -23,7 +23,6 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
dist
|
||||
lib
|
||||
|
||||
node_modules
|
||||
|
||||
@ -46,3 +45,10 @@ httpData
|
||||
public/upload/**
|
||||
!public/upload/*.gitkeep
|
||||
.history
|
||||
|
||||
# Package manager lock file
|
||||
# package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
auto-imports.d.ts
|
||||
components.d.ts
|
||||
|
3
.vscode/extensions.json
vendored
Normal file
3
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
44
README.md
44
README.md
@ -8,22 +8,20 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](https://github.com/doocs/md/actions) [](https://github.com/doocs/md/actions) [](https://github.com/doocs/md/actions) [](#谁在使用) [](../../pulls)<br> [](https://github.com/doocs/md) [](https://gitee.com/doocs/md) [](./LICENSE) [](../../releases)
|
||||
[](https://github.com/doocs/md/actions) [](https://nodejs.org/en/about/previous-releases) [](https://github.com/doocs/md/releases) [](./LICENSE) [](https://github.com/doocs/md/pulls)<br>[](https://github.com/doocs/md/stargazers) [](https://github.com/doocs/md)
|
||||
|
||||
</div>
|
||||
|
||||
## 项目介绍
|
||||
|
||||
> 本项目基于 [wechat-format](https://github.com/lyricat/wechat-format) 进行二次开发,感谢 [lyricat](https://github.com/lyricat) 的创意和贡献!
|
||||
|
||||
Markdown 文档自动即时渲染为微信图文,让你不再为微信文章排版而发愁!只要你会基本的 Markdown 语法,就能做出一篇样式简洁而又美观大方的微信图文。
|
||||
|
||||
## 在线编辑器地址
|
||||
|
||||
- Gitee Pages:https://doocs.gitee.io/md
|
||||
- GitHub Pages:https://doocs.github.io/md
|
||||
- [https://doocs.github.io/md](https://doocs.github.io/md)
|
||||
- [https://doocs-md.pages.dev](https://doocs-md.pages.dev)
|
||||
|
||||
注:推荐使用 Chrome 浏览器,效果最佳。另外,对于国内(中国)的朋友,访问 [Gitee Pages](https://doocs.gitee.io/md) 速度会相对快一些。
|
||||
注:推荐使用 Chrome 浏览器,效果最佳。
|
||||
|
||||
## 为何二次开发
|
||||
|
||||
@ -31,12 +29,14 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章
|
||||
|
||||
欢迎各位朋友随时提交 PR,让这款微信 Markdown 编辑器变得更好!如果你有新的想法,也欢迎在 [Discussions 讨论区](https://github.com/doocs/md/discussions)反馈。
|
||||
|
||||
注:我们项目最新版本基于 Vue3 开发,基于 Vue2 的旧版本已经不再维护,如果你需要 Vue2 版本,请切换到 [1.x](https://github.com/doocs/md/tree/1.x) 分支。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- [x] 支持自定义 CSS 样式
|
||||
- [x] 支持 Markdown 所有基础语法
|
||||
- [x] 支持浅色、暗黑两种主题模式
|
||||
- [x] 支持 <kbd>Ctrl</kbd> + <kbd>F</kbd> 快速格式化文档
|
||||
- [x] 支持 Markdown 所有基础语法、代码块、LaTeX 公式
|
||||
- [x] 支持浅色、深色两种编辑器外观
|
||||
- [x] 支持 <kbd>Alt</kbd> + <kbd>Shift</kbd> + <kbd>F</kbd> 快速格式化文档
|
||||
- [x] 支持色盘取色,快速替换文章整体色调
|
||||
- [x] 支持多图上传,可自定义配置图床
|
||||
- [x] 支持自定义上传逻辑
|
||||
@ -55,13 +55,13 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章
|
||||
| 6 | [MinIO](https://min.io/) | 配置 `Endpoint`、`Port`、`UseSSL`、`Bucket`、`AccessKey`、`SecretKey` 参数 | [如何使用 MinIO?](http://docs.minio.org.cn/docs/master/minio-client-complete-guide) |
|
||||
| 7 | 自定义上传 | 是 | [如何自定义上传?](#自定义上传逻辑) |
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
## 注意事项
|
||||
|
||||
@ -77,19 +77,19 @@ Markdown 文档自动即时渲染为微信图文,让你不再为微信文章
|
||||
示例代码:
|
||||
|
||||
```js
|
||||
const { file, util, okCb, errCb } = CUSTOM_ARG;
|
||||
const param = new FormData();
|
||||
param.append("file", file);
|
||||
const { file, util, okCb, errCb } = CUSTOM_ARG
|
||||
const param = new FormData()
|
||||
param.append(`file`, file)
|
||||
util.axios
|
||||
.post("http://127.0.0.1:9000/upload", param, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
.post(`http://127.0.0.1:9000/upload`, param, {
|
||||
headers: { 'Content-Type': `multipart/form-data` },
|
||||
})
|
||||
.then((res) => {
|
||||
okCb(res.url);
|
||||
okCb(res.url)
|
||||
})
|
||||
.catch((err) => {
|
||||
errCb(err);
|
||||
});
|
||||
errCb(err)
|
||||
})
|
||||
|
||||
// 提供的可用参数:
|
||||
// CUSTOM_ARG = {
|
||||
@ -98,7 +98,7 @@ util.axios
|
||||
// util: {
|
||||
// axios, // axios 实例
|
||||
// CryptoJS, // 加密库
|
||||
// OSS, // ali-oss
|
||||
// OSS, // tiny-oss
|
||||
// COS, // cos-js-sdk-v5
|
||||
// Buffer, // buffer-from
|
||||
// uuidv4, // uuid
|
||||
|
@ -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`,
|
||||
},
|
||||
})
|
99
index.html
Normal file
99
index.html
Normal file
@ -0,0 +1,99 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="keywords" content="md,markdown,markdown-editor,wechat,official-account,yanglbme,doocs" />
|
||||
<meta name="description" content="Wechat Markdown Editor | 一款高度简洁的微信 Markdown 编辑器" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<title>微信 Markdown 编辑器 | Doocs 开源社区</title>
|
||||
<link rel="shortcut icon" href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/favicon.png" />
|
||||
<link
|
||||
rel="apple-touch-icon-precomposed"
|
||||
href="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png"
|
||||
/>
|
||||
<style>
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.loading::before {
|
||||
content: url('/src/assets/images/favicon.png');
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.loading.dark {
|
||||
color: #ffffff;
|
||||
background-color: #141414;
|
||||
}
|
||||
|
||||
.loading .txt {
|
||||
position: absolute;
|
||||
bottom: 10%;
|
||||
}
|
||||
|
||||
.loading .txt::after {
|
||||
content: '...';
|
||||
animation: dots 1.5s steps(4, end) infinite;
|
||||
}
|
||||
|
||||
@keyframes dots {
|
||||
0% {
|
||||
content: ' ';
|
||||
}
|
||||
25% {
|
||||
content: '.';
|
||||
}
|
||||
50% {
|
||||
content: '..';
|
||||
}
|
||||
75% {
|
||||
content: '...';
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>Please enable JavaScript to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div class="loading">
|
||||
<strong>致力于让 Markdown 编辑更简单</strong>
|
||||
<p class="txt">正在加载编辑器</p>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const theme = localStorage.getItem('vueuse-color-scheme')
|
||||
if (theme === 'dark' || (theme === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
document.querySelector('.loading').classList.add('dark')
|
||||
}
|
||||
</script>
|
||||
<script>
|
||||
MathJax = {
|
||||
loader: { load: ['[tex]/ams'] },
|
||||
tex: { packages: { '[+]': ['ams'] }, tags: 'ams' },
|
||||
svg: { fontCache: 'none' },
|
||||
}
|
||||
</script>
|
||||
<script id="MathJax-script" src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js"></script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
<script src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/wechatsync/article-syncjs@latest/dist/main.js"></script>
|
||||
</html>
|
@ -1,9 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
25515
package-lock.json
generated
25515
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
114
package.json
114
package.json
@ -1,62 +1,88 @@
|
||||
{
|
||||
"name": "md",
|
||||
"type": "module",
|
||||
"version": "1.6.0",
|
||||
"private": false,
|
||||
"scripts": {
|
||||
"start": "npm run dev",
|
||||
"dev": "vite --host",
|
||||
"build": "run-p type-check \"build:only {@}\" --",
|
||||
"build:only": "vite build",
|
||||
"build:h5-netlify": "run-p type-check \"build:h5-netlify:only {@}\" --",
|
||||
"build:h5-netlify:only": "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",
|
||||
"build:analyze": "cross-env ANALYZE=true vite build",
|
||||
"preview": "npm run build && vite preview",
|
||||
"release:cli": "node ./bin/release.js",
|
||||
"lint": "vue-cli-service lint src",
|
||||
"start": "npm run lint -- --fix && run-p serve",
|
||||
"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"
|
||||
"lint": "eslint . --fix",
|
||||
"type-check": "vue-tsc --build --force",
|
||||
"postinstall": "simple-git-hooks"
|
||||
},
|
||||
"dependencies": {
|
||||
"ali-oss": "^6.17.1",
|
||||
"axios": "^1.6.0",
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@vueuse/core": "^11.1.0",
|
||||
"axios": "^1.7.7",
|
||||
"buffer-from": "^1.1.2",
|
||||
"codemirror": "^5.65.7",
|
||||
"core-js": "^3.34.0",
|
||||
"cos-js-sdk-v5": "^1.3.9",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"codemirror": "^5.65.17",
|
||||
"core-js": "^3.38.1",
|
||||
"cos-js-sdk-v5": "^1.8.4",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-ui": "^2.15.9",
|
||||
"csstype": "^3.1.3",
|
||||
"element-plus": "^2.8.3",
|
||||
"es-toolkit": "^1.20.0",
|
||||
"form-data": "4.0.0",
|
||||
"highlight.js": "^11.6.0",
|
||||
"juice": "^8.0.0",
|
||||
"katex": "^0.16.9",
|
||||
"marked": "^4.0.18",
|
||||
"marked-katex-extension": "^4.0.3",
|
||||
"minio": "7.0.33",
|
||||
"node-fetch": "^3.2.10",
|
||||
"pinia": "^2.1.6",
|
||||
"prettify": "^0.1.7",
|
||||
"qiniu-js": "^3.4.1",
|
||||
"uuid": "^8.3.2",
|
||||
"vue": "^2.7.14"
|
||||
"highlight.js": "^11.10.0",
|
||||
"juice": "^11.0.0",
|
||||
"lucide-vue-next": "^0.445.0",
|
||||
"marked": "^14.1.2",
|
||||
"mermaid": "^11.2.1",
|
||||
"minio": "7.1.3",
|
||||
"node-fetch": "^3.3.2",
|
||||
"pinia": "^2.2.2",
|
||||
"qiniu-js": "^3.4.2",
|
||||
"radix-vue": "^1.9.6",
|
||||
"tailwind-merge": "^2.5.2",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tiny-oss": "^0.5.1",
|
||||
"uuid": "^10.0.0",
|
||||
"vue": "^3.5.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.5.19",
|
||||
"@vue/cli-plugin-eslint": "^4.5.19",
|
||||
"@vue/cli-service": "^4.5.15",
|
||||
"@vue/eslint-config-prettier": "^6.0.0",
|
||||
"async-validator": "^4.0.7",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-plugin-import": "^1.13.3",
|
||||
"cache-loader": "^4.1.0",
|
||||
"@antfu/eslint-config": "3.7.1",
|
||||
"@types/buffer-from": "^1.1.3",
|
||||
"@types/codemirror": "^5.60.15",
|
||||
"@types/crypto-js": "^4.2.2",
|
||||
"@types/node": "^22.7.0",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@unocss/eslint-plugin": "^0.62.4",
|
||||
"@vitejs/plugin-vue": "^5.1.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-prettier": "^3.4.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"husky": "^8.0.3",
|
||||
"less": "^4.1.2",
|
||||
"less-loader": "^7.3.0",
|
||||
"mini-types": "*",
|
||||
"miniprogram-api-typings": "*",
|
||||
"eslint": "^9.11.1",
|
||||
"eslint-plugin-format": "^0.1.2",
|
||||
"less": "^4.2.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"postcss-comment": "^2.0.0",
|
||||
"prettier": "^2.8.8",
|
||||
"raw-loader": "^4.0.2",
|
||||
"postcss": "^8.4.47",
|
||||
"prettier": "^3.3.3",
|
||||
"rollup-plugin-visualizer": "^5.12.0",
|
||||
"shx": "^0.3.4",
|
||||
"vue-template-compiler": "^2.7.14"
|
||||
"simple-git-hooks": "^2.11.1",
|
||||
"tailwindcss": "^3.4.13",
|
||||
"typescript": "^5.6.2",
|
||||
"unocss": "^0.62.4",
|
||||
"unplugin-auto-import": "^0.18.3",
|
||||
"unplugin-vue-components": "^0.27.4",
|
||||
"vite": "^5.4.7",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"vite-plugin-vue-devtools": "^7.4.6",
|
||||
"vue-tsc": "^2.1.6"
|
||||
},
|
||||
"simple-git-hooks": {
|
||||
"pre-commit": "npx lint-staged"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*": "npm run lint"
|
||||
}
|
||||
}
|
||||
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"
|
||||
/>
|
||||
<title>微信 Markdown 编辑器</title>
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
href="./assets/favicon.png"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="./assets/css/css.css"
|
||||
/>
|
||||
<!-- KaTeX CSS -->
|
||||
<link rel="stylesheet" href="https://i-aws.czl.net/jsdelivr/npm/katex@0.16.9/dist/katex.min.css" >
|
||||
|
||||
<script src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/prettify/r298/prettify.min.js"></script>
|
||||
<style>
|
||||
/**
|
||||
解决公众号复制字体问题
|
||||
*/
|
||||
.katex .mathnormal {
|
||||
font-family: "Times New Roman" !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>Please enable JavaScript to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
<script src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/wechatsync/article-syncjs@latest/dist/main.js"></script>
|
||||
</html>
|
||||
|
36
src/App.vue
36
src/App.vue
@ -1,24 +1,26 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<codemirror-editor />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
<script setup lang="ts">
|
||||
import CodemirrorEditor from '@/views/CodemirrorEditor.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CodemirrorEditor />
|
||||
</template>
|
||||
|
||||
<style lang="less">
|
||||
// 仿 uniapp 外层全屏
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// 抵消下拉菜单开启时带来的样式
|
||||
body {
|
||||
pointer-events: initial !important;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
@ -50,7 +52,9 @@ body,
|
||||
|
||||
color: #333333;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
box-shadow:
|
||||
0 4px 8px 0 rgba(0, 0, 0, 0.12),
|
||||
0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.CodeMirror-hint {
|
||||
@ -68,4 +72,14 @@ body,
|
||||
background: #f0f0f0;
|
||||
}
|
||||
}
|
||||
|
||||
// 修复分栏线负数 margin 导致的轴向滚动条
|
||||
.el-dropdown-menu__item--divided:before {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
// 修复颜色选择器下拉箭头位置
|
||||
.el-icon.el-color-picker__icon.is-icon-arrow-down {
|
||||
position: absolute;
|
||||
}
|
||||
</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 }
|
@ -1,97 +1,171 @@
|
||||
# 示例文章:Google 搜索的即时自动补全功能究竟是如何“工作”的?
|
||||
# 探索 Markdown 的奇妙世界
|
||||
|
||||
> Google 搜索**自动补全功能**的强大,相信不少朋友都能感受到,它帮助我们更快地“补全”我们所要输入的搜索关键字。那么,它怎么知道我们要输入什么内容?它又是如何工作的?在这篇文章里,我们一起来看看。
|
||||
欢迎来到 Markdown 的奇妙世界!无论你是写作爱好者、开发者、博主,还是想要简单记录点什么的人,Markdown 都能成为你新的好伙伴。它不仅让写作变得简单明了,还能轻松地将内容转化为漂亮的网页格式。今天,我们将全面探讨 Markdown 的基础和进阶语法,让你在这个过程中充分享受写作的乐趣!
|
||||
|
||||
## 使用自动补全
|
||||
Markdown 是一种轻量级标记语言,用于格式化纯文本。它以简单、直观的语法而著称,可以快速地生成 HTML。Markdown 是写作与代码的完美结合,既简单又强大。
|
||||
|
||||
Google 搜索的自动补全功能可以在 Google 搜索应用的大多数位置使用,包括 [Google](https://www.google.com/) 主页、适用于 IOS 和 Android 的 Google 应用,我们只需要在 Google 搜索框上开始键入关键字,就可以看到联想词了。
|
||||
## Markdown 基础语法
|
||||
|
||||

|
||||
### 1. 标题:让你的内容层次分明
|
||||
|
||||
在上图示例中,我们可以看到,输入关键字 `juej`,Google 搜索会联想到“掘金”、“掘金小册”、“绝句”等等,好处就是,我们无须输入完整的关键字即可轻松完成针对这些 topics 的搜索。
|
||||
用 `#` 号来创建标题。标题从 `#` 开始,`#` 的数量表示标题的级别。
|
||||
|
||||
谷歌搜索的自动补全功能对于使用移动设备的用户来说特别有用,用户可以轻松在难以键入的小屏幕上完成搜索。当然,对于移动设备用户和台式机用户而言,这都节省了大量的时间。根据 Google 官方报告,自动补全功能可以减少大约 25% 的打字,累积起来,预计每天可以节省 200 多年的打字时间。是的,每天!
|
||||
```markdown
|
||||
# 一级标题
|
||||
|
||||
> 注意,本文所提到的“**联想词**”与“**预测**”,是同一个意思。
|
||||
## 二级标题
|
||||
|
||||
## 基于“预测”而非“建议”
|
||||
### 三级标题
|
||||
|
||||
Google 官方将自动补全功能称之为“预测”,而不是“建议”,为什么呢?其实是有充分理由的。自动补全功能是为了**帮助用户完成他们打算进行的搜索**,而不是建议用户要执行什么搜索。
|
||||
#### 四级标题
|
||||
```
|
||||
|
||||
那么,Google 是如何确定这些“预测”的?其实,Google 会根据趋势搜索 [trends](https://trends.google.com/trends/?geo=US) 给到我们这些“预测”。简单来说,哪个热门、哪个搜索频率高,就更可能推给我们。当然,这也与我们当前所处的位置以及我们的搜索历史相关。
|
||||
以上代码将渲染出一组层次分明的标题,使你的文章井井有条。
|
||||
|
||||
另外,这些“预测”也会随着我们键入的关键字的变更而更改。例如,当我们把键入的关键字从 `juej` 更改为 `juex` 时,与“掘金”相关的预测会“消失”,同时,与“觉醒”、“决心”相关联的词会出现。
|
||||
### 2. 段落与换行:自然流畅
|
||||
|
||||

|
||||
Markdown 中的段落就是一行接一行的文本。要创建新段落,只需在两行文本之间空一行。
|
||||
|
||||
## 为什么看不到某些联想词?
|
||||
### 3. 字体样式:强调你的文字
|
||||
|
||||
如果我们在输入某个关键字时看不到联想词,那么表明 Google 的算法可能检测到:
|
||||
- **粗体**:用两个星号或下划线包裹文字,如 `**粗体**` 或 `__粗体__`。
|
||||
- _斜体_:用一个星号或下划线包裹文字,如 `*斜体*` 或 `_斜体_`。
|
||||
- ~~删除线~~:用两个波浪线包裹文字,如 `~~删除线~~`。
|
||||
|
||||
- 这个关键字不是热门字词;
|
||||
- 搜索的字词太新了,我们可能需要等待几天或几周才能看到联想词;
|
||||
- 这是一个侮辱性或敏感字词,这个搜索字词违反了 Google 的相关政策。更加详细的情况,可以了解 [Google 搜索自动补全政策](https://support.google.com/websearch/answer/7368877)。
|
||||
这些简单的标记可以让你的内容更有层次感和重点突出。
|
||||
|
||||
## 为什么会看到某些不当的联想词?
|
||||
### 4. 列表:整洁有序
|
||||
|
||||
Google 拥有专门设计的系统,可以自动捕获不适当的预测结果而不显示出来。然而,Google 每天需要处理数十亿次搜索,这意味着 Google 每天会显示数十亿甚至上百亿条预测。再好的系统,也可能存在缺陷,不正确的预测也可能随时会出现。
|
||||
- **无序列表**:用 `-`、`*` 或 `+` 加空格开始一行。
|
||||
- **有序列表**:使用数字加点号(`1.`、`2.`)开始一行。
|
||||
|
||||
我们作为 Google 搜索的用户,如果认定某条预测违反了相关的搜索自动补全政策,可以进行举报反馈,点击右下角“**举报不当的联想查询**”并勾选相关选项即可。
|
||||
在列表中嵌套其他内容?只需缩进即可实现嵌套效果。
|
||||
|
||||

|
||||
- 无序列表项 1
|
||||
1. 嵌套有序列表项 1
|
||||
2. 嵌套有序列表项 2
|
||||
- 无序列表项 2
|
||||
|
||||
## 如何实现自动补全算法?
|
||||
1. 有序列表项 1
|
||||
2. 有序列表项 2
|
||||
|
||||
目前,Google 官方似乎并没有公开搜索自动补全的算法实现,但是业界在这方面已经有了不少研究。
|
||||
### 5. 链接与图片:丰富内容
|
||||
|
||||
一个好的自动补全器必须是快速的,并且在用户键入下一个字符后立即更新联想词列表。**自动补全器的核心是一个函数,它接受输入的前缀,并搜索以给定前缀开头的词汇或语句列表**。通常来说,只需要返回少量的数目即可。
|
||||
- **链接**:用方括号和圆括号创建链接 `[显示文本](链接地址)`。
|
||||
- **图片**:和链接类似,只需在前面加上 `!`,如 ``。
|
||||
|
||||
接下来,我们先从一个简单且低效的实现开始,并在此基础上逐步构建更高效的方法。
|
||||
[访问 Doocs](https://github.com/doocs)
|
||||
|
||||
### 词汇表实现
|
||||

|
||||
|
||||
一个**简单粗暴的实现方式**是:顺序查找词汇表,依次检查每个词汇,看它是否以给定的前缀开头。
|
||||
轻松实现富媒体内容展示!
|
||||
|
||||
但是,此方法需要将前缀与每个词汇进行匹配检查,若词汇量较少,这种方式可能勉强行得通。但是,如果词汇量规模较大,效率就太低了。
|
||||
> 因微信公众号平台不支持除公众号文章以外的链接,故其他平台的链接,会呈现链接色泽但不能点击。
|
||||
|
||||
一个**更好的实现方式是**:让词汇按字典顺序排序。借助二分搜索算法,可以快速搜索有序词汇表中的前缀。由于二分搜索的每一步都会将搜索的范围减半,因此,总的搜索时间与词汇表中单词数量的对数成正比,即时间复杂度是 `O(log N)`。二分搜索的性能很好,但有没有更好的实现呢?当然有,往下看。
|
||||
> 对于这些链接请注意明文书写,或点击左上角「格式->微信外链接转底部引用」开启引用,这样就可以在底部观察到链接指向。
|
||||
|
||||
### 前缀树实现
|
||||
### 6. 引用:引用名言或引人深思的句子
|
||||
|
||||
通常来说,许多词汇都以相同的前缀开头,比如 `need`、`nested` 都以 `ne` 开头,`seed`、`speed` 都以 `s` 开头。要是为每个单词分别存储公共前缀似乎很浪费。
|
||||
使用 `>` 来创建引用,只需在文本前面加上它。多层引用?在前一层 `>` 后再加一个就行。
|
||||
|
||||
> 这是一个引用
|
||||
>
|
||||
> > 这是一个嵌套引用
|
||||
|
||||

|
||||
这让你的引用更加富有层次感。
|
||||
|
||||
### 7. 代码块:展示你的代码
|
||||
|
||||
前缀树是一种利用公共前缀来加速补全速度的数据结构。前缀树在节点树中排列一组单词,单词沿着从根节点到叶子节点的路径存储,树的层次对应于前缀的字母位置。
|
||||
- **行内代码**:用反引号包裹,如 `code`。
|
||||
- **代码块**:用三个反引号包裹,并指定语言,如:
|
||||
|
||||
前缀的补全是顺着前缀定义的路径来查找的。例如,在上图的前缀树中,前缀 `ne` 对应于从子节点取左边缘 `N` 和唯一边缘 `E` 的路径。然后可以通过继续遍历从 `E` 节点可以达到的所有叶节点来生成补全列表。在图中,`ne` 的补全可以是两个分支:`-ed` 和 `-sted`。如果在数中找不到由前缀定义的路径,则说明词汇表中不包含以该前缀开头的单词。
|
||||
```js
|
||||
console.log("Hello, Doocs!");
|
||||
```
|
||||
|
||||
### 有限状态自动机(DFA)实现
|
||||
语法高亮让你的代码更易读。
|
||||
|
||||
前缀树可以有效处理公共前缀,但是,对于其他共享词部分,仍会分别存储在每个分支中。比如,后缀 `ed`、`ing`、`tion` 在英文单词中特别常见。在上一个例子中,`e`、`d` 分别存放在了每一个分支上。
|
||||
### 8. 分割线:分割内容
|
||||
|
||||
有没有一种方法可以更加节省存储空间呢?有的,那就是 DFA。
|
||||
用三个或更多的 `-`、`*` 或 `_` 来创建分割线。
|
||||
|
||||
<center>
|
||||
<img src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303158478-66a96e2d-6424-43d6-8cb3-2f7a39f960b8.gif" style="width: 50%;"></center>
|
||||
---
|
||||
|
||||
在上面的例子中,单词 `need`、`nested`、`seed` 和 `speed` 仅由 9 个节点组成,而上一张图中的前缀树包含了 17 个节点。
|
||||
为你的内容添加视觉分隔。
|
||||
|
||||
可以看出,最小化前缀树 DFA 可以在很大程度上减少数据结构的大小。即使词汇量很大,最小化 DFA 通常也适合在内存中存储,避免昂贵的磁盘访问是实现快速自动补全的关键。
|
||||
### 9. 表格:清晰展示数据
|
||||
|
||||
### 一些扩展
|
||||
Markdown 支持简单的表格,用 `|` 和 `-` 分隔单元格和表头。
|
||||
|
||||
上面介绍了如何利用合理的数据结构实现基本的自动补全功能。这些数据结构可以通过多种方式进行扩展,从而改善用户体验。
|
||||
| 项目人员 | 邮箱 | 微信号 |
|
||||
| ------------------------------------------- | ---------------------- | ------------ |
|
||||
| [yanglbme](https://github.com/yanglbme) | contact@yanglibin.info | YLB0109 |
|
||||
| [YangFong](https://github.com/YangFong) | yangfong2022@gmail.com | yq2419731931 |
|
||||
| [thinkasany](https://github.com/thinkasany) | thinkasany@gmail.com | thinkasany |
|
||||
|
||||
通常,满足特定前缀的词汇可能很多,而用户界面上能够显示的却不多,我们更希望能显示最常搜索或者最有价值的词汇。这通常可以通过为词汇表中的每个单词增加一个代表单词值的**权重** `weight`,并且按照权重高低来排序自动补全列表。
|
||||
这样的表格让数据展示更为清爽!
|
||||
|
||||
- 对于排序后的词汇表来说,在词汇表每个元素上增加 `weight` 属性并不难;
|
||||
- 对于前缀树来说,将 `weight` 存储在叶子节点中,也是很简单的一个实现;
|
||||
- 对于 `DFA` 来说,则较为复杂。因为一个叶子节点可以通过多条路径到达。一种解决方案是将权重关联到路径而不是叶子节点。
|
||||
> 手动编写标记太麻烦?我们提供了便捷方式。左上方点击「编辑->插入表格」,即可快速实现表格渲染。
|
||||
|
||||
目前有不少开源库都提供了这个功能,比如主流的搜索引擎框架 [Elasticsearch](https://www.elastic.co/products/elasticsearch)、[Solr](https://lucene.apache.org/solr/) 等,基于此,我们可以实现高效而强大的自动补全功能。
|
||||
## Markdown 进阶技巧
|
||||
|
||||
### 1. LaTeX 公式:完美展示数学表达式
|
||||
|
||||
Markdown 允许嵌入 LaTeX 语法展示数学公式:
|
||||
|
||||
- **行内公式**:用 `$` 包裹公式,如 $E = mc^2$。
|
||||
- **块级公式**:用 `$$` 包裹公式,如:
|
||||
|
||||
$$
|
||||
\begin{aligned}
|
||||
d_{i, j} &\leftarrow d_{i, j} + 1 \\
|
||||
d_{i, y + 1} &\leftarrow d_{i, y + 1} - 1 \\
|
||||
d_{x + 1, j} &\leftarrow d_{x + 1, j} - 1 \\
|
||||
d_{x + 1, y + 1} &\leftarrow d_{x + 1, y + 1} + 1
|
||||
\end{aligned}
|
||||
$$
|
||||
|
||||
这是展示复杂数学表达的利器!
|
||||
|
||||
### 2. Mermaid 流程图:可视化流程
|
||||
|
||||
Mermaid 是强大的可视化工具,可以在 Markdown 中创建流程图、时序图等。
|
||||
|
||||
```mermaid
|
||||
graph TD;
|
||||
A-->B;
|
||||
A-->C;
|
||||
B-->D;
|
||||
C-->D;
|
||||
```
|
||||
|
||||
```mermaid
|
||||
pie
|
||||
title Key elements in Product X
|
||||
"Calcium" : 42.96
|
||||
"Potassium" : 50.05
|
||||
"Magnesium" : 10.01
|
||||
"Iron" : 5
|
||||
```
|
||||
|
||||
```mermaid
|
||||
pie
|
||||
title 为什么总是宅在家里?
|
||||
"喜欢宅" : 45
|
||||
"天气太热" : 70
|
||||
"穷" : 500
|
||||
"没人约" : 95
|
||||
```
|
||||
|
||||
这种方式不仅能直观展示流程,还能提升文档的专业性。
|
||||
|
||||
> 更多用法,参见:[Mermaid User Guide](https://mermaid.js.org/intro/getting-started.html)。
|
||||
|
||||
## 结语
|
||||
|
||||
Markdown 是一种简单、强大且易于掌握的标记语言,通过学习基础和进阶语法,你可以快速创作内容并有效传达信息。无论是技术文档、个人博客还是项目说明,Markdown 都是你的得力助手。希望这篇文章能够带你全面了解 Markdown 的潜力,让你的写作更加丰富多彩!
|
||||
|
||||
现在,拿起 Markdown 编辑器,开始创作吧!探索 Markdown 的世界,你会发现它远比想象中更精彩!
|
||||
|
||||
#### 推荐阅读
|
||||
|
||||
@ -102,8 +176,6 @@ Google 拥有专门设计的系统,可以自动捕获不适当的预测结果
|
||||
|
||||
---
|
||||
|
||||
欢迎关注我的公众号“**Doocs**”,原创技术文章第一时间推送。
|
||||
|
||||
<center>
|
||||
<img src="https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/gh/doocs/md/images/1648303220922-7e14aefa-816e-44c1-8604-ade709ca1c69.png" style="width: 100px;">
|
||||
</center>
|
||||
|
@ -1,6 +1,11 @@
|
||||
/*
|
||||
按Ctrl/Command+F可格式化
|
||||
*/
|
||||
/**
|
||||
* 按 Alt/Option + Shift + F 可格式化
|
||||
* 如需使用主题色,请使用 var(--md-primary-color) 代替颜色值
|
||||
* 如:color: var(--md-primary-color);
|
||||
*
|
||||
* 召集令:如果你有好看的主题样式,欢迎分享,让更多人能够使用到你的主题。
|
||||
* 提交区:https://github.com/doocs/md/discussions/426
|
||||
*/
|
||||
/* 一级标题样式 */
|
||||
h1 {
|
||||
}
|
||||
@ -13,6 +18,12 @@ h3 {
|
||||
/* 四级标题样式 */
|
||||
h4 {
|
||||
}
|
||||
/* 五级标题样式 */
|
||||
h5 {
|
||||
}
|
||||
/* 六级标题样式 */
|
||||
h6 {
|
||||
}
|
||||
/* 图片样式 */
|
||||
image {
|
||||
}
|
||||
@ -52,3 +63,6 @@ li {
|
||||
/* 代码块样式 */
|
||||
code {
|
||||
}
|
||||
/* 代码块外层样式 */
|
||||
code_pre {
|
||||
}
|
||||
|
75
src/assets/index.css
Normal file
75
src/assets/index.css
Normal file
@ -0,0 +1,75 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border:0 0% 89.8%;
|
||||
--input:0 0% 89.8%;
|
||||
--ring:0 0% 3.9%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background:0 0% 3.9%;
|
||||
--foreground:0 0% 98%;
|
||||
|
||||
--card:0 0% 3.9%;
|
||||
--card-foreground:0 0% 98%;
|
||||
|
||||
--popover:0 0% 3.9%;
|
||||
--popover-foreground:0 0% 98%;
|
||||
|
||||
--primary:0 0% 98%;
|
||||
--primary-foreground:0 0% 9%;
|
||||
|
||||
--secondary:0 0% 14.9%;
|
||||
--secondary-foreground:0 0% 98%;
|
||||
|
||||
--muted:0 0% 14.9%;
|
||||
--muted-foreground:0 0% 63.9%;
|
||||
|
||||
--accent:0 0% 14.9%;
|
||||
--accent-foreground:0 0% 98%;
|
||||
|
||||
--destructive:0 62.8% 30.6%;
|
||||
--destructive-foreground:0 0% 98%;
|
||||
|
||||
--border:0 0% 14.9%;
|
||||
--input:0 0% 14.9%;
|
||||
--ring:0 0% 83.1%;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
@ -4,6 +4,13 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
textarea {
|
||||
@ -23,23 +30,15 @@ em {
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
section {
|
||||
height: 100%;
|
||||
font-family: "PingFang SC", BlinkMacSystemFont, Roboto, "Helvetica Neue",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
|
||||
.el-message__icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.web-title {
|
||||
margin: 0 15px 0 5px;
|
||||
}
|
||||
@ -51,21 +50,11 @@ body {
|
||||
}
|
||||
|
||||
#editor {
|
||||
height: 100%;
|
||||
display: block;
|
||||
border: none;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
section {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.main-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.ctrl {
|
||||
@ -77,20 +66,13 @@ section {
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: flex;
|
||||
word-break: break-all;
|
||||
padding: 0;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.main-section {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.hint {
|
||||
opacity: 0.6;
|
||||
margin: 20px 0;
|
||||
@ -104,7 +86,6 @@ section {
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
box-shadow: 0 0 60px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.preview table {
|
||||
@ -113,20 +94,3 @@ section {
|
||||
display: table;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
/* ele ui */
|
||||
.el-form-item {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.el-tooltip {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
uni-page-body,
|
||||
uni-page-refresh {
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
@ -1,130 +0,0 @@
|
||||
/*
|
||||
|
||||
Name: Base16 Default Light
|
||||
Author: Chris Kempson (http://chriskempson.com)
|
||||
|
||||
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
|
||||
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
|
||||
|
||||
*/
|
||||
|
||||
.cm-s-style-mirror.CodeMirror {
|
||||
color: #444;
|
||||
font-size: 16px;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-scroll {
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
overflow-x: hidden !important;
|
||||
overflow-y: scroll !important;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror div.CodeMirror-selected {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-line::selection,
|
||||
.cm-s-style-mirror .CodeMirror-line > span::selection,
|
||||
.cm-s-style-mirror .CodeMirror-line > span > span::selection {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-line::-moz-selection,
|
||||
.cm-s-style-mirror .CodeMirror-line > span::-moz-selection,
|
||||
.cm-s-style-mirror .CodeMirror-line > span > span::-moz-selection {
|
||||
background: #e0e0e0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-gutters {
|
||||
background: #f5f5f5;
|
||||
border-right: 0px;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-guttermarker {
|
||||
color: #ac4142;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-guttermarker-subtle {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-linenumber {
|
||||
color: #b0b0b0;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-cursor {
|
||||
border-left: 1px solid #505050;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-comment {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-atom {
|
||||
color: #aa759f;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-number {
|
||||
color: #aa759f;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-property,
|
||||
.cm-s-style-mirror span.cm-attribute {
|
||||
color: #90a959;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-keyword {
|
||||
color: #023a52;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-string {
|
||||
color: #e46918;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-variable {
|
||||
color: #90a959;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-variable-2 {
|
||||
color: #00695f;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-variable-3 {
|
||||
color: #2e6e8a;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-def {
|
||||
color: #d28445;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-bracket {
|
||||
color: #202020;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-tag {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-link {
|
||||
color: #b26a00;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror span.cm-error {
|
||||
/* background: #ac4142;
|
||||
color: #f5f5f5; */
|
||||
text-decoration: underline;
|
||||
text-decoration-style: wavy;
|
||||
text-decoration-color: #df8d8e;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-activeline-background {
|
||||
background: #dddcdc;
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-matchingbracket {
|
||||
color: rgb(32, 32, 32) !important;
|
||||
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
@ -1,142 +1,40 @@
|
||||
@nightBgColor: #333333;
|
||||
@nightPreviewColor: #1e1e1e;
|
||||
@nightHeaderColor: #3c3c3c;
|
||||
@nightCodeMirrorColor: #1e1e1e;
|
||||
@nightPreviewColor: #191919;
|
||||
@nightCodeMirrorColor: #191919;
|
||||
@nightActiveCodeMirrorColor: gray;
|
||||
@nightFontColor: gray;
|
||||
@nightLinkColor: #8e9eb9;
|
||||
@nightLinkTextColor: #84868b;
|
||||
@nightWhiteColor: #f0f0f0;
|
||||
@nightButtonBg: #1e1e1e;
|
||||
@nightButtonHoverColor: #84868b;
|
||||
@nightLineColor: #84868b;
|
||||
|
||||
.container_night {
|
||||
background-color: @nightBgColor;
|
||||
.dark {
|
||||
.container {
|
||||
.output_night {
|
||||
.preview {
|
||||
background-color: @nightPreviewColor;
|
||||
box-shadow: 0 0 70px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.el-main {
|
||||
background-color: @nightBgColor;
|
||||
}
|
||||
.preview-wrapper {
|
||||
background-color: @nightCodeMirrorColor;
|
||||
box-shadow: inset 0 0 0 1px rgba(233, 231, 231, 0.102);
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
caret-color: @nightFontColor;
|
||||
color: @nightFontColor;
|
||||
background-color: @nightCodeMirrorColor;
|
||||
box-shadow: inset 0 0 0 1px rgba(100, 37, 37, 0.102);
|
||||
}
|
||||
|
||||
.output_night {
|
||||
.preview {
|
||||
background-color: @nightPreviewColor;
|
||||
box-shadow: 0 0 70px rgba(0, 0, 0, 0.3);
|
||||
.code-snippet__fix {
|
||||
background-color: rgb(238, 238, 238);
|
||||
}
|
||||
}
|
||||
|
||||
.preview-wrapper {
|
||||
::-webkit-scrollbar {
|
||||
background-color: @nightCodeMirrorColor;
|
||||
box-shadow: inset 0 0 0 1px rgba(233, 231, 231, 0.102);
|
||||
}
|
||||
|
||||
.code-snippet__fix {
|
||||
background-color: rgb(238, 238, 238);
|
||||
}
|
||||
}
|
||||
|
||||
.cm-s-style-mirror .CodeMirror-matchingbracket {
|
||||
color: @nightWhiteColor !important;
|
||||
background: rgb(30, 30, 30) !important;
|
||||
}
|
||||
|
||||
.cm-s-xq-light span.cm-variable-2,
|
||||
.cm-s-style-mirror span.cm-tag {
|
||||
color: @nightFontColor;
|
||||
}
|
||||
|
||||
.cm-s-xq-light .CodeMirror-activeline-background {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.cm-s-xq-light span.cm-string {
|
||||
color: @nightLinkColor;
|
||||
}
|
||||
|
||||
.cm-s-xq-light span.cm-link {
|
||||
color: @nightLinkTextColor;
|
||||
}
|
||||
|
||||
.editor__header {
|
||||
background-color: @nightHeaderColor;
|
||||
}
|
||||
|
||||
.el-button {
|
||||
color: @nightWhiteColor;
|
||||
background-color: @nightCodeMirrorColor;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.el-button.is-plain:focus,
|
||||
.el-button.is-plain:hover {
|
||||
background: @nightButtonBg;
|
||||
color: @nightWhiteColor;
|
||||
border: 1px solid @nightWhiteColor;
|
||||
|
||||
i {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
}
|
||||
|
||||
.insert__dialog,
|
||||
.about__dialog,
|
||||
.reset__dialog,
|
||||
.upload__dialog {
|
||||
.el-dialog {
|
||||
background-color: @nightBgColor;
|
||||
}
|
||||
|
||||
.el-dialog__body {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
|
||||
.el-dialog__title,
|
||||
.el-form-item__label {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
color: @nightActiveCodeMirrorColor;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
background-color: @nightLineColor;
|
||||
}
|
||||
|
||||
.is-active {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
|
||||
.el-upload-dragger {
|
||||
background-color: @nightButtonBg;
|
||||
}
|
||||
}
|
||||
|
||||
::v-deep .el-icon-upload,
|
||||
.el-icon-download,
|
||||
.el-icon-refresh,
|
||||
.el-icon-s-grid,
|
||||
.el-icon-document {
|
||||
color: @nightWhiteColor;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
background-color: @nightCodeMirrorColor;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
padding-bottom: 0;
|
||||
height: 100% !important;
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.1);
|
||||
font-size: 14px;
|
||||
font-family: "PingFang SC", BlinkMacSystemFont, Roboto, "Helvetica Neue",
|
||||
font-family: 'PingFang SC', BlinkMacSystemFont, Roboto, 'Helvetica Neue',
|
||||
sans-serif !important;
|
||||
}
|
||||
|
||||
@ -160,3 +58,11 @@
|
||||
padding-bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.cm-em {
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.cm-comment {
|
||||
font-style: normal !important;
|
||||
}
|
||||
|
@ -1,90 +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: `Abc`,
|
||||
},
|
||||
{
|
||||
label: `衬线`,
|
||||
value: `Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif`,
|
||||
desc: `Abc`,
|
||||
},
|
||||
],
|
||||
sizeOption: [
|
||||
{
|
||||
label: `12px`,
|
||||
value: `12px`,
|
||||
desc: `更小`,
|
||||
},
|
||||
{
|
||||
label: `13px`,
|
||||
value: `13px`,
|
||||
desc: `稍小`,
|
||||
},
|
||||
{
|
||||
label: `14px`,
|
||||
value: `14px`,
|
||||
desc: `推荐`,
|
||||
},
|
||||
{
|
||||
label: `15px`,
|
||||
value: `15px`,
|
||||
desc: `稍大`,
|
||||
},
|
||||
{
|
||||
label: `16px`,
|
||||
value: `16px`,
|
||||
desc: `更大`,
|
||||
},
|
||||
],
|
||||
colorOption: [
|
||||
{
|
||||
label: `经典蓝`,
|
||||
value: `rgba(15, 76, 129, 1)`,
|
||||
desc: `最新流行`,
|
||||
},
|
||||
{
|
||||
label: `翡翠绿`,
|
||||
value: `rgba(0, 152, 116, 1)`,
|
||||
desc: `优雅清新`,
|
||||
},
|
||||
{
|
||||
label: `活力橘`,
|
||||
value: `rgba(250, 81, 81, 1)`,
|
||||
desc: `热情活泼`,
|
||||
},
|
||||
// { 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`,
|
||||
},
|
||||
],
|
||||
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,212 +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`
|
||||
}));
|
||||
|
||||
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 = hljs.getLanguage(lang) ? lang : "plaintext";
|
||||
|
||||
text = hljs.highlight(text, { language: lang }).value;
|
||||
|
||||
text = text
|
||||
.replace(/\r\n/g, "<br/>")
|
||||
.replace(/\n/g, "<br/>")
|
||||
.replace(/(>[^<]+)|(^[^<]+)/g, function (str) {
|
||||
return str.replace(/\s/g, " ");
|
||||
});
|
||||
|
||||
return `<pre class="hljs code__pre" ${getStyles(
|
||||
"code_pre"
|
||||
)}><code class="prettyprint language-${lang}" ${getStyles(
|
||||
"code"
|
||||
)}>${text}</code></pre>`;
|
||||
};
|
||||
renderer.codespan = (text, lang) =>
|
||||
`<code ${getStyles("codespan")}>${text}</code>`;
|
||||
renderer.listitem = (text) =>
|
||||
`<li ${getStyles("listitem")}><span><%s/></span>${text}</li>`;
|
||||
|
||||
renderer.list = (text, ordered, start) => {
|
||||
text = text.replace(/<\/*p .*?>/g, "").replace(/<\/*p>/g, "");
|
||||
let segments = text.split(`<%s/>`);
|
||||
if (!ordered) {
|
||||
text = segments.join("• ");
|
||||
return `<ul ${getStyles("ul")}>${text}</ul>`;
|
||||
}
|
||||
text = segments[0];
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
text = text + i + ". " + segments[i];
|
||||
}
|
||||
return `<ol ${getStyles("ol")}>${text}</ol>`;
|
||||
};
|
||||
renderer.image = (href, title, text) => {
|
||||
let subText = "";
|
||||
if (text) {
|
||||
subText = `<figcaption ${getStyles(
|
||||
"figcaption"
|
||||
)}>${text}</figcaption>`;
|
||||
}
|
||||
let figureStyles = getStyles("figure");
|
||||
let imgStyles = getStyles("image");
|
||||
return `<figure ${figureStyles}><img ${imgStyles} src="${href}" title="${title}" alt="${text}"/>${subText}</figure>`;
|
||||
};
|
||||
renderer.link = (href, title, text) => {
|
||||
if (href.startsWith("https://mp.weixin.qq.com")) {
|
||||
return `<a href="${href}" title="${title || text}" ${getStyles(
|
||||
"wx_link"
|
||||
)}>${text}</a>`;
|
||||
}
|
||||
if (href === text) {
|
||||
return text;
|
||||
}
|
||||
if (status) {
|
||||
let ref = addFootnote(title || text, href);
|
||||
return `<span ${getStyles("link")}>${text}<sup>[${ref}]</sup></span>`;
|
||||
}
|
||||
return `<span ${getStyles("link")}>${text}</span>`;
|
||||
};
|
||||
renderer.strong = (text) =>
|
||||
`<strong ${getStyles("strong")}>${text}</strong>`;
|
||||
renderer.em = (text) =>
|
||||
`<span style="font-style: italic;">${text}</span>`;
|
||||
renderer.table = (header, body) =>
|
||||
`<section style="padding:0 8px;"><table class="preview-table"><thead ${getStyles(
|
||||
"thead"
|
||||
)}>${header}</thead><tbody>${body}</tbody></table></section>`;
|
||||
renderer.tablecell = (text, flags) =>
|
||||
`<td ${getStyles("td")}>${text}</td>`;
|
||||
renderer.hr = () => `<hr ${getStyles("hr")}>`;
|
||||
return renderer;
|
||||
};
|
||||
}
|
||||
}
|
||||
export default WxRenderer;
|
@ -1,192 +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,
|
||||
},
|
||||
|
||||
// 引用样式
|
||||
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,371 +0,0 @@
|
||||
import prettier from 'prettier/standalone'
|
||||
import prettierCss from 'prettier/parser-postcss'
|
||||
import prettierMarkdown from 'prettier/parser-markdown'
|
||||
import defaultTheme from './themes/default-theme'
|
||||
|
||||
const createCustomTheme = (theme, color) => {
|
||||
const customTheme = JSON.parse(JSON.stringify(theme))
|
||||
customTheme.block.h1[`border-bottom`] = `2px solid ${color}`
|
||||
customTheme.block.h2[`background`] = color
|
||||
customTheme.block.h3[`border-left`] = `3px solid ${color}`
|
||||
customTheme.block.h4[`color`] = color
|
||||
customTheme.inline.strong[`color`] = color
|
||||
return customTheme
|
||||
}
|
||||
|
||||
// 设置自定义颜色
|
||||
export function setColorWithTemplate(theme) {
|
||||
return (color) => {
|
||||
return createCustomTheme(theme, color)
|
||||
}
|
||||
}
|
||||
|
||||
export function setColorWithCustomTemplate(theme, color) {
|
||||
return createCustomTheme(theme, color)
|
||||
}
|
||||
|
||||
// 设置自定义字体大小
|
||||
export function setFontSizeWithTemplate(template) {
|
||||
return function (fontSize) {
|
||||
const customTheme = JSON.parse(JSON.stringify(template))
|
||||
customTheme.block.h1[`font-size`] = `${fontSize * 1.14}px`
|
||||
customTheme.block.h2[`font-size`] = `${fontSize * 1.1}px`
|
||||
customTheme.block.h3[`font-size`] = `${fontSize}px`
|
||||
customTheme.block.h4[`font-size`] = `${fontSize}px`
|
||||
return customTheme
|
||||
}
|
||||
}
|
||||
|
||||
export const setColor = setColorWithTemplate(defaultTheme)
|
||||
export const setFontSize = setFontSizeWithTemplate(defaultTheme)
|
||||
|
||||
export function customCssWithTemplate(jsonString, color, theme) {
|
||||
// block
|
||||
const customTheme = createCustomTheme(theme, color)
|
||||
|
||||
customTheme.block.h1 = Object.assign(customTheme.block.h1, jsonString.h1)
|
||||
customTheme.block.h2 = Object.assign(customTheme.block.h2, jsonString.h2)
|
||||
customTheme.block.h3 = Object.assign(customTheme.block.h3, jsonString.h3)
|
||||
customTheme.block.h4 = Object.assign(customTheme.block.h4, jsonString.h4)
|
||||
customTheme.block.code = Object.assign(
|
||||
customTheme.block.code,
|
||||
jsonString.code
|
||||
)
|
||||
customTheme.block.p = Object.assign(customTheme.block.p, jsonString.p)
|
||||
customTheme.block.hr = Object.assign(customTheme.block.hr, jsonString.hr)
|
||||
customTheme.block.blockquote = Object.assign(
|
||||
customTheme.block.blockquote,
|
||||
jsonString.blockquote
|
||||
)
|
||||
customTheme.block.blockquote_p = Object.assign(
|
||||
customTheme.block.blockquote_p,
|
||||
jsonString.blockquote_p
|
||||
)
|
||||
customTheme.block.image = Object.assign(
|
||||
customTheme.block.image,
|
||||
jsonString.image
|
||||
)
|
||||
|
||||
// inline
|
||||
customTheme.inline.strong = Object.assign(
|
||||
customTheme.inline.strong,
|
||||
jsonString.strong
|
||||
)
|
||||
customTheme.inline.codespan = Object.assign(
|
||||
customTheme.inline.codespan,
|
||||
jsonString.codespan
|
||||
)
|
||||
customTheme.inline.link = Object.assign(
|
||||
customTheme.inline.link,
|
||||
jsonString.link
|
||||
)
|
||||
customTheme.inline.wx_link = Object.assign(
|
||||
customTheme.inline.wx_link,
|
||||
jsonString.wx_link
|
||||
)
|
||||
customTheme.block.ul = Object.assign(customTheme.block.ul, jsonString.ul)
|
||||
customTheme.block.ol = Object.assign(customTheme.block.ol, jsonString.ol)
|
||||
customTheme.inline.listitem = Object.assign(
|
||||
customTheme.inline.listitem,
|
||||
jsonString.li
|
||||
)
|
||||
return customTheme
|
||||
}
|
||||
|
||||
/**
|
||||
* 将CSS形式的字符串转换为JSON
|
||||
*
|
||||
* @param {string} css - css字符串
|
||||
*/
|
||||
export function css2json(css) {
|
||||
// 移除CSS所有注释
|
||||
let open, close
|
||||
while (
|
||||
(open = css.indexOf(`/*`)) !== -1 &&
|
||||
(close = css.indexOf(`*/`)) !== -1
|
||||
) {
|
||||
css = css.substring(0, open) + css.substring(close + 2)
|
||||
}
|
||||
|
||||
// 初始化返回值
|
||||
let json = {}
|
||||
|
||||
while (css.length > 0 && css.indexOf(`{`) !== -1 && css.indexOf(`}`) !== -1) {
|
||||
// 存储第一个左/右花括号的下标
|
||||
const lbracket = css.indexOf(`{`)
|
||||
const rbracket = css.indexOf(`}`)
|
||||
|
||||
// 第一步:将声明转换为Object,如:
|
||||
// `font: 'Times New Roman' 1em; color: #ff0000; margin-top: 1em;`
|
||||
// ==>
|
||||
// `{"font": "'Times New Roman' 1em", "color": "#ff0000", "margin-top": "1em"}`
|
||||
|
||||
// 辅助方法:将array转为object
|
||||
// eslint-disable-next-line no-inner-declarations
|
||||
function toObject(array) {
|
||||
let ret = {}
|
||||
array.forEach((e) => {
|
||||
const index = e.indexOf(`:`)
|
||||
const property = e.substring(0, index).trim()
|
||||
ret[property] = e.substring(index + 1).trim()
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
// 切割声明块并移除空白符,然后放入数组中
|
||||
let declarations = css
|
||||
.substring(lbracket + 1, rbracket)
|
||||
.split(`;`)
|
||||
.map((e) => e.trim())
|
||||
.filter((e) => e.length > 0) // 移除所有""空值
|
||||
|
||||
// 转为Object对象
|
||||
declarations = toObject(declarations)
|
||||
|
||||
// 第二步:选择器处理,每个选择器会与它对应的声明相关联,如:
|
||||
// `h1, p#bar {color: red}`
|
||||
// ==>
|
||||
// {"h1": {color: red}, "p#bar": {color: red}}
|
||||
|
||||
let selectors = css
|
||||
.substring(0, lbracket)
|
||||
// 以,切割,并移除空格:`"h1, p#bar, span.foo"` => ["h1", "p#bar", "span.foo"]
|
||||
.split(`,`)
|
||||
.map((selector) => selector.trim())
|
||||
|
||||
// 迭代赋值
|
||||
selectors.forEach((selector) => {
|
||||
// 若不存在,则先初始化
|
||||
if (!json[selector]) json[selector] = {}
|
||||
// 赋值到JSON
|
||||
Object.keys(declarations).forEach((key) => {
|
||||
json[selector][key] = declarations[key]
|
||||
})
|
||||
})
|
||||
|
||||
// 继续下个声明块
|
||||
css = css.slice(rbracket + 1).trim()
|
||||
}
|
||||
|
||||
// 返回JSON形式的结果串
|
||||
return json
|
||||
}
|
||||
|
||||
/**
|
||||
* 将编辑器内容保存到 LocalStorage
|
||||
* @param {*} editor
|
||||
* @param {*} name
|
||||
*/
|
||||
export function saveEditorContent(editor, name) {
|
||||
const content = editor.getValue(0)
|
||||
if (content) {
|
||||
localStorage.setItem(name, content)
|
||||
} else {
|
||||
localStorage.removeItem(name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化文档
|
||||
* @param {string} content - 文档内容
|
||||
*/
|
||||
export function formatDoc(content) {
|
||||
return prettier.format(content, {
|
||||
parser: `markdown`,
|
||||
plugins: [prettierMarkdown],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化css
|
||||
* @param {string} content - css内容
|
||||
*/
|
||||
export function formatCss(content) {
|
||||
return prettier.format(content, {
|
||||
parser: `css`,
|
||||
plugins: [prettierCss],
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出原始 Markdown 文档
|
||||
* @param {string} doc - 文档内容
|
||||
*/
|
||||
export function downloadMD(doc) {
|
||||
const downLink = document.createElement(`a`)
|
||||
|
||||
downLink.download = `content.md`
|
||||
downLink.style.display = `none`
|
||||
const blob = new Blob([doc])
|
||||
|
||||
downLink.href = URL.createObjectURL(blob)
|
||||
document.body.appendChild(downLink)
|
||||
downLink.click()
|
||||
document.body.removeChild(downLink)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出 HTML 生成内容
|
||||
*/
|
||||
export function exportHTML() {
|
||||
const element = document.querySelector(`#output`)
|
||||
setStyles(element)
|
||||
const htmlStr = element.innerHTML
|
||||
|
||||
const downLink = document.createElement(`a`)
|
||||
|
||||
downLink.download = `content.html`
|
||||
downLink.style.display = `none`
|
||||
let blob = new Blob([
|
||||
`<html><head><meta charset="utf-8" /></head><body><div style="width: 750px; margin: auto;">${htmlStr}</div></body></html>`,
|
||||
])
|
||||
|
||||
downLink.href = URL.createObjectURL(blob)
|
||||
document.body.appendChild(downLink)
|
||||
downLink.click()
|
||||
document.body.removeChild(downLink)
|
||||
|
||||
function setStyles(element) {
|
||||
/**
|
||||
* 获取一个 DOM 元素的所有样式,
|
||||
* @param {DOM 元素} element DOM 元素
|
||||
* @param {排除的属性} excludes 如果某些属性对结果有不良影响,可以使用这个参数来排除
|
||||
* @returns 行内样式拼接结果
|
||||
*/
|
||||
function getElementStyles(element, excludes = [`width`, `height`]) {
|
||||
const styles = getComputedStyle(element, null)
|
||||
return Object.entries(styles)
|
||||
.filter(
|
||||
([key]) => styles.getPropertyValue(key) && !excludes.includes(key)
|
||||
)
|
||||
.map(([key, value]) => `${key}:${value};`)
|
||||
.join(``)
|
||||
}
|
||||
|
||||
switch (true) {
|
||||
case isPre(element):
|
||||
case isCode(element):
|
||||
case isSpan(element):
|
||||
element.setAttribute(`style`, getElementStyles(element))
|
||||
// eslint-disable-next-line no-fallthrough
|
||||
default:
|
||||
}
|
||||
if (element.children.length) {
|
||||
Array.from(element.children).forEach((child) => setStyles(child))
|
||||
}
|
||||
|
||||
// 判断是否是包裹代码块的 pre 元素
|
||||
function isPre(element) {
|
||||
return (
|
||||
element.tagName === `PRE` &&
|
||||
Array.from(element.classList).includes(`code__pre`)
|
||||
)
|
||||
}
|
||||
|
||||
// 判断是否是包裹代码块的 code 元素
|
||||
function isCode(element) {
|
||||
return (
|
||||
element.tagName === `CODE` &&
|
||||
Array.from(element.classList).includes(`prettyprint`)
|
||||
)
|
||||
}
|
||||
|
||||
// 判断是否是包裹代码字符的 span 元素
|
||||
function isSpan(element) {
|
||||
return (
|
||||
element.tagName === `SPAN` &&
|
||||
(isCode(element.parentElement) ||
|
||||
isCode(element.parentElement.parentElement))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成列表字符串
|
||||
* @param {*} data 对应内容集合
|
||||
* @param {*} rows 行
|
||||
* @param {*} cols 列
|
||||
*/
|
||||
export function createTable({ data, rows, cols }) {
|
||||
let table = ``
|
||||
for (let i = 0; i < rows + 2; ++i) {
|
||||
table += `| `
|
||||
const currRow = []
|
||||
for (let j = 0; j < cols; ++j) {
|
||||
const rowIdx = i > 1 ? i - 1 : i
|
||||
currRow.push(i === 1 ? `---` : data[`k_${rowIdx}_${j}`] || ` `)
|
||||
}
|
||||
table += currRow.join(` | `)
|
||||
table += ` |\n`
|
||||
}
|
||||
|
||||
return table
|
||||
}
|
||||
|
||||
export function toBase64(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = () => resolve(reader.result.split(`,`).pop())
|
||||
reader.onerror = (error) => reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
export function checkImage(file) {
|
||||
// check filename suffix
|
||||
const isValidSuffix = /\.(gif|jpg|jpeg|png|GIF|JPG|PNG)$/.test(file.name)
|
||||
if (!isValidSuffix) {
|
||||
return {
|
||||
ok: false,
|
||||
msg: `请上传 JPG/PNG/GIF 格式的图片`,
|
||||
}
|
||||
}
|
||||
|
||||
// check file size
|
||||
const maxSize = 10
|
||||
const valid = file.size / 1024 / 1024 <= maxSize
|
||||
if (!valid) {
|
||||
return {
|
||||
ok: false,
|
||||
msg: `由于公众号限制,图片大小不能超过 ${maxSize}M`,
|
||||
}
|
||||
}
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除左边多余空格
|
||||
* @param {*} str
|
||||
* @returns
|
||||
*/
|
||||
export function removeLeft(str) {
|
||||
const lines = str.split(`\n`)
|
||||
// 获取应该删除的空白符数量
|
||||
const minSpaceNum = lines
|
||||
.filter((item) => item.trim())
|
||||
.map((item) => item.match(/(^\s+)?/)[0].length)
|
||||
.sort((a, b) => a - b)[0]
|
||||
// 删除空白符
|
||||
return lines.map((item) => item.slice(minSpaceNum)).join(`\n`)
|
||||
}
|
@ -1,28 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { useDisplayStore, useStore } from '@/stores'
|
||||
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const store = useStore()
|
||||
const displayStore = useDisplayStore()
|
||||
|
||||
function editTabName() {
|
||||
ElMessageBox.prompt(`请输入新的方案名称`, `编辑方案名称`, {
|
||||
confirmButtonText: `确认`,
|
||||
cancelButtonText: `取消`,
|
||||
inputValue: store.cssContentConfig.active,
|
||||
inputErrorMessage: `不能与现有方案重名`,
|
||||
inputValidator: store.validatorTabName,
|
||||
})
|
||||
.then(({ value }) => {
|
||||
if (!(`${value}`).trim()) {
|
||||
ElMessage.error(`修改失败,方案名不可为空`)
|
||||
return
|
||||
}
|
||||
store.renameTab(value)
|
||||
ElMessage.success(`修改成功~`)
|
||||
})
|
||||
}
|
||||
|
||||
function handleTabsEdit(targetName: string, action: string) {
|
||||
if (action === `add`) {
|
||||
ElMessageBox.prompt(`请输入方案名称`, `新建自定义 CSS`, {
|
||||
confirmButtonText: `确认`,
|
||||
cancelButtonText: `取消`,
|
||||
inputValue: `方案${store.cssContentConfig.tabs.length + 1}`,
|
||||
inputErrorMessage: `不能与现有方案重名`,
|
||||
inputValidator: store.validatorTabName,
|
||||
})
|
||||
.then(({ value }) => {
|
||||
if (!(`${value}`).trim()) {
|
||||
ElMessage.error(`新建失败,方案名不可为空`)
|
||||
return
|
||||
}
|
||||
store.addCssContentTab(value)
|
||||
ElMessage.success(`新建成功~`)
|
||||
})
|
||||
}
|
||||
else if (action === `remove`) {
|
||||
const tabs = store.cssContentConfig.tabs
|
||||
if (tabs.length === 1) {
|
||||
ElMessage.warning(`至少保留一个方案`)
|
||||
return
|
||||
}
|
||||
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.tabChanged(activeName)
|
||||
store.cssContentConfig.tabs = tabs.filter(tab => tab.name !== targetName)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition enter-active-class="bounceInRight">
|
||||
<el-col :span="12" v-show="showCssEditor" class="cssEditor-wrapper">
|
||||
<el-col v-show="displayStore.isShowCssEditor" :span="8" class="cssEditor-wrapper order-1 h-full flex flex-col border-l-1">
|
||||
<el-tabs
|
||||
v-model="store.cssContentConfig.active"
|
||||
type="border-card"
|
||||
stretch
|
||||
editable
|
||||
@edit="handleTabsEdit"
|
||||
@tab-change="store.tabChanged"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="item in store.cssContentConfig.tabs"
|
||||
:key="item.name"
|
||||
:name="item.name"
|
||||
>
|
||||
<template #label>
|
||||
{{ item.title }}
|
||||
<el-icon
|
||||
v-if="store.cssContentConfig.active === item.name"
|
||||
class="ml-1"
|
||||
@click="editTabName()"
|
||||
>
|
||||
<ElIconEditPen />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<textarea
|
||||
id="cssEditor"
|
||||
type="textarea"
|
||||
placeholder="Your custom css here."
|
||||
>
|
||||
</textarea>
|
||||
/>
|
||||
</el-col>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: `CssEditor`,
|
||||
props: {
|
||||
showCssEditor: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.bounceInRight {
|
||||
animation-name: bounceInRight;
|
||||
@ -61,4 +141,19 @@ export default {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__content) {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
// 当 tab 为激活状态时,隐藏关闭按钮
|
||||
:deep(.el-tabs__item.is-active) {
|
||||
.is-icon-close {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-tabs__new-tab) {
|
||||
margin-right: 1em;
|
||||
}
|
||||
</style>
|
||||
|
63
src/components/CodemirrorEditor/EditorHeader/AboutDialog.vue
Normal file
63
src/components/CodemirrorEditor/EditorHeader/AboutDialog.vue
Normal file
@ -0,0 +1,63 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
|
||||
const props = defineProps({
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits([`close`])
|
||||
|
||||
function onUpdate(val: boolean) {
|
||||
if (!val) {
|
||||
emit(`close`)
|
||||
}
|
||||
}
|
||||
|
||||
const links = [
|
||||
{ label: `GitHub 仓库`, url: `https://github.com/doocs/md` },
|
||||
{ label: `Gitee 仓库`, url: `https://gitee.com/doocs/md` },
|
||||
{ label: `GitCode 仓库`, url: `https://gitcode.com/doocs/md` },
|
||||
]
|
||||
|
||||
function onRedirect(url: string) {
|
||||
window.open(url, `_blank`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog :open="props.visible" @update:open="onUpdate">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>关于</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div class="text-center">
|
||||
<h3>一款高度简洁的微信 Markdown 编辑器</h3>
|
||||
<p>扫码关注公众号 Doocs,原创技术文章第一时间推送!</p>
|
||||
<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"
|
||||
alt="Doocs Markdown 编辑器"
|
||||
style="width: 40%"
|
||||
>
|
||||
</div>
|
||||
<DialogFooter class="sm:justify-evenly">
|
||||
<Button
|
||||
v-for="link in links"
|
||||
:key="link.url"
|
||||
@click="onRedirect(link.url)"
|
||||
>
|
||||
{{ link.label }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
@ -0,0 +1,30 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarMenu,
|
||||
MenubarTrigger,
|
||||
} from '@/components/ui/menubar'
|
||||
import { useDisplayStore } from '@/stores'
|
||||
import { TableIcon, UploadCloudIcon } from 'lucide-vue-next'
|
||||
|
||||
const { toggleShowInsertFormDialog, toggleShowUploadImgDialog } = useDisplayStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>
|
||||
编辑
|
||||
</MenubarTrigger>
|
||||
<MenubarContent align="start">
|
||||
<MenubarItem @click="toggleShowUploadImgDialog()">
|
||||
<UploadCloudIcon class="mr-2 h-4 w-4" />
|
||||
上传图片
|
||||
</MenubarItem>
|
||||
<MenubarItem @click="toggleShowInsertFormDialog()">
|
||||
<TableIcon class="mr-2 h-4 w-4" />
|
||||
插入表格
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</template>
|
@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
import { useStore } from '@/stores'
|
||||
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const {
|
||||
isDark,
|
||||
isEditOnLeft,
|
||||
} = storeToRefs(store)
|
||||
|
||||
const {
|
||||
toggleDark,
|
||||
toggleEditOnLeft,
|
||||
exportEditorContent2HTML,
|
||||
exportEditorContent2MD,
|
||||
importMarkdownContent,
|
||||
} = store
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>
|
||||
文件
|
||||
</MenubarTrigger>
|
||||
<MenubarContent align="start">
|
||||
<MenubarItem @click="importMarkdownContent()">
|
||||
<el-icon class="mr-2 h-4 w-4">
|
||||
<ElIconUpload />
|
||||
</el-icon>
|
||||
导入 .md
|
||||
</MenubarItem>
|
||||
<MenubarItem @click="exportEditorContent2MD()">
|
||||
<el-icon class="mr-2 h-4 w-4">
|
||||
<ElIconDownload />
|
||||
</el-icon>
|
||||
导出 .md
|
||||
</MenubarItem>
|
||||
<MenubarItem @click="exportEditorContent2HTML()">
|
||||
<el-icon class="mr-2 h-4 w-4">
|
||||
<ElIconDocument />
|
||||
</el-icon>
|
||||
导出 .html
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem @click="toggleDark()">
|
||||
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isDark }">
|
||||
<ElIconCheck />
|
||||
</el-icon>
|
||||
深色模式
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem @click="toggleEditOnLeft()">
|
||||
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isEditOnLeft }">
|
||||
<ElIconCheck />
|
||||
</el-icon>
|
||||
左侧编辑
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</template>
|
@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
import AboutDialog from './AboutDialog.vue'
|
||||
|
||||
const aboutDialogVisible = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger>
|
||||
帮助
|
||||
</MenubarTrigger>
|
||||
<MenubarContent align="start">
|
||||
<MenubarItem @click="aboutDialogVisible = true">
|
||||
<el-icon class="mr-2 h-4 w-4" />
|
||||
<span>关于</span>
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
|
||||
<AboutDialog :visible="aboutDialogVisible" @close="aboutDialogVisible = false" />
|
||||
</template>
|
118
src/components/CodemirrorEditor/EditorHeader/PostInfo.vue
Normal file
118
src/components/CodemirrorEditor/EditorHeader/PostInfo.vue
Normal file
@ -0,0 +1,118 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { useStore } from '@/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
const store = useStore()
|
||||
const { output } = storeToRefs(store)
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const form = ref<any>({
|
||||
title: ``,
|
||||
desc: ``,
|
||||
thumb: ``,
|
||||
content: ``,
|
||||
auto: {},
|
||||
})
|
||||
|
||||
function prePost() {
|
||||
let auto = {}
|
||||
try {
|
||||
auto = {
|
||||
thumb: document.querySelector<HTMLImageElement>(`#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
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
syncPost: (data: { thumb: string, title: string, desc: string, content: string }) => void
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
function onUpdate(val: boolean) {
|
||||
if (!val) {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Button variant="outline" @click="prePost">
|
||||
发布
|
||||
</Button>
|
||||
|
||||
<Dialog :open="dialogVisible" @update:open="onUpdate">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>发布</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<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>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="dialogVisible = false">
|
||||
取 消
|
||||
</Button>
|
||||
<Button @click="post">
|
||||
确 定
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</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>
|
153
src/components/CodemirrorEditor/EditorHeader/StyleDropdown.vue
Normal file
153
src/components/CodemirrorEditor/EditorHeader/StyleDropdown.vue
Normal file
@ -0,0 +1,153 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
HoverCard,
|
||||
HoverCardContent,
|
||||
HoverCardTrigger,
|
||||
} from '@/components/ui/hover-card'
|
||||
import {
|
||||
codeBlockThemeOptions,
|
||||
colorOptions,
|
||||
fontFamilyOptions,
|
||||
fontSizeOptions,
|
||||
legendOptions,
|
||||
themeOptions,
|
||||
} from '@/config'
|
||||
|
||||
import { useDisplayStore, useStore } from '@/stores'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import StyleOptionMenu from './StyleOptionMenu.vue'
|
||||
|
||||
const store = useStore()
|
||||
const { toggleShowCssEditor } = useDisplayStore()
|
||||
|
||||
const {
|
||||
theme,
|
||||
fontFamily,
|
||||
fontSize,
|
||||
primaryColor,
|
||||
codeBlockTheme,
|
||||
legend,
|
||||
isMacCodeBlock,
|
||||
cssEditor,
|
||||
} = storeToRefs(store)
|
||||
|
||||
const {
|
||||
resetStyleConfirm,
|
||||
themeChanged,
|
||||
fontChanged,
|
||||
sizeChanged,
|
||||
colorChanged,
|
||||
codeBlockThemeChanged,
|
||||
legendChanged,
|
||||
macCodeBlockChanged,
|
||||
} = store
|
||||
|
||||
const colorPicker = ref<HTMLElement & { show: () => void } | null>(null)
|
||||
|
||||
function showPicker() {
|
||||
colorPicker.value?.show()
|
||||
}
|
||||
|
||||
// 自定义CSS样式
|
||||
function customStyle() {
|
||||
toggleShowCssEditor()
|
||||
setTimeout(() => {
|
||||
cssEditor.value!.refresh()
|
||||
}, 50)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger> 样式 </MenubarTrigger>
|
||||
<MenubarContent class="w-56" align="start">
|
||||
<StyleOptionMenu
|
||||
title="主题"
|
||||
:options="themeOptions"
|
||||
:current="theme"
|
||||
:change="themeChanged"
|
||||
/>
|
||||
<MenubarSeparator />
|
||||
<StyleOptionMenu
|
||||
title="字体"
|
||||
:options="fontFamilyOptions"
|
||||
:current="fontFamily"
|
||||
:change="fontChanged"
|
||||
/>
|
||||
<StyleOptionMenu
|
||||
title="字号"
|
||||
:options="fontSizeOptions"
|
||||
:current="fontSize"
|
||||
:change="sizeChanged"
|
||||
/>
|
||||
<StyleOptionMenu
|
||||
title="主题色"
|
||||
:options="colorOptions"
|
||||
:current="primaryColor"
|
||||
:change="colorChanged"
|
||||
/>
|
||||
<StyleOptionMenu
|
||||
title="代码块主题"
|
||||
:options="codeBlockThemeOptions"
|
||||
:current="codeBlockTheme"
|
||||
:change="codeBlockThemeChanged"
|
||||
/>
|
||||
<StyleOptionMenu
|
||||
title="图注格式"
|
||||
:options="legendOptions"
|
||||
:current="legend"
|
||||
:change="legendChanged"
|
||||
/>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem @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">
|
||||
<ElColorPicker
|
||||
ref="colorPicker"
|
||||
v-model="primaryColor"
|
||||
: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="primaryColor"
|
||||
:teleported="false"
|
||||
show-alpha
|
||||
class="ml-auto"
|
||||
style="height: 2em"
|
||||
@change="colorChanged"
|
||||
@click="showPicker"
|
||||
/> -->
|
||||
</MenubarItem>
|
||||
<MenubarItem @click="customStyle">
|
||||
<el-icon class="mr-2 h-4 w-4" />
|
||||
自定义 CSS
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem @click="macCodeBlockChanged">
|
||||
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isMacCodeBlock }">
|
||||
<ElIconCheck />
|
||||
</el-icon>
|
||||
Mac 代码块
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem divided @click="resetStyleConfirm">
|
||||
<el-icon class="mr-2 h-4 w-4" />
|
||||
重置
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
</template>
|
@ -1,67 +1,56 @@
|
||||
<template>
|
||||
<el-dropdown placement="right" class="style-option-menu">
|
||||
<div class="el-dropdown-link">
|
||||
{{ label }}
|
||||
<i class="el-icon-arrow-right el-icon--right"></i>
|
||||
</div>
|
||||
<el-dropdown-menu slot="dropdown" style="width: 200px">
|
||||
<el-dropdown-item
|
||||
v-for="{ value, label, desc } in options"
|
||||
:key="value"
|
||||
:label="label"
|
||||
:value="value"
|
||||
@click.native="charge(value)"
|
||||
>
|
||||
<i
|
||||
class="el-icon-check"
|
||||
:style="{ opacity: current === value ? 1 : 0 }"
|
||||
></i>
|
||||
{{ label }}
|
||||
<span class="select-item-right">{{ desc }}</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { IConfigOption } from '@/types'
|
||||
import {
|
||||
MenubarItem,
|
||||
MenubarSub,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
} from '@/components/ui/menubar'
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: `StyleOptionMenu`,
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
options: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
current: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
charge: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
const props = defineProps<{
|
||||
title: string
|
||||
options: IConfigOption[]
|
||||
current: string
|
||||
change: <T>(val: T) => void
|
||||
}>()
|
||||
|
||||
function setStyle(title: string, value: string) {
|
||||
switch (title) {
|
||||
case `字体`:
|
||||
return { fontFamily: value }
|
||||
case `字号`:
|
||||
return { fontSize: value }
|
||||
case `主题色`:
|
||||
return { color: value }
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.style-option-menu.el-dropdown {
|
||||
margin: 0;
|
||||
width: 150px;
|
||||
|
||||
.el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.select-item-right {
|
||||
float: right;
|
||||
color: #8492a6;
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
<template>
|
||||
<MenubarSub>
|
||||
<MenubarSubTrigger>
|
||||
<el-icon class="mr-2 h-4 w-4" />
|
||||
<span>{{ props.title }}</span>
|
||||
</MenubarSubTrigger>
|
||||
<MenubarSubContent class="max-h-56 overflow-auto">
|
||||
<MenubarItem
|
||||
v-for="{ label, value, desc } in options"
|
||||
:key="value"
|
||||
:label="label"
|
||||
:model-value="value"
|
||||
class="w-50"
|
||||
@click="change(value)"
|
||||
>
|
||||
<el-icon class="mr-2 h-4 w-4" :style="{ opacity: +(current === value) }">
|
||||
<ElIconCheck />
|
||||
</el-icon>
|
||||
{{ label }}
|
||||
<DropdownMenuShortcut :style="setStyle(title, value)">
|
||||
{{ desc }}
|
||||
</DropdownMenuShortcut>
|
||||
</MenubarItem>
|
||||
</MenubarSubContent>
|
||||
</MenubarSub>
|
||||
</template>
|
||||
|
@ -1,472 +1,517 @@
|
||||
<template>
|
||||
<el-container class="header-container is-dark">
|
||||
<div class="dropdowns">
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
文件<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="refClick">
|
||||
<i class="el-icon-upload2"></i>
|
||||
导入 .md
|
||||
<input hidden type="file" ref="fileInput" accept=".md" />
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="$emit('download')">
|
||||
<i class="el-icon-download"></i>
|
||||
导出 .md
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="$emit('export')">
|
||||
<i class="el-icon-document"></i>
|
||||
导出 .html
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click.native="themeChanged">
|
||||
<i
|
||||
class="el-icon-check"
|
||||
:style="{ opacity: nightMode ? 1 : 0 }"
|
||||
></i>
|
||||
暗黑模式
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
格式<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
class="format-item"
|
||||
v-for="{ label, kbd, emitArgs } in formatItems"
|
||||
:key="kbd"
|
||||
@click.native="$emit(...emitArgs)"
|
||||
>
|
||||
{{ label }}
|
||||
<kbd>{{ kbd }}</kbd>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item divided @click.native="statusChanged">
|
||||
<i
|
||||
class="el-icon-check"
|
||||
:style="{ opacity: citeStatus ? 1 : 0 }"
|
||||
></i>
|
||||
微信外链转底部引用
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
编辑<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item @click.native="$emit('show-dialog-upload-img')">
|
||||
<i class="el-icon-upload"></i>
|
||||
上传图片
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.native="$emit('show-dialog-form')">
|
||||
<i class="el-icon-s-grid"></i>
|
||||
插入表格
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
<el-dropdown>
|
||||
<span class="el-dropdown-link">
|
||||
样式<i class="el-icon-arrow-down el-icon--right"></i>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item class="padding-left-3">
|
||||
<style-option-menu
|
||||
label="字体"
|
||||
:options="config.builtinFonts"
|
||||
:current="selectFont"
|
||||
:charge="fontChanged"
|
||||
></style-option-menu>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="padding-left-3">
|
||||
<style-option-menu
|
||||
label="字号"
|
||||
:options="config.sizeOption"
|
||||
:current="selectSize"
|
||||
:charge="sizeChanged"
|
||||
></style-option-menu>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="padding-left-3">
|
||||
<style-option-menu
|
||||
label="颜色"
|
||||
:options="config.colorOption"
|
||||
:current="selectColor"
|
||||
:charge="colorChanged"
|
||||
></style-option-menu>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="padding-left-3">
|
||||
<style-option-menu
|
||||
label="代码主题"
|
||||
:options="config.codeThemeOption"
|
||||
:current="selectCodeTheme"
|
||||
:charge="codeThemeChanged"
|
||||
></style-option-menu>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
divided
|
||||
class="padding-left-3"
|
||||
@click.native="showPicker()"
|
||||
>
|
||||
自定义颜色
|
||||
<el-color-picker
|
||||
show-alpha
|
||||
ref="colorPicker"
|
||||
size="mini"
|
||||
style="float: right; margin-top: 3px"
|
||||
v-model="selectColor"
|
||||
@change="colorChanged"
|
||||
></el-color-picker>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item class="padding-left-3" @click.native="customStyle">
|
||||
自定义 CSS
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
divided
|
||||
class="padding-left-3"
|
||||
@click.native="showResetConfirm = true"
|
||||
>
|
||||
重置
|
||||
</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>
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Menubar,
|
||||
MenubarContent,
|
||||
MenubarItem,
|
||||
MenubarMenu,
|
||||
MenubarSeparator,
|
||||
MenubarShortcut,
|
||||
MenubarTrigger,
|
||||
} from '@/components/ui/menubar'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
|
||||
<post-info-dialog
|
||||
:form="form"
|
||||
@post="post"
|
||||
@close="form.dialogVisible = false"
|
||||
>
|
||||
</post-info-dialog>
|
||||
<reset-dialog
|
||||
:show-reset-confirm="showResetConfirm"
|
||||
@confirm="confirmReset"
|
||||
@close="cancelReset"
|
||||
></reset-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
import {
|
||||
altSign,
|
||||
codeBlockThemeOptions,
|
||||
colorOptions,
|
||||
ctrlKey,
|
||||
ctrlSign,
|
||||
fontFamilyOptions,
|
||||
fontSizeOptions,
|
||||
legendOptions,
|
||||
shiftSign,
|
||||
themeOptions,
|
||||
} from '@/config'
|
||||
import { useDisplayStore, useStore } from '@/stores'
|
||||
import { mergeCss, solveWeChatImage } from '@/utils'
|
||||
import { ElNotification } from 'element-plus'
|
||||
import { Moon, Paintbrush, Sun } from 'lucide-vue-next'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useStore } from '@/stores'
|
||||
import { nextTick } from 'vue'
|
||||
import EditDropdown from './EditDropdown.vue'
|
||||
|
||||
import { setFontSize, setColorWithCustomTemplate } from '@/assets/scripts/util'
|
||||
import { solveWeChatImage, mergeCss } from '@/assets/scripts/converter'
|
||||
import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt'
|
||||
import config from '@/assets/scripts/config'
|
||||
import ResetDialog from './ResetDialog'
|
||||
import StyleOptionMenu from './StyleOptionMenu'
|
||||
import PostInfoDialog from './PostInfoDialog'
|
||||
import FileDropdown from './FileDropdown.vue'
|
||||
import HelpDropdown from './HelpDropdown.vue'
|
||||
|
||||
export default {
|
||||
name: `editor-header`,
|
||||
data() {
|
||||
return {
|
||||
config,
|
||||
citeStatus: false,
|
||||
showResetConfirm: false,
|
||||
selectFont: ``,
|
||||
selectSize: ``,
|
||||
selectColor: ``,
|
||||
selectCodeTheme: config.codeThemeOption[2].value,
|
||||
form: {
|
||||
dialogVisible: false,
|
||||
title: ``,
|
||||
desc: ``,
|
||||
thumb: ``,
|
||||
content: ``,
|
||||
},
|
||||
formatItems: [
|
||||
{
|
||||
label: `加粗`,
|
||||
kbd: `Ctrl/Command + B`,
|
||||
emitArgs: [`addFormat`, `**`],
|
||||
},
|
||||
{
|
||||
label: `斜体`,
|
||||
kbd: `Ctrl/Command + I`,
|
||||
emitArgs: [`addFormat`, `*`],
|
||||
},
|
||||
{
|
||||
label: `删除线`,
|
||||
kbd: `Ctrl/Command + D`,
|
||||
emitArgs: [`addFormat`, `~~`],
|
||||
},
|
||||
{
|
||||
label: `超链接`,
|
||||
kbd: `Ctrl/Command + K`,
|
||||
emitArgs: [`addFormat`, `[`, `]()`],
|
||||
},
|
||||
{
|
||||
label: `格式化`,
|
||||
kbd: `Ctrl/Command + F`,
|
||||
emitArgs: [`formatContent`],
|
||||
},
|
||||
],
|
||||
import PostInfo from './PostInfo.vue'
|
||||
import StyleDropdown from './StyleDropdown.vue'
|
||||
|
||||
const emit = defineEmits([`addFormat`, `formatContent`, `startCopy`, `endCopy`])
|
||||
|
||||
const formatItems = [
|
||||
{
|
||||
label: `加粗`,
|
||||
kbd: [ctrlSign, `B`],
|
||||
emitArgs: [`addFormat`, `${ctrlKey}-B`],
|
||||
},
|
||||
{
|
||||
label: `斜体`,
|
||||
kbd: [ctrlSign, `I`],
|
||||
emitArgs: [`addFormat`, `${ctrlKey}-I`],
|
||||
},
|
||||
{
|
||||
label: `删除线`,
|
||||
kbd: [ctrlSign, `D`],
|
||||
emitArgs: [`addFormat`, `${ctrlKey}-D`],
|
||||
},
|
||||
{
|
||||
label: `超链接`,
|
||||
kbd: [ctrlSign, `K`],
|
||||
emitArgs: [`addFormat`, `${ctrlKey}-K`],
|
||||
},
|
||||
{
|
||||
label: `行内代码`,
|
||||
kbd: [ctrlSign, `E`],
|
||||
emitArgs: [`addFormat`, `${ctrlKey}-E`],
|
||||
},
|
||||
{
|
||||
label: `格式化`,
|
||||
kbd: [altSign, shiftSign, `F`],
|
||||
emitArgs: [`formatContent`],
|
||||
},
|
||||
] as const
|
||||
|
||||
const store = useStore()
|
||||
const displayStore = useDisplayStore()
|
||||
|
||||
const { isDark, isCiteStatus, output, primaryColor } = storeToRefs(store)
|
||||
|
||||
const { toggleDark, editorRefresh, citeStatusChanged } = store
|
||||
|
||||
// 复制到微信公众号
|
||||
function copy() {
|
||||
emit(`startCopy`)
|
||||
setTimeout(() => {
|
||||
function modifyHtmlStructure(htmlString: string) {
|
||||
// 创建一个 div 元素来暂存原始 HTML 字符串
|
||||
const tempDiv = document.createElement(`div`)
|
||||
tempDiv.innerHTML = htmlString
|
||||
|
||||
const originalItems = tempDiv.querySelectorAll(`li > ul, li > ol`)
|
||||
|
||||
originalItems.forEach((originalItem) => {
|
||||
originalItem.parentElement!.insertAdjacentElement(`afterend`, originalItem)
|
||||
})
|
||||
|
||||
// 返回修改后的 HTML 字符串
|
||||
return tempDiv.innerHTML
|
||||
}
|
||||
},
|
||||
components: {
|
||||
PostInfoDialog,
|
||||
StyleOptionMenu,
|
||||
ResetDialog,
|
||||
},
|
||||
computed: {
|
||||
btnType() {
|
||||
return this.nightMode ? `default` : `primary`
|
||||
},
|
||||
...mapState(useStore, {
|
||||
output: (state) => state.output,
|
||||
editor: (state) => state.editor,
|
||||
cssEditor: (state) => state.cssEditor,
|
||||
currentFont: (state) => state.currentFont,
|
||||
currentSize: (state) => state.currentSize,
|
||||
currentColor: (state) => state.currentColor,
|
||||
codeTheme: (state) => state.codeTheme,
|
||||
nightMode: (state) => state.nightMode,
|
||||
currentCiteStatus: (state) => state.citeStatus,
|
||||
}),
|
||||
},
|
||||
methods: {
|
||||
refClick() {
|
||||
this.$refs.fileInput.click()
|
||||
},
|
||||
showPicker() {
|
||||
this.$refs.colorPicker.showPicker = true
|
||||
},
|
||||
prePost() {
|
||||
let auto = {}
|
||||
try {
|
||||
auto = {
|
||||
thumb: document.querySelector(`#output img`).src,
|
||||
title: [1, 2, 3, 4, 5, 6]
|
||||
.map((h) => document.querySelector(`#output h${h}`))
|
||||
.filter((h) => h)[0].innerText,
|
||||
desc: document.querySelector(`#output p`).innerText,
|
||||
content: this.output,
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`error`, error)
|
||||
}
|
||||
this.form = {
|
||||
dialogVisible: true,
|
||||
...auto,
|
||||
auto,
|
||||
}
|
||||
},
|
||||
post() {
|
||||
this.form.dialogVisible = false
|
||||
// 使用 window.$syncer 可以检测是否安装插件
|
||||
window.syncPost({
|
||||
title: this.form.title || this.form.auto.title,
|
||||
desc: this.form.desc || this.form.auto.desc,
|
||||
content: this.form.content || this.form.auto.content,
|
||||
thumb: this.form.thumb || this.form.auto.thumb,
|
||||
})
|
||||
},
|
||||
fontChanged(fonts) {
|
||||
this.setWxRendererOptions({
|
||||
fonts: fonts,
|
||||
})
|
||||
this.setCurrentFont(fonts)
|
||||
this.selectFont = fonts
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
sizeChanged(size) {
|
||||
let theme = setFontSize(size.replace(`px`, ``))
|
||||
theme = setColorWithCustomTemplate(theme, this.currentColor)
|
||||
this.setWxRendererOptions({
|
||||
size: size,
|
||||
theme: theme,
|
||||
})
|
||||
this.setCurrentSize(size)
|
||||
this.selectSize = size
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
colorChanged(color) {
|
||||
let theme = setFontSize(this.currentSize.replace(`px`, ``))
|
||||
|
||||
theme = setColorWithCustomTemplate(theme, color)
|
||||
this.setWxRendererOptions({
|
||||
theme: theme,
|
||||
})
|
||||
this.setCurrentColor(color)
|
||||
this.selectColor = color
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
codeThemeChanged(theme) {
|
||||
this.setCurrentCodeTheme(theme)
|
||||
this.selectCodeTheme = theme
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
statusChanged() {
|
||||
this.citeStatus = !this.citeStatus
|
||||
this.setCiteStatus(this.citeStatus)
|
||||
this.$emit(`refresh`)
|
||||
},
|
||||
// 复制到微信公众号
|
||||
copy() {
|
||||
this.$emit(`startCopy`)
|
||||
setTimeout(() => {
|
||||
solveWeChatImage()
|
||||
|
||||
const clipboardDiv = document.getElementById(`output`)
|
||||
clipboardDiv.innerHTML = mergeCss(clipboardDiv.innerHTML)
|
||||
|
||||
// 调整 katex 公式元素为行内标签,目的是兼容微信公众号渲染
|
||||
clipboardDiv.innerHTML = clipboardDiv.innerHTML
|
||||
.replace(
|
||||
/class="base"( style="display: inline")*/g,
|
||||
`class="base" style="display: inline"`
|
||||
)
|
||||
// 公众号不支持 position, 转换为等价的 translateY
|
||||
.replace(/top:(.*?)em/g, `transform: translateY($1em)`)
|
||||
|
||||
clipboardDiv.focus()
|
||||
window.getSelection().removeAllRanges()
|
||||
let range = document.createRange()
|
||||
|
||||
range.setStartBefore(clipboardDiv.firstChild)
|
||||
range.setEndAfter(clipboardDiv.lastChild)
|
||||
window.getSelection().addRange(range)
|
||||
document.execCommand(`copy`)
|
||||
window.getSelection().removeAllRanges()
|
||||
clipboardDiv.innerHTML = this.output
|
||||
// 输出提示
|
||||
this.$notify({
|
||||
showClose: true,
|
||||
message: `已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴`,
|
||||
offset: 80,
|
||||
duration: 1600,
|
||||
type: `success`,
|
||||
})
|
||||
this.$emit(`refresh`)
|
||||
this.$emit(`endCopy`)
|
||||
}, 350)
|
||||
},
|
||||
// 自定义CSS样式
|
||||
async customStyle() {
|
||||
this.$emit(`showCssEditor`)
|
||||
this.$nextTick(() => {
|
||||
if (!this.cssEditor) {
|
||||
this.cssEditor.refresh()
|
||||
}
|
||||
})
|
||||
setTimeout(() => {
|
||||
this.cssEditor.refresh()
|
||||
}, 50)
|
||||
|
||||
let flag = localStorage.getItem(`__css_content`)
|
||||
if (!flag) {
|
||||
this.setCssEditorValue(DEFAULT_CSS_CONTENT)
|
||||
}
|
||||
},
|
||||
// 重置样式
|
||||
confirmReset() {
|
||||
this.showResetConfirm = false
|
||||
localStorage.clear()
|
||||
this.cssEditor.setValue(DEFAULT_CSS_CONTENT)
|
||||
this.citeStatus = false
|
||||
this.statusChanged(false)
|
||||
this.fontChanged(this.config.builtinFonts[0].value)
|
||||
this.colorChanged(this.config.colorOption[0].value)
|
||||
this.sizeChanged(this.config.sizeOption[2].value)
|
||||
this.codeThemeChanged(this.config.codeThemeOption[2].value)
|
||||
this.$emit(`cssChanged`)
|
||||
this.selectFont = this.currentFont
|
||||
this.selectSize = this.currentSize
|
||||
this.selectColor = this.currentColor
|
||||
this.selectCodeTheme = this.codeTheme
|
||||
this.codeBlockChanged()
|
||||
},
|
||||
cancelReset() {
|
||||
this.showResetConfirm = false
|
||||
this.editor.focus()
|
||||
},
|
||||
...mapActions(useStore, [
|
||||
`setCurrentColor`,
|
||||
`setCiteStatus`,
|
||||
`themeChanged`,
|
||||
`setCurrentFont`,
|
||||
`setCurrentSize`,
|
||||
`setCssEditorValue`,
|
||||
`setCurrentCodeTheme`,
|
||||
`setWxRendererOptions`,
|
||||
]),
|
||||
},
|
||||
mounted() {
|
||||
this.selectFont = this.currentFont
|
||||
this.selectSize = this.currentSize
|
||||
this.selectColor = this.currentColor
|
||||
this.selectCodeTheme = this.codeTheme
|
||||
this.citeStatus = this.currentCiteStatus
|
||||
|
||||
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)
|
||||
}
|
||||
// 如果是深色模式,复制之前需要先切换到白天模式
|
||||
const isBeforeDark = isDark.value
|
||||
if (isBeforeDark) {
|
||||
toggleDark()
|
||||
}
|
||||
},
|
||||
|
||||
nextTick(() => {
|
||||
solveWeChatImage()
|
||||
|
||||
const clipboardDiv = document.getElementById(`output`)!
|
||||
clipboardDiv.innerHTML = mergeCss(clipboardDiv.innerHTML)
|
||||
clipboardDiv.innerHTML = modifyHtmlStructure(clipboardDiv.innerHTML)
|
||||
clipboardDiv.innerHTML = clipboardDiv.innerHTML
|
||||
// 公众号不支持 position, 转换为等价的 translateY
|
||||
.replace(/top:(.*?)em/g, `transform: translateY($1em)`)
|
||||
// 适配主题中的颜色变量
|
||||
.replaceAll(`var(--el-text-color-regular)`, `#3f3f3f`)
|
||||
.replaceAll(`var(--md-primary-color)`, primaryColor.value)
|
||||
.replaceAll(/--md-primary-color:.+?;/g, ``)
|
||||
clipboardDiv.focus()
|
||||
window.getSelection()!.removeAllRanges()
|
||||
const range = document.createRange()
|
||||
|
||||
range.setStartBefore(clipboardDiv.firstChild!)
|
||||
range.setEndAfter(clipboardDiv.lastChild!)
|
||||
window.getSelection()!.addRange(range)
|
||||
document.execCommand(`copy`)
|
||||
window.getSelection()!.removeAllRanges()
|
||||
clipboardDiv.innerHTML = output.value
|
||||
|
||||
if (isBeforeDark) {
|
||||
nextTick(() => toggleDark())
|
||||
}
|
||||
|
||||
// 输出提示
|
||||
ElNotification({
|
||||
showClose: true,
|
||||
message: `已复制渲染后的文章到剪贴板,可直接到公众号后台粘贴`,
|
||||
offset: 80,
|
||||
duration: 1600,
|
||||
type: `success`,
|
||||
})
|
||||
|
||||
editorRefresh()
|
||||
emit(`endCopy`)
|
||||
})
|
||||
}, 350)
|
||||
}
|
||||
|
||||
function customStyle() {
|
||||
displayStore.toggleShowCssEditor()
|
||||
setTimeout(() => {
|
||||
store.cssEditor!.refresh()
|
||||
}, 50)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="header-container h-15 flex items-center px-5">
|
||||
<Menubar class="menubar mr-auto">
|
||||
<FileDropdown />
|
||||
|
||||
<MenubarMenu>
|
||||
<MenubarTrigger> 格式 </MenubarTrigger>
|
||||
<MenubarContent class="w-60" align="start">
|
||||
<MenubarItem
|
||||
v-for="{ label, kbd, emitArgs } in formatItems"
|
||||
:key="label"
|
||||
@click="emitArgs[0] === 'addFormat' ? $emit(emitArgs[0], emitArgs[1]) : $emit(emitArgs[0])"
|
||||
>
|
||||
<el-icon class="mr-2 h-4 w-4" />
|
||||
{{ label }}
|
||||
<MenubarShortcut>
|
||||
<kbd v-for="item in kbd" :key="item" class="mx-1 bg-gray-2 dark:bg-stone-9">
|
||||
{{ item }}
|
||||
</kbd>
|
||||
</MenubarShortcut>
|
||||
</MenubarItem>
|
||||
<MenubarSeparator />
|
||||
<MenubarItem @click="citeStatusChanged()">
|
||||
<el-icon class="mr-2 h-4 w-4" :class="{ 'opacity-0': !isCiteStatus }">
|
||||
<ElIconCheck />
|
||||
</el-icon>
|
||||
微信外链转底部引用
|
||||
</MenubarItem>
|
||||
</MenubarContent>
|
||||
</MenubarMenu>
|
||||
<EditDropdown />
|
||||
<StyleDropdown />
|
||||
<HelpDropdown />
|
||||
</Menubar>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button variant="outline">
|
||||
<Paintbrush class="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="h-100 w-100 overflow-auto px-6" align="end">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<h2>主题</h2>
|
||||
<div class="grid grid-cols-3 justify-items-center gap-2">
|
||||
<Button
|
||||
v-for="{ label, value } in themeOptions"
|
||||
:key="value"
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': store.theme === value,
|
||||
}"
|
||||
@click="store.themeChanged(value)"
|
||||
>
|
||||
{{ label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>字体</h2>
|
||||
<div class="grid grid-cols-3 justify-items-center gap-2">
|
||||
<Button
|
||||
v-for="{ label, value } in fontFamilyOptions"
|
||||
:key="value"
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
:class="{ 'border-black dark:border-white': store.fontFamily === value }"
|
||||
@click="store.fontChanged(value)"
|
||||
>
|
||||
{{ label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>字号</h2>
|
||||
<div class="grid grid-cols-5 justify-items-center gap-2">
|
||||
<Button
|
||||
v-for="{ value, desc } in fontSizeOptions"
|
||||
:key="value"
|
||||
variant="outline"
|
||||
class="w-full"
|
||||
:class="{
|
||||
'border-black dark:border-white': store.fontSize === value,
|
||||
}"
|
||||
@click="store.sizeChanged(value)"
|
||||
>
|
||||
{{ desc }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>主题色</h2>
|
||||
<div class="grid grid-cols-3 justify-items-center gap-2">
|
||||
<Button
|
||||
v-for="{ label, value } in colorOptions"
|
||||
:key="value"
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': store.primaryColor === value,
|
||||
}"
|
||||
@click="store.colorChanged(value)"
|
||||
>
|
||||
<span
|
||||
class="mr-2 inline-block h-4 w-4 rounded-full"
|
||||
:style="{
|
||||
background: value,
|
||||
}"
|
||||
/>
|
||||
{{ label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>自定义主题色</h2>
|
||||
<div>
|
||||
<el-color-picker
|
||||
v-model="primaryColor"
|
||||
:teleported="false"
|
||||
show-alpha
|
||||
@change="store.colorChanged"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>代码块主题</h2>
|
||||
<div>
|
||||
<Select
|
||||
v-model="store.codeBlockTheme"
|
||||
@update:model-value="store.codeBlockThemeChanged"
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select a fruit" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="{ label, value } in codeBlockThemeOptions"
|
||||
:key="label"
|
||||
:value="value"
|
||||
>
|
||||
{{ label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>图注格式</h2>
|
||||
<div class="grid grid-cols-3 justify-items-center gap-2">
|
||||
<Button
|
||||
v-for="{ label, value } in legendOptions"
|
||||
:key="value"
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': store.legend === value,
|
||||
}"
|
||||
@click="store.legendChanged(value)"
|
||||
>
|
||||
{{ label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<h2>Mac 代码块</h2>
|
||||
<div class="grid grid-cols-5 justify-items-center gap-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': store.isMacCodeBlock,
|
||||
}"
|
||||
@click="!store.isMacCodeBlock && store.macCodeBlockChanged()"
|
||||
>
|
||||
开启
|
||||
</Button>
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': !store.isMacCodeBlock,
|
||||
}"
|
||||
@click="store.isMacCodeBlock && store.macCodeBlockChanged()"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>微信外链转底部引用</h2>
|
||||
<div class="grid grid-cols-5 justify-items-center gap-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': store.isCiteStatus,
|
||||
}"
|
||||
@click="!store.isCiteStatus && store.citeStatusChanged()"
|
||||
>
|
||||
开启
|
||||
</Button>
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': !store.isCiteStatus,
|
||||
}"
|
||||
@click="store.isCiteStatus && store.citeStatusChanged()"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>段落首行缩进</h2>
|
||||
<div class="grid grid-cols-5 justify-items-center gap-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': store.isUseIndent,
|
||||
}"
|
||||
@click="!store.isUseIndent && store.useIndentChanged()"
|
||||
>
|
||||
开启
|
||||
</Button>
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': !store.isUseIndent,
|
||||
}"
|
||||
@click="store.isUseIndent && store.useIndentChanged()"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>自定义 CSS 面板</h2>
|
||||
<div class="grid grid-cols-5 justify-items-center gap-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': displayStore.isShowCssEditor,
|
||||
}"
|
||||
@click="!displayStore.isShowCssEditor && customStyle()"
|
||||
>
|
||||
开启
|
||||
</Button>
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': !displayStore.isShowCssEditor,
|
||||
}"
|
||||
@click="displayStore.isShowCssEditor && customStyle()"
|
||||
>
|
||||
关闭
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>编辑区位置</h2>
|
||||
<div class="grid grid-cols-5 justify-items-center gap-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': store.isEditOnLeft,
|
||||
}"
|
||||
@click="!store.isEditOnLeft && store.toggleEditOnLeft()"
|
||||
>
|
||||
左侧
|
||||
</Button>
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': !store.isEditOnLeft,
|
||||
}"
|
||||
@click="store.isEditOnLeft && store.toggleEditOnLeft()"
|
||||
>
|
||||
右侧
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>模式</h2>
|
||||
<div class="grid grid-cols-5 justify-items-center gap-2">
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': !isDark,
|
||||
}"
|
||||
@click="store.toggleDark(false)"
|
||||
>
|
||||
<Sun class="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
class="w-full"
|
||||
variant="outline"
|
||||
:class="{
|
||||
'border-black dark:border-white': isDark,
|
||||
}"
|
||||
@click="store.toggleDark(true)"
|
||||
>
|
||||
<Moon class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h2>样式配置</h2>
|
||||
<div>
|
||||
<Button
|
||||
class="w-full"
|
||||
@click="store.resetStyleConfirm()"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Button variant="outline" class="mx-2" @click="copy">
|
||||
复制
|
||||
</Button>
|
||||
|
||||
<PostInfo />
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-container {
|
||||
padding: 10px 20px;
|
||||
.menubar {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdowns {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.el-dropdown {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.padding-left-3 {
|
||||
padding-left: 3em;
|
||||
}
|
||||
|
||||
// 添加边距影响了 divided 行的移入效果,此处做一个兼容处理
|
||||
.el-dropdown-menu__item--divided.padding-left-3 {
|
||||
position: relative;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 3em;
|
||||
height: 6px;
|
||||
background: white;
|
||||
}
|
||||
}
|
||||
|
||||
.format-item {
|
||||
.padding-left-3;
|
||||
width: 180px;
|
||||
|
||||
kbd {
|
||||
font-size: 0.75em;
|
||||
float: right;
|
||||
color: #666;
|
||||
}
|
||||
border: 1px solid #a8a8a8;
|
||||
padding: 1px 4px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,126 +1,108 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
title="插入表格"
|
||||
class="insert__dialog"
|
||||
:visible="visible"
|
||||
@close="$emit('close')"
|
||||
border
|
||||
>
|
||||
<el-row class="tb-options" type="flex" align="middle" :gutter="10">
|
||||
<el-col>
|
||||
行数:
|
||||
<el-input-number
|
||||
v-model="rowNum"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="100"
|
||||
size="small"
|
||||
></el-input-number>
|
||||
</el-col>
|
||||
<el-col>
|
||||
列数:
|
||||
<el-input-number
|
||||
v-model="colNum"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="100"
|
||||
size="small"
|
||||
></el-input-number>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<table style="border-collapse: collapse" class="input-table">
|
||||
<tr
|
||||
:class="{ 'head-style': row === 1 }"
|
||||
v-for="row in rowNum + 1"
|
||||
:key="row"
|
||||
>
|
||||
<td v-for="col in colNum" :key="col">
|
||||
<el-input
|
||||
align="center"
|
||||
v-model="tableData[`k_${row - 1}_${col - 1}`]"
|
||||
:placeholder="row === 1 ? '表头' : ''"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div slot="footer" class="dialog-footer">
|
||||
<el-button :type="btnType" @click="$emit('close')" plain>
|
||||
取 消
|
||||
</el-button>
|
||||
<el-button :type="btnType" @click="insertTable" plain> 确 定 </el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { useDisplayStore, useStore } from '@/stores'
|
||||
|
||||
<script>
|
||||
import { mapState, mapActions } from 'pinia'
|
||||
import { useStore } from '@/stores'
|
||||
import { createTable } from '@/utils'
|
||||
import { ref, toRaw } from 'vue'
|
||||
|
||||
import config from '@/assets/scripts/config'
|
||||
import { createTable } from '@/assets/scripts/util'
|
||||
const store = useStore()
|
||||
const displayStore = useDisplayStore()
|
||||
|
||||
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,
|
||||
})
|
||||
const { toggleShowInsertFormDialog } = displayStore
|
||||
|
||||
this.tableData = {}
|
||||
this.rowNum = 3
|
||||
this.colNum = 3
|
||||
this.editor.replaceSelection(`\n${table}\n`, `end`)
|
||||
this.$emit(`close`)
|
||||
this.editorRefresh()
|
||||
},
|
||||
...mapActions(useStore, [`editorRefresh`]),
|
||||
},
|
||||
const rowNum = ref(3)
|
||||
const colNum = ref(3)
|
||||
const tableData = ref<Record<string, string>>({})
|
||||
|
||||
function resetVal() {
|
||||
rowNum.value = 3
|
||||
colNum.value = 3
|
||||
tableData.value = {}
|
||||
}
|
||||
|
||||
// 插入表格
|
||||
function insertTable() {
|
||||
const table = createTable({
|
||||
rows: rowNum.value,
|
||||
cols: colNum.value,
|
||||
data: tableData.value,
|
||||
})
|
||||
toRaw(store.editor!).replaceSelection(`\n${table}\n`, `end`)
|
||||
resetVal()
|
||||
toggleShowInsertFormDialog()
|
||||
}
|
||||
|
||||
function onUpdate(val: boolean) {
|
||||
if (!val) {
|
||||
toggleShowInsertFormDialog(false)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/deep/ .el-dialog {
|
||||
width: 55%;
|
||||
min-height: 375px;
|
||||
min-width: 440px;
|
||||
}
|
||||
<template>
|
||||
<Dialog :open="displayStore.isShowInsertFormDialog" @update:open="onUpdate">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>插入表格</DialogTitle>
|
||||
</DialogHeader>
|
||||
<el-row class="tb-options" type="flex" align="middle" :gutter="10">
|
||||
<el-col :span="12">
|
||||
行数:
|
||||
<el-input-number
|
||||
v-model="rowNum"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="100"
|
||||
size="small"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
列数:
|
||||
<el-input-number
|
||||
v-model="colNum"
|
||||
controls-position="right"
|
||||
:min="1"
|
||||
:max="100"
|
||||
size="small"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<table style="border-collapse: collapse" class="input-table">
|
||||
<tr v-for="row in rowNum + 1" :key="row" :class="{ 'head-style': row === 1 }">
|
||||
<td v-for="col in colNum" :key="col">
|
||||
<el-input
|
||||
v-model="tableData[`k_${row - 1}_${col - 1}`]"
|
||||
align="center"
|
||||
:placeholder="row === 1 ? '表头' : ''"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="toggleShowInsertFormDialog(false)">
|
||||
取 消
|
||||
</Button>
|
||||
<Button @click="insertTable">
|
||||
确 定
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.tb-options {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.input-table ::v-deep .el-input__inner {
|
||||
.input-table :deep(.el-input__inner) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.head-style /deep/ .el-input__inner {
|
||||
background-color: #f2f2f2;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,126 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
id="menu"
|
||||
class="menu"
|
||||
v-show="visible"
|
||||
:style="`left: ${left}px;top: ${top}px;`"
|
||||
>
|
||||
<ul class="menu__group" v-for="(menuItem, index) in menu" :key="index">
|
||||
<li
|
||||
class="menu_item"
|
||||
v-for="{ key, text } in menuItem"
|
||||
:key="key"
|
||||
@mousedown="onMouseDown(key)"
|
||||
>
|
||||
{{ text }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
top: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
left: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
menu: [
|
||||
[
|
||||
{
|
||||
text: `上传图片`,
|
||||
key: `insertPic`,
|
||||
},
|
||||
{
|
||||
text: `插入表格`,
|
||||
key: `insertTable`,
|
||||
},
|
||||
{
|
||||
text: `恢复默认样式`,
|
||||
key: `resetStyle`,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
text: `导入 .md 文档`,
|
||||
key: `importMarkdown`,
|
||||
},
|
||||
{
|
||||
text: `导出 .md 文档`,
|
||||
key: `download`,
|
||||
},
|
||||
{
|
||||
text: `导出 .html`,
|
||||
key: `export`,
|
||||
},
|
||||
{
|
||||
text: `格式化`,
|
||||
key: `formatMarkdown`,
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onMouseDown(key) {
|
||||
this.$emit(`menuTick`, key)
|
||||
this.$emit(`closeMenu`)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.menu {
|
||||
position: absolute;
|
||||
border-radius: 4px;
|
||||
background-color: #ffffff;
|
||||
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.12), 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
.menu__group {
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
border-bottom: 1px solid #eeeeee;
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.menu_item {
|
||||
list-style: none;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 0 4px 24px;
|
||||
margin-top: 10px;
|
||||
min-width: 200px;
|
||||
line-height: 20px;
|
||||
font-size: 12px;
|
||||
color: #333333;
|
||||
cursor: pointer;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
::v-deep .el-upload {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
File diff suppressed because it is too large
Load Diff
@ -1,48 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
loading.value = false
|
||||
}, 100)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<transition name="fade" v-if="loading">
|
||||
<transition name="fade">
|
||||
<div
|
||||
v-if="loading"
|
||||
class="loading"
|
||||
:class="{
|
||||
loading_night: nightMode,
|
||||
}"
|
||||
>
|
||||
<strong>致力于让 Markdown 编辑更简单</strong>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'pinia'
|
||||
import { useStore } from '@/stores'
|
||||
|
||||
export default {
|
||||
name: `RunLoading`,
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(() => {
|
||||
this.loading = false
|
||||
}, 100)
|
||||
},
|
||||
computed: {
|
||||
...mapState(useStore, {
|
||||
nightMode: ({ nightMode }) => nightMode,
|
||||
}),
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@light-color: #303133;
|
||||
@light-background-color: #f2f2f2;
|
||||
@night-color: #bbbbbb;
|
||||
@night-background-color: #303133;
|
||||
|
||||
.loading {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 99999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -51,8 +34,7 @@ export default {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
font-size: 18px;
|
||||
color: @light-color;
|
||||
background-color: @light-background-color;
|
||||
background-color: var(--el-bg-color-page);
|
||||
|
||||
&::before {
|
||||
content: url('../assets/images/favicon.png');
|
||||
@ -62,11 +44,6 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.loading_night {
|
||||
color: @night-color;
|
||||
background-color: @night-background-color;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
|
26
src/components/ui/button/Button.vue
Normal file
26
src/components/ui/button/Button.vue
Normal file
@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Primitive, type PrimitiveProps } from 'radix-vue'
|
||||
import { type ButtonVariants, buttonVariants } from '.'
|
||||
|
||||
interface Props extends PrimitiveProps {
|
||||
variant?: ButtonVariants[`variant`]
|
||||
size?: ButtonVariants[`size`]
|
||||
class?: HTMLAttributes[`class`]
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
as: `button`,
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
35
src/components/ui/button/index.ts
Normal file
35
src/components/ui/button/index.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
export { default as Button } from './Button.vue'
|
||||
|
||||
export const buttonVariants = cva(
|
||||
`inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50`,
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: `bg-primary text-primary-foreground hover:bg-primary/90`,
|
||||
destructive:
|
||||
`bg-destructive text-destructive-foreground hover:bg-destructive/90`,
|
||||
outline:
|
||||
`border border-input bg-background hover:bg-accent hover:text-accent-foreground`,
|
||||
secondary:
|
||||
`bg-secondary text-secondary-foreground hover:bg-secondary/80`,
|
||||
ghost: `hover:bg-accent hover:text-accent-foreground`,
|
||||
link: `text-primary underline-offset-4 hover:underline`,
|
||||
},
|
||||
size: {
|
||||
default: `h-10 px-4 py-2`,
|
||||
xs: `h-7 rounded px-2`,
|
||||
sm: `h-9 rounded-md px-3`,
|
||||
lg: `h-11 rounded-md px-8`,
|
||||
icon: `h-10 w-10`,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: `default`,
|
||||
size: `default`,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
export type ButtonVariants = VariantProps<typeof buttonVariants>
|
15
src/components/ui/context-menu/ContextMenu.vue
Normal file
15
src/components/ui/context-menu/ContextMenu.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { ContextMenuRootEmits, ContextMenuRootProps } from 'radix-vue'
|
||||
import { ContextMenuRoot, useForwardPropsEmits } from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuRootProps>()
|
||||
const emits = defineEmits<ContextMenuRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</ContextMenuRoot>
|
||||
</template>
|
40
src/components/ui/context-menu/ContextMenuCheckboxItem.vue
Normal file
40
src/components/ui/context-menu/ContextMenuCheckboxItem.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuCheckboxItem,
|
||||
type ContextMenuCheckboxItemEmits,
|
||||
type ContextMenuCheckboxItemProps,
|
||||
ContextMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuCheckboxItemProps & { class?: HTMLAttributes[`class`] }>()
|
||||
const emits = defineEmits<ContextMenuCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuCheckboxItem
|
||||
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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
|
||||
<ContextMenuItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</ContextMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</ContextMenuCheckboxItem>
|
||||
</template>
|
36
src/components/ui/context-menu/ContextMenuContent.vue
Normal file
36
src/components/ui/context-menu/ContextMenuContent.vue
Normal file
@ -0,0 +1,36 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
ContextMenuContent,
|
||||
type ContextMenuContentEmits,
|
||||
type ContextMenuContentProps,
|
||||
ContextMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuContentProps & { class?: HTMLAttributes[`class`] }>()
|
||||
const emits = defineEmits<ContextMenuContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuPortal>
|
||||
<ContextMenuContent
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'z-50 min-w-32 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80 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 />
|
||||
</ContextMenuContent>
|
||||
</ContextMenuPortal>
|
||||
</template>
|
11
src/components/ui/context-menu/ContextMenuGroup.vue
Normal file
11
src/components/ui/context-menu/ContextMenuGroup.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuGroup, type ContextMenuGroupProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuGroup v-bind="props">
|
||||
<slot />
|
||||
</ContextMenuGroup>
|
||||
</template>
|
34
src/components/ui/context-menu/ContextMenuItem.vue
Normal file
34
src/components/ui/context-menu/ContextMenuItem.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
ContextMenuItem,
|
||||
type ContextMenuItemEmits,
|
||||
type ContextMenuItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuItemProps & { class?: HTMLAttributes[`class`], inset?: boolean }>()
|
||||
const emits = defineEmits<ContextMenuItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</ContextMenuItem>
|
||||
</template>
|
25
src/components/ui/context-menu/ContextMenuLabel.vue
Normal file
25
src/components/ui/context-menu/ContextMenuLabel.vue
Normal file
@ -0,0 +1,25 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ContextMenuLabel, type ContextMenuLabelProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuLabelProps & { class?: HTMLAttributes[`class`], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuLabel
|
||||
v-bind="delegatedProps"
|
||||
:class="
|
||||
cn('px-2 py-1.5 text-sm font-semibold text-foreground',
|
||||
inset && 'pl-8', props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</ContextMenuLabel>
|
||||
</template>
|
11
src/components/ui/context-menu/ContextMenuPortal.vue
Normal file
11
src/components/ui/context-menu/ContextMenuPortal.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuPortal, type ContextMenuPortalProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuPortalProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuPortal v-bind="props">
|
||||
<slot />
|
||||
</ContextMenuPortal>
|
||||
</template>
|
19
src/components/ui/context-menu/ContextMenuRadioGroup.vue
Normal file
19
src/components/ui/context-menu/ContextMenuRadioGroup.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ContextMenuRadioGroup,
|
||||
type ContextMenuRadioGroupEmits,
|
||||
type ContextMenuRadioGroupProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuRadioGroupProps>()
|
||||
const emits = defineEmits<ContextMenuRadioGroupEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuRadioGroup v-bind="forwarded">
|
||||
<slot />
|
||||
</ContextMenuRadioGroup>
|
||||
</template>
|
40
src/components/ui/context-menu/ContextMenuRadioItem.vue
Normal file
40
src/components/ui/context-menu/ContextMenuRadioItem.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuItemIndicator,
|
||||
ContextMenuRadioItem,
|
||||
type ContextMenuRadioItemEmits,
|
||||
type ContextMenuRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuRadioItemProps & { class?: HTMLAttributes[`class`] }>()
|
||||
const emits = defineEmits<ContextMenuRadioItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuRadioItem
|
||||
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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
|
||||
<ContextMenuItemIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</ContextMenuItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</ContextMenuRadioItem>
|
||||
</template>
|
20
src/components/ui/context-menu/ContextMenuSeparator.vue
Normal file
20
src/components/ui/context-menu/ContextMenuSeparator.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
ContextMenuSeparator,
|
||||
type ContextMenuSeparatorProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuSeparatorProps & { class?: HTMLAttributes[`class`] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuSeparator v-bind="delegatedProps" :class="cn('-mx-1 my-1 h-px bg-border', props.class)" />
|
||||
</template>
|
14
src/components/ui/context-menu/ContextMenuShortcut.vue
Normal file
14
src/components/ui/context-menu/ContextMenuShortcut.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 text-muted-foreground', props.class)">
|
||||
<slot />
|
||||
</span>
|
||||
</template>
|
19
src/components/ui/context-menu/ContextMenuSub.vue
Normal file
19
src/components/ui/context-menu/ContextMenuSub.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
ContextMenuSub,
|
||||
type ContextMenuSubEmits,
|
||||
type ContextMenuSubProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuSubProps>()
|
||||
const emits = defineEmits<ContextMenuSubEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuSub v-bind="forwarded">
|
||||
<slot />
|
||||
</ContextMenuSub>
|
||||
</template>
|
35
src/components/ui/context-menu/ContextMenuSubContent.vue
Normal file
35
src/components/ui/context-menu/ContextMenuSubContent.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
ContextMenuSubContent,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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>
|
||||
<ContextMenuSubContent
|
||||
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 />
|
||||
</ContextMenuSubContent>
|
||||
</template>
|
34
src/components/ui/context-menu/ContextMenuSubTrigger.vue
Normal file
34
src/components/ui/context-menu/ContextMenuSubTrigger.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import {
|
||||
ContextMenuSubTrigger,
|
||||
type ContextMenuSubTriggerProps,
|
||||
useForwardProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<ContextMenuSubTriggerProps & { class?: HTMLAttributes[`class`], inset?: boolean }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuSubTrigger
|
||||
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 focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
<ChevronRight class="ml-auto h-4 w-4" />
|
||||
</ContextMenuSubTrigger>
|
||||
</template>
|
13
src/components/ui/context-menu/ContextMenuTrigger.vue
Normal file
13
src/components/ui/context-menu/ContextMenuTrigger.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ContextMenuTrigger, type ContextMenuTriggerProps, useForwardProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<ContextMenuTriggerProps>()
|
||||
|
||||
const forwardedProps = useForwardProps(props)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ContextMenuTrigger v-bind="forwardedProps">
|
||||
<slot />
|
||||
</ContextMenuTrigger>
|
||||
</template>
|
14
src/components/ui/context-menu/index.ts
Normal file
14
src/components/ui/context-menu/index.ts
Normal file
@ -0,0 +1,14 @@
|
||||
export { default as ContextMenu } from './ContextMenu.vue'
|
||||
export { default as ContextMenuCheckboxItem } from './ContextMenuCheckboxItem.vue'
|
||||
export { default as ContextMenuContent } from './ContextMenuContent.vue'
|
||||
export { default as ContextMenuGroup } from './ContextMenuGroup.vue'
|
||||
export { default as ContextMenuItem } from './ContextMenuItem.vue'
|
||||
export { default as ContextMenuLabel } from './ContextMenuLabel.vue'
|
||||
export { default as ContextMenuRadioGroup } from './ContextMenuRadioGroup.vue'
|
||||
export { default as ContextMenuRadioItem } from './ContextMenuRadioItem.vue'
|
||||
export { default as ContextMenuSeparator } from './ContextMenuSeparator.vue'
|
||||
export { default as ContextMenuShortcut } from './ContextMenuShortcut.vue'
|
||||
export { default as ContextMenuSub } from './ContextMenuSub.vue'
|
||||
export { default as ContextMenuSubContent } from './ContextMenuSubContent.vue'
|
||||
export { default as ContextMenuSubTrigger } from './ContextMenuSubTrigger.vue'
|
||||
export { default as ContextMenuTrigger } from './ContextMenuTrigger.vue'
|
14
src/components/ui/dialog/Dialog.vue
Normal file
14
src/components/ui/dialog/Dialog.vue
Normal file
@ -0,0 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogRoot, type DialogRootEmits, type DialogRootProps, useForwardPropsEmits } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogRootProps>()
|
||||
const emits = defineEmits<DialogRootEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogRoot v-bind="forwarded">
|
||||
<slot />
|
||||
</DialogRoot>
|
||||
</template>
|
11
src/components/ui/dialog/DialogClose.vue
Normal file
11
src/components/ui/dialog/DialogClose.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogClose, type DialogCloseProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogCloseProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogClose v-bind="props">
|
||||
<slot />
|
||||
</DialogClose>
|
||||
</template>
|
50
src/components/ui/dialog/DialogContent.vue
Normal file
50
src/components/ui/dialog/DialogContent.vue
Normal file
@ -0,0 +1,50 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes[`class`] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80"
|
||||
/>
|
||||
<DialogContent
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 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-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="data-[state=open]:bg-accent ring-offset-background data-[state=open]:text-muted-foreground focus:ring-ring absolute right-4 top-4 rounded-sm opacity-70 transition-opacity disabled:pointer-events-none hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-offset-2"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogPortal>
|
||||
</template>
|
24
src/components/ui/dialog/DialogDescription.vue
Normal file
24
src/components/ui/dialog/DialogDescription.vue
Normal file
@ -0,0 +1,24 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { DialogDescription, type DialogDescriptionProps, useForwardProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes[`class`] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogDescription
|
||||
v-bind="forwardedProps"
|
||||
:class="cn('text-sm text-muted-foreground', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</DialogDescription>
|
||||
</template>
|
19
src/components/ui/dialog/DialogFooter.vue
Normal file
19
src/components/ui/dialog/DialogFooter.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{ class?: HTMLAttributes[`class`] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
16
src/components/ui/dialog/DialogHeader.vue
Normal file
16
src/components/ui/dialog/DialogHeader.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const props = defineProps<{
|
||||
class?: HTMLAttributes[`class`]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
59
src/components/ui/dialog/DialogScrollContent.vue
Normal file
59
src/components/ui/dialog/DialogScrollContent.vue
Normal file
@ -0,0 +1,59 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
type DialogContentEmits,
|
||||
type DialogContentProps,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<DialogContentProps & { class?: HTMLAttributes[`class`] }>()
|
||||
const emits = defineEmits<DialogContentEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogPortal>
|
||||
<DialogOverlay
|
||||
class="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80"
|
||||
>
|
||||
<DialogContent
|
||||
:class="
|
||||
cn(
|
||||
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
v-bind="forwarded"
|
||||
@pointer-down-outside="(event) => {
|
||||
const originalEvent = event.detail.originalEvent;
|
||||
const target = originalEvent.target as HTMLElement;
|
||||
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}"
|
||||
>
|
||||
<slot />
|
||||
|
||||
<DialogClose
|
||||
class="hover:bg-secondary absolute right-3 top-3 rounded-md p-0.5 transition-colors"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
<span class="sr-only">Close</span>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</DialogOverlay>
|
||||
</DialogPortal>
|
||||
</template>
|
29
src/components/ui/dialog/DialogTitle.vue
Normal file
29
src/components/ui/dialog/DialogTitle.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { DialogTitle, type DialogTitleProps, useForwardProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes[`class`] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTitle
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'text-lg font-semibold leading-none tracking-tight',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</DialogTitle>
|
||||
</template>
|
11
src/components/ui/dialog/DialogTrigger.vue
Normal file
11
src/components/ui/dialog/DialogTrigger.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DialogTrigger, type DialogTriggerProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<DialogTriggerProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DialogTrigger v-bind="props">
|
||||
<slot />
|
||||
</DialogTrigger>
|
||||
</template>
|
9
src/components/ui/dialog/index.ts
Normal file
9
src/components/ui/dialog/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export { default as Dialog } from './Dialog.vue'
|
||||
export { default as DialogClose } from './DialogClose.vue'
|
||||
export { default as DialogContent } from './DialogContent.vue'
|
||||
export { default as DialogDescription } from './DialogDescription.vue'
|
||||
export { default as DialogFooter } from './DialogFooter.vue'
|
||||
export { default as DialogHeader } from './DialogHeader.vue'
|
||||
export { default as DialogScrollContent } from './DialogScrollContent.vue'
|
||||
export { default as DialogTitle } from './DialogTitle.vue'
|
||||
export { default as DialogTrigger } from './DialogTrigger.vue'
|
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 { cn } from '@/lib/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuCheckboxItem,
|
||||
type DropdownMenuCheckboxItemEmits,
|
||||
type DropdownMenuCheckboxItemProps,
|
||||
DropdownMenuItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 h-3.5 w-3.5 flex items-center justify-center">
|
||||
<DropdownMenuItemIndicator>
|
||||
<Check class="h-4 w-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 { cn } from '@/lib/utils'
|
||||
import {
|
||||
DropdownMenuContent,
|
||||
type DropdownMenuContentEmits,
|
||||
type DropdownMenuContentProps,
|
||||
DropdownMenuPortal,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 { cn } from '@/lib/utils'
|
||||
import { DropdownMenuItem, type DropdownMenuItemProps, useForwardProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 { cn } from '@/lib/utils'
|
||||
import { DropdownMenuLabel, type DropdownMenuLabelProps, useForwardProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 { cn } from '@/lib/utils'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuItemIndicator,
|
||||
DropdownMenuRadioItem,
|
||||
type DropdownMenuRadioItemEmits,
|
||||
type DropdownMenuRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 h-3.5 w-3.5 flex 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 { cn } from '@/lib/utils'
|
||||
import {
|
||||
DropdownMenuSeparator,
|
||||
type DropdownMenuSeparatorProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 { cn } from '@/lib/utils'
|
||||
import {
|
||||
DropdownMenuSubContent,
|
||||
type DropdownMenuSubContentEmits,
|
||||
type DropdownMenuSubContentProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 { cn } from '@/lib/utils'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import {
|
||||
DropdownMenuSubTrigger,
|
||||
type DropdownMenuSubTriggerProps,
|
||||
useForwardProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 { default as DropdownMenu } from './DropdownMenu.vue'
|
||||
|
||||
export { default as DropdownMenuCheckboxItem } from './DropdownMenuCheckboxItem.vue'
|
||||
export { default as DropdownMenuContent } from './DropdownMenuContent.vue'
|
||||
export { default as DropdownMenuGroup } from './DropdownMenuGroup.vue'
|
||||
export { default as DropdownMenuItem } from './DropdownMenuItem.vue'
|
||||
export { default as DropdownMenuLabel } from './DropdownMenuLabel.vue'
|
||||
export { default as DropdownMenuRadioGroup } from './DropdownMenuRadioGroup.vue'
|
||||
export { default as DropdownMenuRadioItem } from './DropdownMenuRadioItem.vue'
|
||||
export { default as DropdownMenuSeparator } from './DropdownMenuSeparator.vue'
|
||||
export { default as DropdownMenuShortcut } from './DropdownMenuShortcut.vue'
|
||||
export { default as DropdownMenuSub } from './DropdownMenuSub.vue'
|
||||
export { default as DropdownMenuSubContent } from './DropdownMenuSubContent.vue'
|
||||
export { default as DropdownMenuSubTrigger } from './DropdownMenuSubTrigger.vue'
|
||||
export { default as DropdownMenuTrigger } from './DropdownMenuTrigger.vue'
|
||||
export { DropdownMenuPortal } from 'radix-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 { cn } from '@/lib/utils'
|
||||
import {
|
||||
HoverCardContent,
|
||||
type HoverCardContentProps,
|
||||
HoverCardPortal,
|
||||
useForwardProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
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 HoverCardContent } from './HoverCardContent.vue'
|
||||
export { default as HoverCardTrigger } from './HoverCardTrigger.vue'
|
35
src/components/ui/menubar/Menubar.vue
Normal file
35
src/components/ui/menubar/Menubar.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
MenubarRoot,
|
||||
type MenubarRootEmits,
|
||||
type MenubarRootProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<MenubarRootProps & { class?: HTMLAttributes[`class`] }>()
|
||||
const emits = defineEmits<MenubarRootEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarRoot
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'flex h-10 items-center gap-x-1 rounded-md border bg-background p-1',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</MenubarRoot>
|
||||
</template>
|
40
src/components/ui/menubar/MenubarCheckboxItem.vue
Normal file
40
src/components/ui/menubar/MenubarCheckboxItem.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Check } from 'lucide-vue-next'
|
||||
import {
|
||||
MenubarCheckboxItem,
|
||||
type MenubarCheckboxItemEmits,
|
||||
type MenubarCheckboxItemProps,
|
||||
MenubarItemIndicator,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<MenubarCheckboxItemProps & { class?: HTMLAttributes[`class`] }>()
|
||||
const emits = defineEmits<MenubarCheckboxItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarCheckboxItem
|
||||
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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
|
||||
<MenubarItemIndicator>
|
||||
<Check class="h-4 w-4" />
|
||||
</MenubarItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</MenubarCheckboxItem>
|
||||
</template>
|
43
src/components/ui/menubar/MenubarContent.vue
Normal file
43
src/components/ui/menubar/MenubarContent.vue
Normal file
@ -0,0 +1,43 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
MenubarContent,
|
||||
type MenubarContentProps,
|
||||
MenubarPortal,
|
||||
useForwardProps,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<MenubarContentProps & { class?: HTMLAttributes[`class`] }>(),
|
||||
{
|
||||
align: `start`,
|
||||
alignOffset: -4,
|
||||
sideOffset: 8,
|
||||
},
|
||||
)
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarPortal>
|
||||
<MenubarContent
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'z-50 min-w-48 overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in 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 />
|
||||
</MenubarContent>
|
||||
</MenubarPortal>
|
||||
</template>
|
11
src/components/ui/menubar/MenubarGroup.vue
Normal file
11
src/components/ui/menubar/MenubarGroup.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { MenubarGroup, type MenubarGroupProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<MenubarGroupProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarGroup v-bind="props">
|
||||
<slot />
|
||||
</MenubarGroup>
|
||||
</template>
|
35
src/components/ui/menubar/MenubarItem.vue
Normal file
35
src/components/ui/menubar/MenubarItem.vue
Normal file
@ -0,0 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
MenubarItem,
|
||||
type MenubarItemEmits,
|
||||
type MenubarItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<MenubarItemProps & { class?: HTMLAttributes[`class`], inset?: boolean }>()
|
||||
|
||||
const emits = defineEmits<MenubarItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarItem
|
||||
v-bind="forwarded"
|
||||
:class="cn(
|
||||
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
inset && 'pl-8',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<slot />
|
||||
</MenubarItem>
|
||||
</template>
|
13
src/components/ui/menubar/MenubarLabel.vue
Normal file
13
src/components/ui/menubar/MenubarLabel.vue
Normal file
@ -0,0 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import type { HTMLAttributes } from 'vue'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { MenubarLabel, type MenubarLabelProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<MenubarLabelProps & { class?: HTMLAttributes[`class`], inset?: boolean }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarLabel :class="cn('px-2 py-1.5 text-sm font-semibold', inset && 'pl-8', props.class)">
|
||||
<slot />
|
||||
</MenubarLabel>
|
||||
</template>
|
11
src/components/ui/menubar/MenubarMenu.vue
Normal file
11
src/components/ui/menubar/MenubarMenu.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { MenubarMenu, type MenubarMenuProps } from 'radix-vue'
|
||||
|
||||
const props = defineProps<MenubarMenuProps>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarMenu v-bind="props">
|
||||
<slot />
|
||||
</MenubarMenu>
|
||||
</template>
|
20
src/components/ui/menubar/MenubarRadioGroup.vue
Normal file
20
src/components/ui/menubar/MenubarRadioGroup.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
MenubarRadioGroup,
|
||||
type MenubarRadioGroupEmits,
|
||||
type MenubarRadioGroupProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
|
||||
const props = defineProps<MenubarRadioGroupProps>()
|
||||
|
||||
const emits = defineEmits<MenubarRadioGroupEmits>()
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarRadioGroup v-bind="forwarded">
|
||||
<slot />
|
||||
</MenubarRadioGroup>
|
||||
</template>
|
40
src/components/ui/menubar/MenubarRadioItem.vue
Normal file
40
src/components/ui/menubar/MenubarRadioItem.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Circle } from 'lucide-vue-next'
|
||||
import {
|
||||
MenubarItemIndicator,
|
||||
MenubarRadioItem,
|
||||
type MenubarRadioItemEmits,
|
||||
type MenubarRadioItemProps,
|
||||
useForwardPropsEmits,
|
||||
} from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<MenubarRadioItemProps & { class?: HTMLAttributes[`class`] }>()
|
||||
const emits = defineEmits<MenubarRadioItemEmits>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarRadioItem
|
||||
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 focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
|
||||
props.class,
|
||||
)"
|
||||
>
|
||||
<span class="absolute left-2 h-3.5 w-3.5 flex items-center justify-center">
|
||||
<MenubarItemIndicator>
|
||||
<Circle class="h-2 w-2 fill-current" />
|
||||
</MenubarItemIndicator>
|
||||
</span>
|
||||
<slot />
|
||||
</MenubarRadioItem>
|
||||
</template>
|
19
src/components/ui/menubar/MenubarSeparator.vue
Normal file
19
src/components/ui/menubar/MenubarSeparator.vue
Normal file
@ -0,0 +1,19 @@
|
||||
<script setup lang="ts">
|
||||
import { cn } from '@/lib/utils'
|
||||
import { MenubarSeparator, type MenubarSeparatorProps, useForwardProps } from 'radix-vue'
|
||||
import { computed, type HTMLAttributes } from 'vue'
|
||||
|
||||
const props = defineProps<MenubarSeparatorProps & { class?: HTMLAttributes[`class`] }>()
|
||||
|
||||
const delegatedProps = computed(() => {
|
||||
const { class: _, ...delegated } = props
|
||||
|
||||
return delegated
|
||||
})
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<MenubarSeparator :class=" cn('-mx-1 my-1 h-px bg-muted', props.class)" v-bind="forwardedProps" />
|
||||
</template>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user