feat: 添加Vditor编辑器支持并扩展文件上传功能

- 新增Vditor编辑器静态资源文件,包括图片、字体和样式文件
- 在文件上传控制器中添加vditorSave方法,支持Vditor编辑器文件上传
- 在文章创建页面添加编辑器类型选择(富文本/Markdown)
- 更新.gitignore文件,排除Playwright和QA截图目录
- 扩展UploadFiles类以支持Vditor编辑器的文件上传格式
This commit is contained in:
augushong
2026-04-30 22:27:03 +08:00
parent cbf9b21b96
commit aed4b285d8
421 changed files with 24125 additions and 1 deletions

View File

@@ -79,6 +79,13 @@
<input type="radio" name="status" value="0" title="不发布" checked>
</div>
</div>
<div class="layui-form-item">
<div class="layui-form-label">编辑器类型</div>
<div class="layui-input-block">
<input type="radio" name="content_type" value="html" title="富文本" checked>
<input type="radio" name="content_type" value="markdown" title="Markdown">
</div>
</div>
{notin name='$Request.param.type' value='category-start,category-end' }
<div class="layui-form-item">
<div class="layui-form-label">发表时间</div>

View File

@@ -0,0 +1,224 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>内容管理</title>
{include file="common/_require"}
<link rel="stylesheet" href="/static/lib/vditor/dist/index.css">
<script src="/static/lib/vditor/dist/index.min.js"></script>
<script>
var currentHeaderNavItem = 'Post-{$Request.param.type|default="1"}';
var currentLeftNavItem = 'post-{$Request.param.type|default="1"}';
</script>
<style>
html,
body {
background-color: #fff;
height: 100%;
overflow: hidden;
color: #333;
}
#top-container {
border-bottom: 1px solid #e8e8e8;
padding-left: 30px;
}
#content {
height: calc(100% - 440px);
background-color: rgb(245, 245, 245);
overflow-y: auto;
position: relative;
}
#editor-container {
width: 850px;
margin: 30px auto 30px auto;
background-color: #fff;
padding: 20px 50px 50px 50px;
border: 1px solid #e8e8e8;
box-shadow: 0 2px 10px rgb(0 0 0 / 12%);
}
#editor-container li {
list-style: inherit;
}
#title-container {
padding: 20px 0;
border-bottom: 1px solid #e8e8e8;
}
#title-container input {
font-size: 30px;
border: 0;
outline: none;
width: 100%;
line-height: 1;
}
#editor-text-area {
margin-top: 20px;
}
</style>
</head>
<body class="layui-layout-body">
<div class="layui-layout layui-layout-admin">
{include file="common/_header"}
{include file="common/left_post"}
<div class="layui-body">
<div style="">
<div class="">
<div id="content">
<div id="editor-container">
<div id="title-container">
<input id="title-input" value="{$post->title}">
</div>
<div id="editor-text-area"></div>
</div>
</div>
<div style="height: 30px;line-height: 30px;padding-left: 15px;border-top: 1px solid #e8e8e8;color: #666;">
<span id="content-state">内容变动自动提交</span>
<a href="{:url('output',['id'=>$post.id])}">
去导出
</a>
</div>
<div id="content-data" style="display: none;">{:rawurlencode($post->content??$post->content_html??'')}</div>
</div>
</div>
</div>
{include file="common/_footer"}
</div>
<script>
var lastContent = '';
var contentSaveLock = true;
function autoSaveContent() {
if (contentSaveLock) {
setTimeout(() => {
autoSaveContent();
}, 2600);
} else {
contentSaveLock = true;
$.post('{:url("update")}', {
content: vditor.getValue(),
id: '{$post->id}'
}, function (result) {
$('#content-state').text('自动提交成功')
setTimeout(() => {
autoSaveContent();
}, 2600);
})
}
}
autoSaveContent();
var vditor = new Vditor('editor-text-area', {
height: 'auto',
cache: { enable: false },
mode: 'wysiwyg',
toolbar: [
'headings', 'bold', 'italic', 'strike', '|',
'list', 'ordered-list', 'check', '|',
'quote', 'code', 'inline-code', '|',
'link', 'upload', 'table', '|',
'undo', 'redo', '|',
'fullscreen', 'preview'
],
upload: {
url: '{:url("File/vditorSave")}',
fieldName: 'file[]',
extraData: function () {
return { type: 'editor' };
}
},
value: decodeURIComponent($('#content-data').html()),
input: function (value) {
contentSaveLock = false;
$('#content-state').text('等待自动提交');
},
after: function () {
setTimeout(function () {
var toolbarHeight = 0;
var toolbarEl = document.querySelector('.vditor-toolbar');
if (toolbarEl) {
toolbarHeight = toolbarEl.offsetHeight;
}
$('#content').height(window.innerHeight - toolbarHeight - 30 - 45 - 7 - 31);
}, 300);
}
});
// 外部 URL 图片粘贴处理
document.addEventListener('paste', function (e) {
var pasteStr = e.clipboardData.getData('text/html');
if (!pasteStr) return;
var imgReg = /<img.*?(?:>|\/>)/gi;
var srcReg = /src=[\'\"]?([^\'\"]*)[\'\"]?/i;
var arr = pasteStr.match(imgReg);
if (!arr || arr.length === 0) return;
layer.load();
var pending = arr.length;
for (var i = 0; i < arr.length; i++) {
(function (imgTag) {
var src = imgTag.match(srcReg);
if (src && src[1]) {
var imgSrc = src[1];
if (imgSrc.substr(0, 4) === 'http') {
$.ajax({
type: 'POST',
url: "{:url('File/urlSave')}",
data: { url: imgSrc, type: 'editor' },
success: function (result) {
if (result.code === 0) {
var mdImg = '![](' + result.data.src + ')';
var oldMdImg = '![](' + imgSrc + ')';
var current = vditor.getValue();
vditor.setValue(current.replace(oldMdImg, mdImg));
}
pending--;
if (pending === 0) layer.closeAll('loading');
},
error: function () {
pending--;
if (pending === 0) layer.closeAll('loading');
}
});
} else {
pending--;
if (pending === 0) layer.closeAll('loading');
}
} else {
pending--;
if (pending === 0) layer.closeAll('loading');
}
})(arr[i]);
}
});
// 标题变更保存
$('#title-input').change(function () {
window.loading = layer.load();
$.post('{:url("update")}', {
title: $('#title-input').val(),
id: '{$post->id}'
}, function (result) {
layer.close(window.loading);
});
});
</script>
</body>
</html>