perf: rearrange the top layout (#163)

* perf: rearrange the top layout

* chore: toggle selection style

* chore: remove default option

* chore: adjust the theme button position

* feat: the MD import function is added

* feat: add formatting function

* perf: improved color picker experience

* perf: added a format shortcut key

* chore: reduce the margin difference
This commit is contained in:
YangQi 2022-08-01 17:47:02 +08:00 committed by GitHub
parent 9c720a1662
commit 3d26b03430
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 404 additions and 195 deletions

View File

@ -36,204 +36,259 @@
</span> </span>
</el-dialog> </el-dialog>
<div class="left-side"> <div class="left-side">
<!-- 图片上传 --> <el-dropdown>
<el-tooltip :effect="effect" content="上传图片" placement="bottom-start"> <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" size="medium"></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" size="medium"></i>
导出 .md
</el-dropdown-item>
<el-dropdown-item @click.native="$emit('export')">
<i class="el-icon-document" size="medium"></i>
导出 .html
</el-dropdown-item>
<el-dropdown-item divided @click.native="themeChanged">
<i <i
class="el-icon-upload" class="el-icon-check"
size="medium" :style="{ opacity: nightMode ? 1 : 0 }"
@click="$emit('show-dialog-upload-img')"
></i> ></i>
</el-tooltip> 暗黑模式
<!-- 导出 Markdown 文档 --> </el-dropdown-item>
<el-tooltip </el-dropdown-menu>
class="header__item" </el-dropdown>
:effect="effect"
content="导出 Markdown 文档" <el-dropdown>
placement="bottom-start" <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"
@click.native="$emit('addFormat', '**')"
> >
加粗
<kbd> Ctrl + B </kbd>
</el-dropdown-item>
<el-dropdown-item
class="format-item"
@click.native="$emit('addFormat', '*')"
>
斜体
<kbd> Ctrl + I </kbd>
</el-dropdown-item>
<el-dropdown-item
class="format-item"
@click.native="$emit('addFormat', '~~')"
>
删除线
<kbd> Alt + Shift + U </kbd>
</el-dropdown-item>
<el-dropdown-item
class="format-item"
@click.native="$emit('addFormat', '[', ']()')"
>
超链接
<kbd> Alt + Shift + K </kbd>
</el-dropdown-item>
<el-dropdown-item
class="format-item"
@click.native="$emit('formatContent')"
>
格式化
<kbd> Ctrl + Alt + L </kbd>
</el-dropdown-item>
<el-dropdown-item divided @click.native="statusChanged">
<i <i
class="el-icon-download" class="el-icon-check"
size="medium" :style="{ opacity: citeStatus ? 1 : 0 }"
@click="$emit('download')"
></i> ></i>
</el-tooltip> 微信外链转底部引用
<!-- 导出 HTML --> </el-dropdown-item>
<el-tooltip </el-dropdown-menu>
class="header__item" </el-dropdown>
:effect="effect"
content="导出 HTML 页面" <el-dropdown>
placement="bottom-start" <span class="el-dropdown-link">
> 编辑<i class="el-icon-arrow-down el-icon--right"></i>
<i class="el-icon-document" size="medium" @click="$emit('export')"></i> </span>
</el-tooltip> <el-dropdown-menu slot="dropdown">
<!-- 样式重置 --> <el-dropdown-item @click.native="$emit('show-dialog-upload-img')">
<el-tooltip <i class="el-icon-upload" size="medium"></i>
class="header__item" 上传图片
:effect="effect" </el-dropdown-item>
content="重置样式" <el-dropdown-item @click.native="$emit('show-dialog-form')">
placement="bottom-start" <i class="el-icon-s-grid" size="medium"></i>
> 插入表格
<i </el-dropdown-item>
class="el-icon-refresh" </el-dropdown-menu>
size="medium" </el-dropdown>
@click="showResetConfirm = true"
></i> <el-dropdown>
</el-tooltip> <span class="el-dropdown-link">
<!-- 插入表格 --> 样式<i class="el-icon-arrow-down el-icon--right"></i>
<el-tooltip </span>
class="header__item header__item_last" <el-dropdown-menu slot="dropdown">
:effect="effect" <el-dropdown-item class="padding-left-3">
content="插入表格" <el-dropdown placement="right" class="style-option-menu">
placement="bottom-start" <div class="el-dropdown-link">
> 字体
<i <i class="el-icon-arrow-right el-icon--right"></i>
class="el-icon-s-grid" </div>
size="medium" <el-dropdown-menu slot="dropdown" style="width: 200px">
@click="$emit('show-dialog-form')" <el-dropdown-item
></i>
</el-tooltip>
<el-select
v-model="selectFont"
size="mini"
placeholder="选择字体"
clearable
@change="fontChanged"
>
<el-option
v-for="font in config.builtinFonts" v-for="font in config.builtinFonts"
:style="{ fontFamily: font.value }" :style="{ fontFamily: font.value }"
:key="font.value" :key="font.value"
:label="font.label" :label="font.label"
:value="font.value" :value="font.value"
@click.native="fontChanged(font.value)"
> >
<span class="select-item-left">{{ font.label }}</span> <i
class="el-icon-check"
:style="{ opacity: selectFont === font.value ? 1 : 0 }"
></i>
<span>{{ font.label }}</span>
<span class="select-item-right">Abc</span> <span class="select-item-right">Abc</span>
</el-option> </el-dropdown-item>
</el-select> </el-dropdown-menu>
<el-select </el-dropdown>
v-model="selectSize" </el-dropdown-item>
size="mini" <el-dropdown-item class="padding-left-3">
placeholder="选择段落字号" <el-dropdown placement="right" class="style-option-menu">
clearable <div class="el-dropdown-link">
@change="sizeChanged" 字号
> <i class="el-icon-arrow-right el-icon--right"></i>
<el-option </div>
<el-dropdown-menu slot="dropdown" style="width: 200px">
<el-dropdown-item
v-for="size in config.sizeOption" v-for="size in config.sizeOption"
:key="size.value" :key="size.value"
:label="size.label" :label="size.label"
:value="size.value" :value="size.value"
@click.native="sizeChanged(size.value)"
> >
<span class="select-item-left">{{ size.label }}</span> <i
class="el-icon-check"
:style="{ opacity: selectSize === size.value ? 1 : 0 }"
></i>
<span>{{ size.label }}</span>
<span class="select-item-right">{{ size.desc }}</span> <span class="select-item-right">{{ size.desc }}</span>
</el-option> </el-dropdown-item>
</el-select> </el-dropdown-menu>
<el-select </el-dropdown>
v-model="selectColor" </el-dropdown-item>
size="mini" <el-dropdown-item class="padding-left-3">
placeholder="选择颜色" <el-dropdown placement="right" class="style-option-menu">
clearable <div class="el-dropdown-link">
@change="colorChanged" 颜色
> <i class="el-icon-arrow-right el-icon--right"></i>
<el-option </div>
<el-dropdown-menu slot="dropdown" style="width: 200px">
<el-dropdown-item
v-for="color in config.colorOption" v-for="color in config.colorOption"
:key="color.value" :key="color.value"
:label="color.label" :label="color.label"
:value="color.value" :value="color.value"
@click.native="colorChanged(color.value)"
> >
<span class="select-item-left">{{ color.label }}</span> <i
class="el-icon-check"
:style="{ opacity: selectColor === color.value ? 1 : 0 }"
></i>
<span>{{ color.label }}</span>
<span class="select-item-right">{{ color.desc }}</span> <span class="select-item-right">{{ color.desc }}</span>
</el-option> </el-dropdown-item>
</el-select> </el-dropdown-menu>
<el-select </el-dropdown>
v-model="selectCodeTheme" </el-dropdown-item>
size="mini" <el-dropdown-item class="padding-left-3">
placeholder="代码主题" <el-dropdown placement="right" class="style-option-menu">
@change="codeThemeChanged" <div class="el-dropdown-link">
> 代码主题
<el-option <i class="el-icon-arrow-right el-icon--right"></i>
</div>
<el-dropdown-menu slot="dropdown" style="width: 200px">
<el-dropdown-item
v-for="code in config.codeThemeOption" v-for="code in config.codeThemeOption"
:key="code.value" :key="code.value"
:label="code.label" :label="code.label"
:value="code.value" :value="code.value"
@click.native="codeThemeChanged(code.value)"
> >
<span class="select-item-left">{{ code.label }}</span> <i
class="el-icon-check"
:style="{ opacity: selectCodeTheme === code.value ? 1 : 0 }"
></i>
<span>{{ code.label }}</span>
<span class="select-item-right">{{ code.desc }}</span> <span class="select-item-right">{{ code.desc }}</span>
</el-option> </el-dropdown-item>
</el-select> </el-dropdown-menu>
<el-tooltip content="自定义颜色" :effect="effect" placement="top"> </el-dropdown>
</el-dropdown-item>
<el-dropdown-item
divided
class="padding-left-3"
@click.native="showPicker()"
>
自定义颜色
<el-color-picker <el-color-picker
v-model="selectColor"
size="mini"
show-alpha show-alpha
ref="colorPicker"
size="mini"
style="float: right; margin-top: 3px"
v-model="selectColor"
@change="colorChanged" @change="colorChanged"
></el-color-picker> ></el-color-picker>
</el-tooltip> </el-dropdown-item>
<el-tooltip <el-dropdown-item class="padding-left-3" @click.native="customStyle">
content="微信外链自动转为文末引用" 自定义 CSS
:effect="effect" </el-dropdown-item>
placement="top" <el-dropdown-item
divided
class="padding-left-3"
@click.native="showResetConfirm = true"
> >
<el-switch 重置
class="header__switch" </el-dropdown-item>
v-model="citeStatus" </el-dropdown-menu>
active-color="#67c23a" </el-dropdown>
inactive-color="#dcdfe6"
@change="statusChanged" <el-dropdown>
> <span class="el-dropdown-link">
</el-switch> 帮助<i class="el-icon-arrow-down el-icon--right"></i>
</el-tooltip> </span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="$emit('show-about-dialog')">
关于
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div> </div>
<div class="right-side"> <div class="right-side">
<el-tooltip
class="item"
:effect="effect"
content="自定义CSS样式"
placement="left"
>
<el-button
:type="btnType"
plain
size="medium"
icon="el-icon-setting"
@click="customStyle"
></el-button>
</el-tooltip>
<el-button <el-button
:type="btnType" :type="btnType"
plain plain
size="medium" size="medium"
@click="copy" @click="copy"
placement="bottom-start" placement="bottom-start"
>复制</el-button >复制
> </el-button>
<el-button <el-button
:type="btnType" :type="btnType"
plain plain
size="medium" size="medium"
@click="prePost" @click="prePost"
placement="bottom-start" placement="bottom-start"
>发布</el-button >发布
> </el-button>
<el-button
:type="btnType"
plain
size="medium"
class="about"
@click="$emit('show-about-dialog')"
>关于</el-button
>
<el-tooltip
:content="btnContent"
:effect="effect"
placement="bottom-start"
>
<div
class="mode__switch mode__switch_black"
v-if="nightMode"
@click="themeChanged"
></div>
<div class="mode__switch" v-else @click="themeChanged"></div>
</el-tooltip>
</div> </div>
<resetDialog <resetDialog
:showResetConfirm="showResetConfirm" :showResetConfirm="showResetConfirm"
@ -254,6 +309,7 @@ import config from '../../assets/scripts/config'
import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt' import DEFAULT_CSS_CONTENT from '@/assets/example/theme-css.txt'
import resetDialog from './resetDialog' import resetDialog from './resetDialog'
import { mapState, mapMutations } from 'vuex' import { mapState, mapMutations } from 'vuex'
export default { export default {
name: `editor-header`, name: `editor-header`,
data() { data() {
@ -300,6 +356,12 @@ export default {
}), }),
}, },
methods: { methods: {
refClick() {
this.$refs.fileInput.click()
},
showPicker() {
this.$refs.colorPicker.showPicker = true
},
prePost() { prePost() {
let auto = {} let auto = {}
try { try {
@ -335,6 +397,7 @@ export default {
fonts: fonts, fonts: fonts,
}) })
this.setCurrentFont(fonts) this.setCurrentFont(fonts)
this.selectFont = fonts
this.$emit(`refresh`) this.$emit(`refresh`)
}, },
sizeChanged(size) { sizeChanged(size) {
@ -345,6 +408,7 @@ export default {
theme: theme, theme: theme,
}) })
this.setCurrentSize(size) this.setCurrentSize(size)
this.selectSize = size
this.$emit(`refresh`) this.$emit(`refresh`)
}, },
colorChanged(color) { colorChanged(color) {
@ -355,14 +419,17 @@ export default {
theme: theme, theme: theme,
}) })
this.setCurrentColor(color) this.setCurrentColor(color)
this.selectColor = color
this.$emit(`refresh`) this.$emit(`refresh`)
}, },
codeThemeChanged(theme) { codeThemeChanged(theme) {
this.setCurrentCodeTheme(theme) this.setCurrentCodeTheme(theme)
this.selectCodeTheme = theme
this.$emit(`refresh`) this.$emit(`refresh`)
}, },
statusChanged(val) { statusChanged() {
this.setCiteStatus(val) this.citeStatus = !this.citeStatus
this.setCiteStatus(this.citeStatus)
this.$emit(`refresh`) this.$emit(`refresh`)
}, },
// //
@ -449,6 +516,19 @@ export default {
this.selectColor = this.currentColor this.selectColor = this.currentColor
this.selectCodeTheme = this.codeTheme this.selectCodeTheme = this.codeTheme
this.citeStatus = this.currentCiteStatus 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)
}
}
}, },
} }
</script> </script>
@ -457,15 +537,11 @@ export default {
.editor__header { .editor__header {
width: 100%; width: 100%;
} }
.header__item {
margin: 0 3px;
}
.header__item_last {
margin-right: 8px;
}
.header__switch { .header__switch {
margin-left: 8px; margin-left: 8px;
} }
.mode__switch { .mode__switch {
margin-left: 24px; margin-left: 24px;
margin-right: 24px; margin-right: 24px;
@ -475,10 +551,12 @@ export default {
background-size: cover; background-size: cover;
transition: all 0.3s; transition: all 0.3s;
} }
.mode__switch_black { .mode__switch_black {
background: url('../../assets/images/light.png') no-repeat; background: url('../../assets/images/light.png') no-repeat;
background-size: cover; background-size: cover;
} }
.top { .top {
height: 60px; height: 60px;
padding: 10px 20px; padding: 10px 20px;
@ -486,14 +564,17 @@ export default {
align-items: center; align-items: center;
margin-right: 0; margin-right: 0;
} }
.el-select { .el-select {
margin-right: 12px; margin-right: 12px;
} }
.left-side { .left-side {
display: flex; display: flex;
align-items: center; align-items: center;
flex: 1; flex: 1;
} }
.right-side { .right-side {
display: flex; display: flex;
align-items: center; align-items: center;
@ -513,4 +594,53 @@ export default {
color: #8492a6; color: #8492a6;
font-size: 13px; font-size: 13px;
} }
.el-dropdown {
margin: 0 10px;
}
.el-dropdown-link {
cursor: pointer;
}
.style-option-menu {
margin: 0;
width: 150px;
.el-dropdown-link {
display: flex;
align-items: center;
justify-content: space-between;
}
}
.padding-left-3 {
padding-left: 3em;
}
// divided
.el-dropdown-menu__item--divided.padding-left-3 {
position: relative;
&::after {
content: '';
position: absolute;
left: 0;
top: 0;
width: 3em;
height: 6px;
background: white;
}
}
.format-item {
.padding-left-3;
width: 180px;
kbd {
font-size: 0.75em;
float: right;
color: #666;
}
}
</style> </style>

View File

@ -1,11 +1,20 @@
<template> <template>
<div class="container" :class="{ container_night: nightMode }"> <div
class="container"
:class="{ container_night: nightMode }"
@keydown.alt.shift.k="addFormat('[', ']()')"
@keydown.alt.shift.u="addFormat('~~')"
@keydown.ctrl.alt.l="formatContent()"
>
<el-container> <el-container>
<el-header class="editor__header"> <el-header class="editor__header">
<editor-header <editor-header
ref="header" ref="header"
@addFormat="addFormat"
@formatContent="formatContent"
@refresh="onEditorRefresh" @refresh="onEditorRefresh"
@cssChanged="cssChanged" @cssChanged="cssChanged"
@import-md="importMD"
@download="downloadEditorContent" @download="downloadEditorContent"
@export="exportEditorContent" @export="exportEditorContent"
@showCssEditor="showCssEditor = !showCssEditor" @showCssEditor="showCssEditor = !showCssEditor"
@ -110,12 +119,12 @@ import {
customCssWithTemplate, customCssWithTemplate,
checkImage, checkImage,
} from '../assets/scripts/util' } from '../assets/scripts/util'
import { toBase64 } from '../assets/scripts/util' import { toBase64 } from '../assets/scripts/util'
import fileApi from '../api/file' import fileApi from '../api/file'
import { mapState, mapMutations } from 'vuex'
require(`codemirror/mode/javascript/javascript`) require(`codemirror/mode/javascript/javascript`)
import { mapState, mapMutations } from 'vuex'
export default { export default {
data() { data() {
return { return {
@ -446,6 +455,64 @@ export default {
this.isCoping = false this.isCoping = false
}, 800) }, 800)
}, },
//
addFormat(before, after = before) {
const { head, anchor } = this.editor.doc.sel.ranges[0]
let start
let end
//
if (head.line === anchor.line) {
if (head.ch < anchor.ch) {
start = head
end = anchor
} else {
start = anchor
end = head
}
} else if (head.line < anchor.line) {
start = head
end = anchor
} else {
start = head
end = anchor
}
const rows = []
let row = ``
for (const c of this.editor.getValue()) {
if (c === `\n`) {
rows.push(row)
row = ``
} else {
row += c
}
}
rows.push(row)
let txt = ``
for (let i = 0; i < rows.length; i++) {
const row = rows[i]
for (let j = 0; j < row.length; j++) {
if (i === start.line && j === start.ch) {
txt += before
}
if (i === end.line && j === end.ch) {
txt += after
}
txt += row[j]
}
//*
if (i === end.line && row.length === end.ch) {
txt += after
}
txt += `\n`
}
this.editor.setValue(txt)
},
importMD(md) {
this.editor.setValue(md)
this.onEditorRefresh()
},
// //
downloadEditorContent() { downloadEditorContent() {
downloadMD(this.editor.getValue(0)) downloadMD(this.editor.getValue(0))
@ -563,30 +630,37 @@ export default {
padding-top: 12px; padding-top: 12px;
overflow: hidden; overflow: hidden;
} }
.el-main { .el-main {
transition: all 0.3s; transition: all 0.3s;
padding: 0; padding: 0;
margin: 20px; margin: 20px;
margin-top: 0; margin-top: 0;
} }
.container { .container {
transition: all 0.3s; transition: all 0.3s;
} }
.textarea-wrapper { .textarea-wrapper {
height: 100%; height: 100%;
} }
.preview-wrapper_night { .preview-wrapper_night {
overflow-y: inherit; overflow-y: inherit;
position: relative; position: relative;
left: -3px; left: -3px;
.preview { .preview {
background-color: #fff; background-color: #fff;
} }
} }
#output-wrapper { #output-wrapper {
position: relative; position: relative;
user-select: text; user-select: text;
} }
.loading-mask { .loading-mask {
position: absolute; position: absolute;
top: 50%; top: 50%;
@ -598,6 +672,7 @@ export default {
font-size: 15px; font-size: 15px;
color: gray; color: gray;
background-color: #1e1e1e; background-color: #1e1e1e;
.loading__img { .loading__img {
position: absolute; position: absolute;
left: 50%; left: 50%;
@ -608,6 +683,7 @@ export default {
background: url('../assets/images/favicon.png') no-repeat; background: url('../assets/images/favicon.png') no-repeat;
background-size: cover; background-size: cover;
} }
span { span {
position: absolute; position: absolute;
left: 50%; left: 50%;
@ -615,11 +691,13 @@ export default {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
} }
.bounceInRight { .bounceInRight {
animation-name: bounceInRight; animation-name: bounceInRight;
animation-duration: 1s; animation-duration: 1s;
animation-fill-mode: both; animation-fill-mode: both;
} }
/deep/ .preview-table { /deep/ .preview-table {
border-spacing: 0px; border-spacing: 0px;
} }
@ -650,6 +728,7 @@ export default {
transform: none; transform: none;
} }
} }
.codeMirror-wrapper { .codeMirror-wrapper {
overflow-x: auto; overflow-x: auto;
} }