|
|
|
@ -0,0 +1,291 @@
|
|
|
|
|
<template>
|
|
|
|
|
<div class = "flow-container">
|
|
|
|
|
<div class="post-editor">
|
|
|
|
|
<!-- 顶部工具栏 -->
|
|
|
|
|
<div class="toolbar">
|
|
|
|
|
<el-button :icon="ArrowLeft" circle></el-button>
|
|
|
|
|
<el-select v-model="selectedCategory" placeholder="选择分区" class="category-select" size="large">
|
|
|
|
|
<el-option
|
|
|
|
|
v-for="item in categories"
|
|
|
|
|
:key="item.value"
|
|
|
|
|
:label="item.label"
|
|
|
|
|
:value="item.value"
|
|
|
|
|
/>
|
|
|
|
|
</el-select>
|
|
|
|
|
|
|
|
|
|
<div class="icon-group">
|
|
|
|
|
<el-button :icon="Document " circle size="large"></el-button>
|
|
|
|
|
<el-button :icon="Picture" circle size="large" @click="handlePicture"></el-button>
|
|
|
|
|
<el-button :icon="ArrowLeft" circle size="large"></el-button>
|
|
|
|
|
<el-button :icon="ArrowRight" circle size="large"></el-button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class="action-buttons">
|
|
|
|
|
<button class = "btn-primary btn">保存草稿</button>
|
|
|
|
|
<button class = "btn-secondary btn">定时发布</button>
|
|
|
|
|
<button class = "btn-primary btn">发布帖子</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<!-- 图片上传对话框 -->
|
|
|
|
|
<el-dialog v-model="uploadPictureDialog" title="上传图片" :show-close="false">
|
|
|
|
|
<el-upload
|
|
|
|
|
:auto-upload="false"
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
:on-change="handleImageUpload"
|
|
|
|
|
accept="image/*"
|
|
|
|
|
class="upload-dialog"
|
|
|
|
|
>
|
|
|
|
|
<img v-if="previewUrl" :src="previewUrl" class="avatar-preview"/>
|
|
|
|
|
<el-icon size = 30px v-else><Plus/></el-icon>
|
|
|
|
|
</el-upload>
|
|
|
|
|
<template #footer>
|
|
|
|
|
<button class = "btn btn-primary" @click="uploadPictureDialog = false,previewUrl = ''">取消</button>
|
|
|
|
|
<button class = "btn btn-primary" @click="uploadPictureDialog = false ,previewUrl = ''">确定</button>
|
|
|
|
|
</template>
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
|
|
|
|
<!-- 编辑器主体区域 -->
|
|
|
|
|
<div class="editor-body">
|
|
|
|
|
<!-- 左侧Markdown编辑框 -->
|
|
|
|
|
<div class="editor-pane card">
|
|
|
|
|
<el-input
|
|
|
|
|
v-model="title"
|
|
|
|
|
placeholder="请输入文章标题"
|
|
|
|
|
class="editor-title"
|
|
|
|
|
/>
|
|
|
|
|
<el-input
|
|
|
|
|
id="markdown-editor"
|
|
|
|
|
v-model="markdownText"
|
|
|
|
|
type="textarea"
|
|
|
|
|
:autosize="{ minRows: 20 }"
|
|
|
|
|
placeholder="正文"
|
|
|
|
|
class="markdown-input"
|
|
|
|
|
ref="elInputRef"
|
|
|
|
|
@click="saveCursor"
|
|
|
|
|
@keyup="saveCursor"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<!-- 右侧Markdown预览框 -->
|
|
|
|
|
<div class="preview-pane card">
|
|
|
|
|
<div class="editor-title">{{ title || '请输入文章标题' }}</div>
|
|
|
|
|
<div
|
|
|
|
|
class="markdown-preview"
|
|
|
|
|
v-html="DOMPurify.sanitize(compiledMarkdown)"
|
|
|
|
|
></div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
|
|
|
|
import { ref, watch } from 'vue'
|
|
|
|
|
import { marked } from 'marked'
|
|
|
|
|
import hljs from 'highlight.js'
|
|
|
|
|
import type { UploadFile } from 'element-plus'
|
|
|
|
|
import { markedHighlight } from 'marked-highlight'
|
|
|
|
|
import 'highlight.js/styles/github.css'
|
|
|
|
|
import { ArrowLeft, Document, Picture, ArrowRight } from '@element-plus/icons-vue'
|
|
|
|
|
import DOMPurify from 'dompurify'
|
|
|
|
|
|
|
|
|
|
const title = ref('')
|
|
|
|
|
const markdownText = ref('')
|
|
|
|
|
const elInputRef = ref();
|
|
|
|
|
const compiledMarkdown = ref('')
|
|
|
|
|
const selectedCategory = ref(null)
|
|
|
|
|
const categories = [
|
|
|
|
|
{ label: '分区1', value: 1 },
|
|
|
|
|
{ label: '分区2', value: 2 },
|
|
|
|
|
{ label: '分区3', value: 3 },
|
|
|
|
|
{ label: '分区4', value: 4 },
|
|
|
|
|
{ label: '分区5', value: 5 },
|
|
|
|
|
{ label: '分区6', value: 6 },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
marked.use(
|
|
|
|
|
markedHighlight({
|
|
|
|
|
langPrefix: 'hljs language-',
|
|
|
|
|
highlight: (code: string, lang: string) => {
|
|
|
|
|
return hljs.highlightAuto(code, [lang]).value
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
watch(markdownText, async () => {
|
|
|
|
|
compiledMarkdown.value = await marked.parse(markdownText.value)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
//处理图片上传
|
|
|
|
|
const uploadPictureDialog = ref(false)
|
|
|
|
|
const handlePicture = () => {
|
|
|
|
|
uploadPictureDialog.value = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const previewUrl = ref('');
|
|
|
|
|
function handleImageUpload(rawFile: UploadFile) {
|
|
|
|
|
const file = rawFile.raw; // 获取原始 File 对象
|
|
|
|
|
if (!file) return;
|
|
|
|
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
reader.onload = () => {
|
|
|
|
|
const base64 = reader.result as string;
|
|
|
|
|
insertAtCursor(`\n\n`);
|
|
|
|
|
previewUrl.value = base64;
|
|
|
|
|
};
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//图片插入Markdown的函数,插入规则为插入光标所在处
|
|
|
|
|
let cursorPos = 0;
|
|
|
|
|
//记录光标位置
|
|
|
|
|
function saveCursor() {
|
|
|
|
|
const textarea = elInputRef.value?.$el.querySelector('textarea');
|
|
|
|
|
if (textarea) {
|
|
|
|
|
cursorPos = textarea.selectionStart;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
//插入Markdown
|
|
|
|
|
function insertAtCursor(text: string) {
|
|
|
|
|
const textarea = elInputRef.value?.$el.querySelector('textarea');
|
|
|
|
|
if (!textarea) return;
|
|
|
|
|
|
|
|
|
|
const current = markdownText.value;
|
|
|
|
|
markdownText.value =
|
|
|
|
|
current.slice(0, cursorPos) + text + current.slice(cursorPos);
|
|
|
|
|
}
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
|
|
.post-editor {
|
|
|
|
|
position:fixed;
|
|
|
|
|
top:0;
|
|
|
|
|
left:0;
|
|
|
|
|
right:0;
|
|
|
|
|
height:60px;
|
|
|
|
|
padding-right:50px;
|
|
|
|
|
padding-left: 16px;
|
|
|
|
|
padding-bottom: 16px;
|
|
|
|
|
padding-top:16px;
|
|
|
|
|
background: #ead1fb;
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
|
|
|
|
.toolbar {
|
|
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 12px;
|
|
|
|
|
margin-bottom: 12px;
|
|
|
|
|
|
|
|
|
|
.el-button{
|
|
|
|
|
height: 55px;
|
|
|
|
|
width:55px;
|
|
|
|
|
|
|
|
|
|
::v-deep(.el-icon)
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
font-size:24px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.category-select {
|
|
|
|
|
width: 300px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.icon-group {
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
margin-left: 10px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-input {
|
|
|
|
|
flex-grow: 1;
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.title-length {
|
|
|
|
|
color: #999;
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.action-buttons {
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
display: flex;
|
|
|
|
|
gap: 6px;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* 图片上传窗口样式 */
|
|
|
|
|
.upload-dialog {
|
|
|
|
|
min-width: 95%;
|
|
|
|
|
min-height:300px;
|
|
|
|
|
margin:10px;
|
|
|
|
|
margin-left:20px;
|
|
|
|
|
border: 1px dashed #d9d9d9;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
display: flex;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
align-self: center;
|
|
|
|
|
|
|
|
|
|
.el-dialog__header {
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
border-bottom: 1px solid #e0e0e0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.el-dialog__body {
|
|
|
|
|
border: 1px dashed #d9d9d9;
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.avatar-preview {
|
|
|
|
|
width: 900px;
|
|
|
|
|
height: 500px;
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.editor-body {
|
|
|
|
|
display: flex;
|
|
|
|
|
flex-direction:row;
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
gap: 16px;
|
|
|
|
|
margin-top:20px;
|
|
|
|
|
min-height: 100%;
|
|
|
|
|
|
|
|
|
|
.editor-pane,
|
|
|
|
|
.preview-pane {
|
|
|
|
|
width:50%;
|
|
|
|
|
height:auto;
|
|
|
|
|
padding: 16px;
|
|
|
|
|
background: #fff;
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
box-shadow: 0 0 4px rgba(0, 0, 0, 0.05);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.editor-title {
|
|
|
|
|
height:50px;
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
font-size: 25px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.markdown-input {
|
|
|
|
|
font-size:20px;
|
|
|
|
|
width: 100%;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.markdown-preview {
|
|
|
|
|
padding-top: 10px;
|
|
|
|
|
|
|
|
|
|
img {
|
|
|
|
|
max-width: 50%;
|
|
|
|
|
height: auto;
|
|
|
|
|
display: block;
|
|
|
|
|
margin: 0.5rem 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
</style>
|