2022-05-31 16:06:56 +08:00

732 lines
18 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div v-loading="vditorLoading">
<div class="article-editor">
<div class="editor-left">
<cite v-if="currentDarft">你正在编辑的是保存于 {{ file.updateDate }} 的草稿, 你也可以
<el-button type="text" style="color: #F56C6C" @click="deleteDraft">删除它</el-button>
</cite>
<el-header>
<div class="header-item">
<el-input class="articles-title" placeholder="标题" v-model="filename"/>
</div>
<div class="url-slug">
<div>{{ siteUrl + (alonePage ? '/o/' : '/s/') }}</div>
<edit-element
ref="editElement"
class="mark-setting-input"
:style="{minWidth: file.slug && file.slug.length > 0 ? '0px' : '35px'}"
v-model="file.slug"
placeholder="缩略名"
can-edit
></edit-element>
<a v-if="isEdit" class="view-icon" :title="'预览 '+filename" :href="siteUrl + (alonePage ? '/o/':'/s/')+(file.slug ? file.slug : file.id)" target="_blank">
<svg-icon class="wailian" icon-class="eye-open"></svg-icon>预览
</a>
</div>
</el-header>
<el-main>
<div id="vditor" :style="{maxHeight: clientHeight + 'px'}"></div>
</el-main>
</div>
<div class="editor-right">
<div class="operation">
<el-button class="release-button" type="warning" size="small" @click="saveDraft" :loading="updating">保存草稿
</el-button>
<el-button class="release-button" type="primary" size="small" @click="release" :loading="updating">发布文章
</el-button>
</div>
<div class="more-setting">
<p class="mark-setting-label">发布日期:</p>
<el-date-picker
v-model="file.uploadDate"
type="datetime"
size="medium"
value-format="yyyy-MM-dd HH:mm:ss"
format="yyyy-MM-dd HH:mm:ss">
</el-date-picker>
<p class="mark-setting-label">文章封面:</p>
<upload-image-input v-model="file.cover" placeholder="请输入文章封面大图的url"
:intput-autosize="{ minRows: 2, maxRows: 6}"/>
<div v-if="!alonePage">
<p class="mark-setting-label">分类:</p>
<multiple-tree-select
ref="selectTree"
style="width: 100%;"
size="medium"
placeholder="请选择分类"
:options="categories"
select-pack-up
v-model="file.categoryIds"
>
</multiple-tree-select>
</div>
<div v-if="!alonePage" class="setting-tags">
<p class="mark-setting-label">标签:</p>
<el-tag
v-for="tag in dynamicTags"
:key="tag.id"
closable
:disable-transitions="false"
@close="handleClose(tag)">
{{ tag }}
</el-tag>
<el-autocomplete
:class="[inputNewTagClass, inputValueExist?inputErrorClass:'']"
v-if="inputVisible"
v-model="inputValue"
ref="saveTagInput"
size="small"
:fetch-suggestions="querySearch"
:popper-append-to-body="false"
@select="handleSelect"
placeholder="请输入标签名"
@keyup.enter.native="handleInputConfirm"
@blur="handleInputConfirm"
>
</el-autocomplete>
<div v-if="inputValueExist" class="instruction-error">该标签已存在</div>
<el-button v-if="!inputVisible" class="button-new-tag" size="small" @click="showInput"> + 新增标签</el-button>
</div>
<!-- <p class="mark-setting-label">-->
<!-- 其他:-->
<!-- </p>-->
<!-- <el-button size="small" @click="moreSet">更多设置</el-button>-->
</div>
</div>
</div>
<el-drawer
:visible.sync="moreSetting"
:append-to-body='true'
:with-header="false">
<div class="more-setting">
<h2>更多设置</h2>
<p class="mark-setting-label">文章封面:</p>
<upload-image-input v-model="file.cover"/>
</div>
</el-drawer>
</div>
</template>
<script>
import markdownApi from '@/api/markdown-api'
import DirTree from "@/components/FileTree/DirTree"
import fileConfig from '@/utils/file-config'
import Vditor from 'vditor'
import "vditor/src/assets/less/index.less"
import categoryApi from "@/api/category";
import tagApi from "@/api/tag";
import EditElement from "@/views/markdown/EditElement";
import UploadImageInput from "@/components/input/UploadImageInput";
import MultipleTreeSelect from "@/components/select/MultipleTree";
let toolbar = [
'emoji',
'bold',
'italic',
'strike',
'link',
'|',
'list',
'ordered-list',
'check',
'outdent',
'indent',
'|',
'line',
'insert-before',
'insert-after',
'|',
'upload',
'record',
'table',
'|',
'undo',
'redo',
'|',
'edit-mode',
'code-theme',
'outline',
'preview',
'fullscreen',
{
name: 'more',
toolbar: [
'export',
'both',
'info',
'help',
],
}]
export default {
name: 'MarkdownEditor',
components: {
MultipleTreeSelect,
EditElement,
DirTree, Vditor, UploadImageInput
},
props: {
hasChange: {
type: Boolean,
defalut: false
},
alonePage: {
type: Boolean,
default: false
},
siteUrl: {
type: String,
default: ''
},
isEdit: {
type: Boolean,
default: false
}
},
data() {
return {
activeNames: ['1'],
file: {},
editStatus: false,
filename: '',
clientHeight: document.documentElement.clientHeight - 150,
updating: false,
storageLocation: '',
selectLocationVisible: false,
moreSetting: false,
contentEditor: '',
categories: [],
draft: false,
currentDarft: false,
tags: [],
dynamicTags: [],
inputVisible: false,
inputValue: '',
inputValueExist: false,
inputNewTagClass: 'input-new-tag',
inputErrorClass: 'input-error',
vditorLoading: false,
releaseTime: undefined,
}
},
mounted() {
this.getMarkdown()
this.categoryTree()
this.getTags()
},
watch: {
file() {
this.valueHasChanged()
},
filename(val) {
this.valueHasChanged()
this.$emit('onTitle', val)
},
storageLocation() {
this.valueHasChanged()
}
},
computed: {},
methods: {
reload() {
this.getMarkdown(true)
this.categoryTree()
},
querySearch(queryString, cb) {
let tags = this.tags
let results = queryString ? tags.filter((tag) => {
return (tag.name.toLowerCase().indexOf(queryString.toLowerCase()) === 0)
}) : []
// 调用 callback 返回建议列表的数据
results = results.map(res => {
return {value: res.name}
})
cb(results)
},
handleSelect() {
this.handleInputConfirm()
},
valueHasChanged() {
this.$emit('update:hasChange', true)
},
getMarkdown(isReload) {
this.vditorLoading = true
this.draft = false
if (this.$route.query.id) {
this.editStatus = true
markdownApi.getMarkdown({
mark: this.$route.query.id
}).then((res) => {
if (res.data.draft) {
this.file = res.data.draft
if (res.data.release) {
this.currentDarft = true
}
} else {
this.currentDarft = false
this.file = res.data
}
// 初始化编辑器
if (isReload) {
this.contentEditor.setValue(this.file.contentText)
this.vditorLoading = false
} else {
this.vditorInit(this.file.contentText)
}
this.content = this.file.contentText
this.filename = this.file.name.split('.md')[0]
// 加载标签
if (this.file.tagIds && this.file.tagIds.length > 0 && this.tags && this.tags.length > 0) {
this.loadDynamicTags()
}
}).then(() => {
const that = this
setTimeout(function () {
that.$emit('update:hasChange', false)
}, 200)
})
} else {
if (isReload) {
this.contentEditor.setValue('')
} else {
this.vditorInit('')
}
}
},
moreSet() {
this.moreSetting = true
},
categoryTree() {
categoryApi.categoryTree().then(res => {
this.categories = res.data
})
},
getTags() {
tagApi.tagList().then(res => {
this.tags = res.data
if (this.file.tagIds && this.file.tagIds.length > 0) {
this.loadDynamicTags()
}
})
},
loadDynamicTags() {
this.dynamicTags = this.tags.filter(tag => this.file.tagIds.includes(tag.id)).map(tag => tag.name)
},
handleClose(tag) {
this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1)
},
showInput() {
this.inputVisible = true
this.$nextTick(_ => {
this.$refs.saveTagInput.$refs.input.focus()
});
},
handleInputConfirm() {
const that = this
setTimeout(function () {
let inputValue = that.inputValue
if (inputValue) {
if (that.dynamicTags.includes(inputValue)) {
that.inputValueExist = true
return
}
that.dynamicTags.push(inputValue)
}
that.inputValueExist = false
that.inputVisible = false
that.inputValue = ''
}, 100)
},
vditorInit(content) {
this.contentEditor = new Vditor('vditor', {
height: this.clientHeight,
resize: {
"enable": true
},
toolbar: [
...toolbar
],
toolbarConfig: {
pin: true,
},
preview: {
mode: 'both',
hljs: {
lineNumber: true
},
},
cache: {
enable: false,
},
after: () => {
this.contentEditor.setValue(content)
this.vditorLoading = false
},
input: () => {
this.valueHasChanged()
},
upload: this.markdownImageUplaod()
})
},
markdownImageUplaod() {
return {
accept: 'image/*,.mp3, .wav, .rar',
headers: {
'jmal-token': this.$store.state.user.token,
'username': this.$store.state.user.name,
'userId': this.$store.state.user.userId
},
url: '/api/upload-markdown-image',
extraData: {
'username': this.$store.state.user.name,
'userId': this.$store.state.user.userId
},
fieldName: 'files',
filename(name) {
return name.replace(/[^(a-zA-Z0-9\u4e00-\u9fa5\.)]/g, '').replace(/[\?\\/:|<>\*\[\]\(\)\$%\{\}@~]/g, '').replace('/\\s/g', '')
},
format(files, responseText) {
let response = JSON.parse(responseText)
let succMap = {}
response.data.forEach(map => {
succMap[map.filename] = fileConfig.mardownPreviewUrl(map.filepath)
})
response.data = {}
response.data['succMap'] = succMap
return JSON.stringify(response)
},
error(msg) {
console.log('error', msg)
},
linkToImgUrl: '/api/upload-markdown-link-image',
linkToImgFormat(responseText) {
let response = JSON.parse(responseText)
response.data['url'] = fileConfig.mardownPreviewUrl(response.data.url)
return JSON.stringify(response)
},
}
},
save() {
if (this.editStatus) {
this.update()
} else {
this.add()
}
},
imageFilter() {
return true
},
checkParam() {
if (!this.filename) {
this.$message.warning("请输入文章标题")
return false
}
if (this.contentEditor.getValue().length < 2) {
this.$message.warning("请输入文章内容")
return false
}
return true
},
saveDraft() {
this.draft = true
this.update('保存草稿成功')
},
release() {
this.draft = false
this.update()
},
deleteDraft() {
this.$confirm('您确定要删除这份草稿吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
markdownApi.deleteDraft({
fileId: this.$route.query.id,
username: this.$store.state.user.name
}).then(() => {
this.$message({
message: "草稿已被删除",
type: 'success',
duration: 1000
})
this.reload()
})
})
},
// update
update(message) {
if (!this.checkParam()) {
return
}
this.updating = true
if (this.filename) {
const filename = this.filename + ".md"
markdownApi.editMarkdown({
fileId: this.$route.query.id,
userId: this.$store.state.user.userId,
username: this.$store.state.user.name,
filename: filename,
cover: this.file.cover,
isDraft: this.draft,
isRelease: this.file.release,
isAlonePage: this.alonePage,
slug: this.file.slug ? this.file.slug : this.filename,
categoryIds: this.file.categoryIds,
tagNames: this.dynamicTags,
currentDirectory: this.storageLocation,
contentText: this.contentEditor.getValue(),
html: this.contentEditor.getHTML(),
uploadDate: this.file.uploadDate,
}).then((res) => {
this.$emit('update:hasChange', false)
this.updating = false
this.$message({
message: message ? message : "发布成功",
type: 'success',
duration: 1000
})
if (this.draft) {
this.$router.push({path: this.$route.path, query: {operation: 'editor', id: res.data}})
this.$emit('onTitle', this.filename)
this.reload()
} else {
this.$emit('onRelease')
}
}).catch(() => {
this.updating = false
})
}
},
selectDir() {
this.$refs.dirTree.show()
},
confirmSelectDir() {
const node = this.$refs.dirTree.getSelectTreeNode()
this.storageLocation = node.path + node.name
this.$refs.dirTree.hide()
}
}
}
</script>
<style lang="scss" scoped>
@import "src/styles/setting";
/deep/ .el-main {
padding: 35px 20px 20px 20px;
.v-note-wrapper {
z-index: 200;
}
}
/deep/ .el-header {
height: 43px !important;
}
/deep/ .el-input-group {
width: unset;
}
/deep/ .header-item {
line-height: 40px;
padding: 5px 0 5px 0;
.release-button {
float: right;
margin-left: 10px;
}
.el-input {
width: 90%;
input[type='text'] {
font-weight: 900;
color: #000000 !important;
font-size: 16px;
border: none;
border-bottom: #ccc 1px solid;
border-radius: 0;
}
}
.title-label {
font-size: 16px;
font-weight: 500;
}
}
/deep/ .el-drawer__body {
.more-setting {
padding: 15px;
}
}
/deep/ .more-setting {
margin-top: 32px;
padding-top: 15px;
.el-input {
width: 100%;
}
.mark-setting-label {
font-weight: bold;
margin-top: 10px !important;
margin-bottom: 5px;
}
.mark-setting-input {
width: 100%;
}
.mark-description {
font-size: 12px;
font-weight: 100;
}
}
.article-editor {
min-width: 1160px;
margin: auto;
display: flex;
.editor-left {
width: 75%;
}
.editor-right {
margin-top: 15px;
width: 25%;
.operation {
float: right;
}
}
}
.setting-tags {
/deep/ .el-tag {
margin-left: 8px;
margin-top: 8px;
}
/deep/ .button-new-tag {
margin-left: 8px;
margin-top: 8px;
height: 32px;
line-height: 30px;
padding-top: 0;
padding-bottom: 0;
}
/deep/ .input-new-tag {
margin-left: 8px;
margin-top: 8px;
max-width: 94px;
vertical-align: bottom;
input {
padding: 0 5px;
}
}
}
/deep/ .input-error {
input {
border-color: #f56c6c;
}
}
.instruction-error {
color: #F56C6C;
font-size: 12px;
line-height: 1;
padding-top: 4px;
position: absolute;
margin-left: 8px;
}
/deep/ .el-autocomplete-suggestion {
li {
padding: 0;
background-color: #ecf5ff;
color: #409eff;
border: 1px solid #d9ecff;
border-radius: 4px;
box-sizing: border-box;
white-space: nowrap;
margin: 2px 4px;
text-align: center;
}
li:hover {
background-color: #3f9eff;
color: #fafdff;
}
}
/deep/ .el-autocomplete-suggestion__wrap {
padding: 2px 0;
}
@media (min-width: 1200px) {
.article-editor {
max-width: 1200px;
}
}
/deep/ .el-tree-node {
&:focus > .el-tree-node__content {
background-color: unset;
}
.el-tree-node__content:hover {
background-color: unset;
}
}
.category-tree {
max-height: 500px;
overflow: auto;
}
/deep/ .url-slug {
display: flex;
color: #aaaaaa;
.mark-setting-input {
font-size: 14px;
padding: 0 2px;
border-radius: 2px;
color: #666;
width: fit-content;
min-width: 35px;
background-color: #ecf5ff;
&:focus {
outline: none;
}
}
}
/deep/ .vditor-toolbar--pin {
top: unset;
}
/deep/ .el-scrollbar__wrap {
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.view-icon {
margin-left: 10px;
/deep/.svg-icon{
margin-right: 2px;
}
}
</style>