760 lines
20 KiB
Vue
760 lines
20 KiB
Vue
<template>
|
||
<div class="editor-page">
|
||
<!-- 页面头部 -->
|
||
<div class="page-header">
|
||
<div class="header-left">
|
||
<div class="title-section">
|
||
<i class="el-icon-edit-outline title-icon"></i>
|
||
<h1 class="page-title">wangeditor 富文本文档编辑器</h1>
|
||
</div>
|
||
<p class="page-description">使用富文本编辑器创建和管理文档,支持文本格式化、链接、列表等功能</p>
|
||
</div>
|
||
<div class="header-actions">
|
||
<el-button type="info" icon="el-icon-view" @click="handlePreview">
|
||
预览文档
|
||
</el-button>
|
||
<el-button type="success" icon="el-icon-document" @click="handleSave">
|
||
保存文档
|
||
</el-button>
|
||
<el-button type="warning" icon="el-icon-upload2" @click="handelAddDocument">
|
||
发布文档
|
||
</el-button>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 文档配置区域 -->
|
||
<div class="config-section">
|
||
<div class="section-header">
|
||
<i class="el-icon-setting"></i>
|
||
<span>文档配置</span>
|
||
</div>
|
||
|
||
<div class="config-content">
|
||
<div class="config-row">
|
||
<div class="config-item">
|
||
<label class="required">文档标题</label>
|
||
<el-input
|
||
v-model="addParams.title"
|
||
placeholder="请输入文档标题"
|
||
@input="handleAutoSave"
|
||
size="medium"
|
||
/>
|
||
</div>
|
||
|
||
<div class="config-item">
|
||
<label class="required">文档类型</label>
|
||
<el-select
|
||
v-model="addParams.type"
|
||
placeholder="请选择文档类型"
|
||
@change="handleAutoSave"
|
||
size="medium"
|
||
>
|
||
<el-option
|
||
v-for="item in TypeList"
|
||
:key="item.value"
|
||
:label="item.label"
|
||
:value="item.value">
|
||
</el-option>
|
||
</el-select>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 编辑器区域 -->
|
||
<div class="editor-section">
|
||
<div class="section-header">
|
||
<i class="el-icon-edit-outline"></i>
|
||
<span>富文本编辑器</span>
|
||
</div>
|
||
|
||
<div class="editor-container">
|
||
<Toolbar
|
||
:editor="editor"
|
||
:defaultConfig="toolbarConfig"
|
||
:mode="mode"
|
||
style="border-bottom: 1px solid #ccc"
|
||
/>
|
||
<Editor
|
||
style="height: 600px; overflow-y: hidden;"
|
||
v-model="addParams.content"
|
||
:defaultConfig="editorConfig"
|
||
:mode="mode"
|
||
@onCreated="onCreated"
|
||
@onChange="handleEditorChange"
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 预览文档对话框 -->
|
||
<el-dialog
|
||
title="文档预览"
|
||
:visible.sync="previewVisible"
|
||
width="80%"
|
||
:before-close="handlePreviewClose"
|
||
class="preview-dialog"
|
||
>
|
||
<div class="preview-content">
|
||
<div v-html="addParams.content"></div>
|
||
|
||
<div v-if="!addParams.content" class="preview-empty">
|
||
<i class="el-icon-document"></i>
|
||
<p>暂无内容</p>
|
||
</div>
|
||
</div>
|
||
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button @click="previewVisible = false">关闭</el-button>
|
||
<el-button type="primary" @click="handlePrintPreview">打印预览</el-button>
|
||
</span>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import Vue from "vue";
|
||
import E from "wangeditor";
|
||
import { addDocument } from '../api/documentManagement'
|
||
|
||
export default Vue.extend({
|
||
components: { Editor, Toolbar },
|
||
data() {
|
||
return {
|
||
editor: null,
|
||
html: '<p>hello</p>',
|
||
toolbarConfig: {
|
||
excludeKeys: [],
|
||
insertKeys: {
|
||
index: 0,
|
||
keys: ['bold', 'italic', 'underline', 'through', 'code', 'sub', 'sup', 'clearStyle']
|
||
}
|
||
},
|
||
editorConfig: {
|
||
placeholder: '请在此输入文档内容...',
|
||
MENU_CONF: {}
|
||
},
|
||
mode: 'default',
|
||
addParams:{
|
||
title:'',
|
||
content:'',
|
||
type:'',
|
||
lang:"",
|
||
imageUrl: '',
|
||
},
|
||
lastSaveTime: '',
|
||
autoSaveTimer: null,
|
||
isSaving: false, // 新增:控制自动保存提示的显示
|
||
previewVisible: false, // 新增:控制预览对话框的显示
|
||
currentTime: '', // 新增:记录预览时间
|
||
TypeList:[
|
||
// {
|
||
// value:"1",
|
||
// label:"服务条款"
|
||
// },
|
||
// {
|
||
// value:"2",
|
||
// label:"api文档"
|
||
// },
|
||
// {
|
||
// value:"3",
|
||
// label:"挖矿教程"
|
||
// },
|
||
// {
|
||
// value:"0",
|
||
// label:"其他"
|
||
// }
|
||
],
|
||
|
||
}
|
||
},
|
||
methods: {
|
||
async fetchAddDocument(params){
|
||
const res = await addDocument(params)
|
||
},
|
||
|
||
onCreated(editor) {
|
||
this.editor = Object.seal(editor)
|
||
|
||
// 编辑器创建后,如果有恢复的内容,设置到编辑器中
|
||
if (this.addParams.content) {
|
||
this.$nextTick(() => {
|
||
editor.setHtml(this.addParams.content)
|
||
})
|
||
}
|
||
},
|
||
|
||
handelAddDocument(){
|
||
if (!this.addParams.type) {
|
||
this.$message({
|
||
message: '请填写文档类型',
|
||
type: 'warning',
|
||
duration: 4000,
|
||
showClose: true
|
||
})
|
||
return
|
||
}
|
||
|
||
// 确保获取最新的编辑器内容
|
||
if (this.editor) {
|
||
this.addParams.content = this.editor.getHtml()
|
||
}
|
||
|
||
this.fetchAddDocument(this.addParams)
|
||
},
|
||
|
||
handlePreview() {
|
||
this.previewVisible = true
|
||
this.currentTime = new Date().toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
})
|
||
},
|
||
|
||
handlePreviewClose() {
|
||
this.previewVisible = false
|
||
},
|
||
|
||
handlePrintPreview() {
|
||
const printWindow = window.open('', '_blank');
|
||
if (printWindow) {
|
||
printWindow.document.write(`
|
||
<html>
|
||
<head>
|
||
<title>${this.addParams.title || '文档预览'}</title>
|
||
<style>
|
||
body {
|
||
font-family: 'Arial', sans-serif;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
margin: 20px;
|
||
}
|
||
.preview-empty {
|
||
text-align: center;
|
||
padding: 50px 0;
|
||
color: #95a5a6;
|
||
}
|
||
.preview-empty i {
|
||
font-size: 40px;
|
||
margin-bottom: 15px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
${this.addParams.content || '<div class="preview-empty"><i>📄</i><p>暂无内容</p></div>'}
|
||
</body>
|
||
</html>
|
||
`);
|
||
printWindow.document.close();
|
||
printWindow.print();
|
||
printWindow.close();
|
||
}
|
||
},
|
||
|
||
handleSave() {
|
||
this.manualSave()
|
||
this.$message({
|
||
message: '内容已保存到本地,请尽快发布,关闭页面可能丢失内容',
|
||
type: 'success',
|
||
duration: 4000,
|
||
showClose: true
|
||
})
|
||
},
|
||
|
||
handleEditorChange() {
|
||
this.handleAutoSave()
|
||
},
|
||
|
||
handleAutoSave() {
|
||
// 显示正在保存状态
|
||
this.isSaving = true
|
||
|
||
if (this.autoSaveTimer) {
|
||
clearTimeout(this.autoSaveTimer)
|
||
}
|
||
|
||
this.autoSaveTimer = setTimeout(() => {
|
||
this.saveToLocalStorage()
|
||
}, 1500) // 减少到1.5秒,提高响应速度
|
||
},
|
||
|
||
saveToLocalStorage() {
|
||
try {
|
||
// 确保获取最新的编辑器内容
|
||
if (this.editor) {
|
||
this.addParams.content = this.editor.getHtml()
|
||
}
|
||
|
||
const saveData = {
|
||
title: this.addParams.title,
|
||
content: this.addParams.content,
|
||
type: this.addParams.type,
|
||
imageUrl: this.addParams.imageUrl,
|
||
timestamp: new Date().toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit'
|
||
}),
|
||
lastModified: Date.now()
|
||
}
|
||
|
||
localStorage.setItem('editor_draft', JSON.stringify(saveData))
|
||
this.lastSaveTime = saveData.timestamp
|
||
this.isSaving = false
|
||
|
||
|
||
} catch (error) {
|
||
console.error('自动保存失败:', error)
|
||
this.isSaving = false
|
||
}
|
||
},
|
||
|
||
loadFromLocalStorage() {
|
||
try {
|
||
const savedData = localStorage.getItem('editor_draft')
|
||
if (savedData) {
|
||
const data = JSON.parse(savedData)
|
||
|
||
// 检查数据是否过期(7天)
|
||
const isExpired = Date.now() - (data.lastModified || 0) > 7 * 24 * 60 * 60 * 1000
|
||
|
||
if (isExpired) {
|
||
console.log('本地数据已过期,清除缓存')
|
||
localStorage.removeItem('editor_draft')
|
||
return
|
||
}
|
||
|
||
// 使用 $nextTick 确保 DOM 更新后再设置内容
|
||
this.$nextTick(() => {
|
||
this.addParams.title = data.title || ''
|
||
this.addParams.content = data.content || ''
|
||
this.addParams.type = data.type || '1'
|
||
this.addParams.imageUrl = data.imageUrl || ''
|
||
this.lastSaveTime = data.timestamp || ''
|
||
|
||
// 如果编辑器已经创建,直接设置内容
|
||
if (this.editor) {
|
||
this.editor.setHtml(data.content || '')
|
||
}
|
||
|
||
|
||
})
|
||
}
|
||
} catch (error) {
|
||
console.error('恢复数据失败:', error)
|
||
}
|
||
},
|
||
|
||
clearDraft() {
|
||
localStorage.removeItem('editor_draft')
|
||
this.lastSaveTime = ''
|
||
this.isSaving = false
|
||
},
|
||
|
||
// 新增:手动保存方法
|
||
manualSave() {
|
||
// 确保获取最新的编辑器内容
|
||
if (this.editor) {
|
||
this.addParams.content = this.editor.getHtml()
|
||
}
|
||
this.saveToLocalStorage()
|
||
},
|
||
|
||
// 页面卸载前处理
|
||
handleBeforeUnload(event) {
|
||
// 自动保存,不提示用户
|
||
if (this.addParams.title || this.addParams.content) {
|
||
// 确保获取最新的编辑器内容
|
||
if (this.editor) {
|
||
this.addParams.content = this.editor.getHtml()
|
||
}
|
||
this.saveToLocalStorage()
|
||
}
|
||
},
|
||
|
||
// 新增:获取文档类型名称
|
||
getDocumentTypeName(type) {
|
||
switch (type) {
|
||
case '1':
|
||
return '服务条款';
|
||
case '2':
|
||
return 'api文档';
|
||
case '3':
|
||
return '挖矿教程';
|
||
case '0':
|
||
return '其他';
|
||
default:
|
||
return '未知类型';
|
||
}
|
||
},
|
||
|
||
|
||
},
|
||
|
||
mounted() {
|
||
// 先恢复数据
|
||
this.loadFromLocalStorage()
|
||
|
||
// 添加页面卸载前的保存保护
|
||
window.addEventListener('beforeunload', this.handleBeforeUnload)
|
||
|
||
// 延迟设置默认内容,避免覆盖恢复的数据
|
||
setTimeout(() => {
|
||
if (!this.addParams.content) {
|
||
this.html = '<p>模拟 Ajax 异步设置内容 HTML</p>'
|
||
}
|
||
}, 1500)
|
||
},
|
||
|
||
beforeDestroy() {
|
||
// 确保获取最新的编辑器内容
|
||
if (this.editor) {
|
||
this.addParams.content = this.editor.getHtml()
|
||
}
|
||
this.saveToLocalStorage()
|
||
|
||
// 移除事件监听器
|
||
window.removeEventListener('beforeunload', this.handleBeforeUnload)
|
||
|
||
if (this.autoSaveTimer) {
|
||
clearTimeout(this.autoSaveTimer)
|
||
}
|
||
|
||
const editor = this.editor
|
||
if (editor == null) return
|
||
editor.destroy()
|
||
},
|
||
|
||
beforeRouteLeave(to, from, next) {
|
||
// 确保获取最新的编辑器内容
|
||
if (this.editor) {
|
||
this.addParams.content = this.editor.getHtml()
|
||
}
|
||
this.saveToLocalStorage()
|
||
next()
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* 页面整体样式 */
|
||
.editor-page {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
background: #f8f9fa;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* 页面头部样式 */
|
||
.page-header {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 30px;
|
||
border-radius: 12px;
|
||
margin-bottom: 24px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
box-shadow: 0 8px 32px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
.header-left {
|
||
flex: 1;
|
||
}
|
||
|
||
.title-section {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.title-icon {
|
||
font-size: 28px;
|
||
margin-right: 12px;
|
||
color: #ffd700;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 28px;
|
||
font-weight: 700;
|
||
margin: 0;
|
||
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||
}
|
||
|
||
.page-description {
|
||
font-size: 16px;
|
||
opacity: 0.9;
|
||
margin: 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.header-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.header-actions .el-button {
|
||
border-radius: 8px;
|
||
font-weight: 500;
|
||
padding: 12px 20px;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.header-actions .el-button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 20px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
/* 配置区域样式 */
|
||
.config-section {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
.section-header {
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 24px;
|
||
padding-bottom: 16px;
|
||
border-bottom: 2px solid #f8f9fa;
|
||
color: #2c3e50;
|
||
font-weight: 600;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.section-header i {
|
||
margin-right: 10px;
|
||
color: #667eea;
|
||
font-size: 20px;
|
||
}
|
||
|
||
.config-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
}
|
||
|
||
.config-row {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 24px;
|
||
}
|
||
|
||
.config-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.config-item label {
|
||
margin-bottom: 8px;
|
||
font-weight: 600;
|
||
color: #495057;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.required::after {
|
||
content: '*';
|
||
color: #e74c3c;
|
||
margin-left: 4px;
|
||
font-weight: bold;
|
||
}
|
||
|
||
/* 编辑器区域样式 */
|
||
.editor-section {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 4px 20px rgba(0,0,0,0.08);
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
.editor-container {
|
||
border: 1px solid #dcdfe6;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: white;
|
||
min-height: 650px;
|
||
}
|
||
|
||
/* 设置编辑器高度 */
|
||
.editor-container .w-e-text-container {
|
||
height: 600px !important;
|
||
min-height: 600px !important;
|
||
}
|
||
|
||
/* 编辑器内容区域样式 */
|
||
.editor-container .w-e-text {
|
||
height: 600px !important;
|
||
min-height: 600px !important;
|
||
}
|
||
|
||
/* 保存状态样式 */
|
||
.save-status {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background: #67c23a;
|
||
color: white;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
z-index: 1000;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateX(100%);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: translateX(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* 预览对话框样式 */
|
||
.preview-dialog .el-dialog__body {
|
||
padding: 20px;
|
||
max-height: 70vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.preview-dialog .el-dialog__header {
|
||
background-color: #f8f9fa;
|
||
border-bottom: 1px solid #ebeef5;
|
||
}
|
||
|
||
.preview-dialog .el-dialog__header .el-dialog__title {
|
||
color: #333;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.preview-dialog .el-dialog__footer {
|
||
background-color: #f8f9fa;
|
||
border-top: 1px solid #ebeef5;
|
||
padding: 15px 20px;
|
||
}
|
||
|
||
.preview-dialog .el-dialog__footer .dialog-footer {
|
||
text-align: right;
|
||
}
|
||
|
||
.preview-dialog .el-dialog__footer .dialog-footer .el-button {
|
||
border-radius: 6px;
|
||
padding: 8px 15px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.preview-dialog .el-dialog__footer .dialog-footer .el-button:first-child {
|
||
margin-right: 10px;
|
||
}
|
||
|
||
.preview-dialog .el-dialog__footer .dialog-footer .el-button:last-child {
|
||
background-color: #667eea;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.preview-dialog .el-dialog__footer .dialog-footer .el-button:last-child:hover {
|
||
background-color: #5a67d8;
|
||
border-color: #5a67d8;
|
||
}
|
||
|
||
/* 预览内容样式 - 简化版本 */
|
||
.preview-content {
|
||
background: white;
|
||
padding: 20px;
|
||
}
|
||
|
||
.preview-empty {
|
||
text-align: center;
|
||
padding: 50px 20px;
|
||
color: #95a5a6;
|
||
}
|
||
|
||
.preview-empty i {
|
||
font-size: 48px;
|
||
margin-bottom: 15px;
|
||
display: block;
|
||
}
|
||
|
||
.preview-empty p {
|
||
margin: 0;
|
||
font-size: 16px;
|
||
}
|
||
|
||
/* 响应式设计 */
|
||
@media (max-width: 768px) {
|
||
.editor-page {
|
||
padding: 10px;
|
||
}
|
||
|
||
.page-header {
|
||
flex-direction: column;
|
||
gap: 20px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.header-actions {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.config-row {
|
||
grid-template-columns: 1fr;
|
||
gap: 16px;
|
||
}
|
||
|
||
.page-title {
|
||
font-size: 24px;
|
||
}
|
||
|
||
.save-status {
|
||
bottom: 10px;
|
||
right: 10px;
|
||
left: 10px;
|
||
text-align: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
/* Element UI 组件样式优化 */
|
||
.el-input, .el-select {
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.el-input__inner, .el-select .el-input__inner {
|
||
border-radius: 8px;
|
||
border: 1px solid #dcdfe6;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.el-input__inner:focus, .el-select .el-input__inner:focus {
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 2px rgba(102, 126, 234, 0.2);
|
||
}
|
||
|
||
.el-tag {
|
||
border-radius: 6px;
|
||
font-weight: 500;
|
||
}
|
||
</style> |