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 文件<i class="el-icon-arrow-down el-icon--right"></i>
class="el-icon-upload" </span>
size="medium" <el-dropdown-menu slot="dropdown">
@click="$emit('show-dialog-upload-img')" <el-dropdown-item @click.native="refClick">
></i> <i class="el-icon-upload2" size="medium"></i>
</el-tooltip> 导入 .md
<!-- 导出 Markdown 文档 --> <input hidden type="file" ref="fileInput" accept=".md" />
<el-tooltip </el-dropdown-item>
class="header__item" <el-dropdown-item @click.native="$emit('download')">
:effect="effect" <i class="el-icon-download" size="medium"></i>
content="导出 Markdown 文档" 导出 .md
placement="bottom-start" </el-dropdown-item>
> <el-dropdown-item @click.native="$emit('export')">
<i <i class="el-icon-document" size="medium"></i>
class="el-icon-download" 导出 .html
size="medium" </el-dropdown-item>
@click="$emit('download')" <el-dropdown-item divided @click.native="themeChanged">
></i> <i
</el-tooltip> class="el-icon-check"
<!-- 导出 HTML --> :style="{ opacity: nightMode ? 1 : 0 }"
<el-tooltip ></i>
class="header__item" 暗黑模式
:effect="effect" </el-dropdown-item>
content="导出 HTML 页面" </el-dropdown-menu>
placement="bottom-start" </el-dropdown>
>
<i class="el-icon-document" size="medium" @click="$emit('export')"></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" <el-dropdown-menu slot="dropdown">
:effect="effect" <el-dropdown-item
content="重置样式" class="format-item"
placement="bottom-start" @click.native="$emit('addFormat', '**')"
> >
<i 加粗
class="el-icon-refresh" <kbd> Ctrl + B </kbd>
size="medium" </el-dropdown-item>
@click="showResetConfirm = true" <el-dropdown-item
></i> class="format-item"
</el-tooltip> @click.native="$emit('addFormat', '*')"
<!-- 插入表格 --> >
<el-tooltip 斜体
class="header__item header__item_last" <kbd> Ctrl + I </kbd>
:effect="effect" </el-dropdown-item>
content="插入表格" <el-dropdown-item
placement="bottom-start" class="format-item"
> @click.native="$emit('addFormat', '~~')"
<i >
class="el-icon-s-grid" 删除线
size="medium" <kbd> Alt + Shift + U </kbd>
@click="$emit('show-dialog-form')" </el-dropdown-item>
></i> <el-dropdown-item
</el-tooltip> class="format-item"
<el-select @click.native="$emit('addFormat', '[', ']()')"
v-model="selectFont" >
size="mini" 超链接
placeholder="选择字体" <kbd> Alt + Shift + K </kbd>
clearable </el-dropdown-item>
@change="fontChanged" <el-dropdown-item
> class="format-item"
<el-option @click.native="$emit('formatContent')"
v-for="font in config.builtinFonts" >
:style="{ fontFamily: font.value }" 格式化
:key="font.value" <kbd> Ctrl + Alt + L </kbd>
:label="font.label" </el-dropdown-item>
:value="font.value" <el-dropdown-item divided @click.native="statusChanged">
> <i
<span class="select-item-left">{{ font.label }}</span> class="el-icon-check"
<span class="select-item-right">Abc</span> :style="{ opacity: citeStatus ? 1 : 0 }"
</el-option> ></i>
</el-select> 微信外链转底部引用
<el-select </el-dropdown-item>
v-model="selectSize" </el-dropdown-menu>
size="mini" </el-dropdown>
placeholder="选择段落字号"
clearable <el-dropdown>
@change="sizeChanged" <span class="el-dropdown-link">
> 编辑<i class="el-icon-arrow-down el-icon--right"></i>
<el-option </span>
v-for="size in config.sizeOption" <el-dropdown-menu slot="dropdown">
:key="size.value" <el-dropdown-item @click.native="$emit('show-dialog-upload-img')">
:label="size.label" <i class="el-icon-upload" size="medium"></i>
:value="size.value" 上传图片
> </el-dropdown-item>
<span class="select-item-left">{{ size.label }}</span> <el-dropdown-item @click.native="$emit('show-dialog-form')">
<span class="select-item-right">{{ size.desc }}</span> <i class="el-icon-s-grid" size="medium"></i>
</el-option> 插入表格
</el-select> </el-dropdown-item>
<el-select </el-dropdown-menu>
v-model="selectColor" </el-dropdown>
size="mini"
placeholder="选择颜色" <el-dropdown>
clearable <span class="el-dropdown-link">
@change="colorChanged" 样式<i class="el-icon-arrow-down el-icon--right"></i>
> </span>
<el-option <el-dropdown-menu slot="dropdown">
v-for="color in config.colorOption" <el-dropdown-item class="padding-left-3">
:key="color.value" <el-dropdown placement="right" class="style-option-menu">
:label="color.label" <div class="el-dropdown-link">
:value="color.value" 字体
> <i class="el-icon-arrow-right el-icon--right"></i>
<span class="select-item-left">{{ color.label }}</span> </div>
<span class="select-item-right">{{ color.desc }}</span> <el-dropdown-menu slot="dropdown" style="width: 200px">
</el-option> <el-dropdown-item
</el-select> v-for="font in config.builtinFonts"
<el-select :style="{ fontFamily: font.value }"
v-model="selectCodeTheme" :key="font.value"
size="mini" :label="font.label"
placeholder="代码主题" :value="font.value"
@change="codeThemeChanged" @click.native="fontChanged(font.value)"
> >
<el-option <i
v-for="code in config.codeThemeOption" class="el-icon-check"
:key="code.value" :style="{ opacity: selectFont === font.value ? 1 : 0 }"
:label="code.label" ></i>
:value="code.value" <span>{{ font.label }}</span>
> <span class="select-item-right">Abc</span>
<span class="select-item-left">{{ code.label }}</span> </el-dropdown-item>
<span class="select-item-right">{{ code.desc }}</span> </el-dropdown-menu>
</el-option> </el-dropdown>
</el-select> </el-dropdown-item>
<el-tooltip content="自定义颜色" :effect="effect" placement="top"> <el-dropdown-item class="padding-left-3">
<el-color-picker <el-dropdown placement="right" class="style-option-menu">
v-model="selectColor" <div class="el-dropdown-link">
size="mini" 字号
show-alpha <i class="el-icon-arrow-right el-icon--right"></i>
@change="colorChanged" </div>
></el-color-picker> <el-dropdown-menu slot="dropdown" style="width: 200px">
</el-tooltip> <el-dropdown-item
<el-tooltip v-for="size in config.sizeOption"
content="微信外链自动转为文末引用" :key="size.value"
:effect="effect" :label="size.label"
placement="top" :value="size.value"
> @click.native="sizeChanged(size.value)"
<el-switch >
class="header__switch" <i
v-model="citeStatus" class="el-icon-check"
active-color="#67c23a" :style="{ opacity: selectSize === size.value ? 1 : 0 }"
inactive-color="#dcdfe6" ></i>
@change="statusChanged" <span>{{ size.label }}</span>
> <span class="select-item-right">{{ size.desc }}</span>
</el-switch> </el-dropdown-item>
</el-tooltip> </el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item class="padding-left-3">
<el-dropdown placement="right" class="style-option-menu">
<div class="el-dropdown-link">
颜色
<i class="el-icon-arrow-right el-icon--right"></i>
</div>
<el-dropdown-menu slot="dropdown" style="width: 200px">
<el-dropdown-item
v-for="color in config.colorOption"
:key="color.value"
:label="color.label"
:value="color.value"
@click.native="colorChanged(color.value)"
>
<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>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-dropdown-item>
<el-dropdown-item class="padding-left-3">
<el-dropdown placement="right" class="style-option-menu">
<div class="el-dropdown-link">
代码主题
<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"
:key="code.value"
:label="code.label"
:value="code.value"
@click.native="codeThemeChanged(code.value)"
>
<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>
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</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>
<el-dropdown>
<span class="el-dropdown-link">
帮助<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item @click.native="$emit('show-about-dialog')">
关于
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div> </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;
} }