安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
201 lines
9.4 KiB
HTML
201 lines
9.4 KiB
HTML
|
||
<style>
|
||
.import-drop { border: 2px dashed #d2d6de; border-radius: 6px; padding: 40px 20px; text-align: center; background: #fafbfc; cursor: pointer; }
|
||
.import-drop:hover { border-color: #1e9fff; background: #f4f9ff; }
|
||
.import-drop .layui-icon { font-size: 44px; color: #c2c6cc; display: block; margin-bottom: 10px; }
|
||
.import-file { margin-top: 12px; color: #1e9fff; }
|
||
.import-help { background: #f8f9fb; border: 1px solid #eee; border-radius: 4px; padding: 15px; }
|
||
.import-help h3 { font-size: 14px; margin-bottom: 8px; }
|
||
.import-help ul { padding-left: 18px; color: #666; line-height: 2; font-size: 13px; }
|
||
</style>
|
||
|
||
<div class="layui-card">
|
||
<div class="layui-card-header">
|
||
<span>导入文档</span>
|
||
{if condition="$project"}
|
||
<a class="layui-btn layui-btn-sm layui-btn-primary" style="float:right;"
|
||
href="/docs/backend/doc?project_id={$project.id}&version_id={$version.id}">返回列表</a>
|
||
{/if}
|
||
</div>
|
||
<div class="layui-card-body">
|
||
{if condition="empty($projects)"}
|
||
<div class="docs-empty">
|
||
还没有文档项目,请先 <a href="/docs/backend/project/add" class="layui-btn layui-btn-sm">创建项目</a>
|
||
</div>
|
||
{else/}
|
||
<div class="layui-row layui-col-space15">
|
||
<div class="layui-col-md8">
|
||
<form class="layui-form" id="importForm">
|
||
<div class="layui-form-item">
|
||
<label class="layui-form-label">目标项目</label>
|
||
<div class="layui-input-block">
|
||
<select name="project_id" lay-filter="projectPick">
|
||
{volist name="projects" id="p"}
|
||
<option value="{$p.id}" {if condition="$project && $project.id eq $p.id"}selected{/if}>{$p.title}</option>
|
||
{/volist}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="layui-form-item">
|
||
<label class="layui-form-label">目标版本</label>
|
||
<div class="layui-input-block">
|
||
<select name="version_id">
|
||
{volist name="versions" id="v"}
|
||
<option value="{$v.id}" {if condition="$version && $version.id eq $v.id"}selected{/if}>{$v.title}({$v.name})</option>
|
||
{/volist}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="layui-form-item">
|
||
<label class="layui-form-label">父级文档</label>
|
||
<div class="layui-input-block">
|
||
<select name="pid">
|
||
<option value="0">顶级文档</option>
|
||
{volist name="parentOptions" id="opt"}
|
||
<option value="{$opt.id}">{$opt.title}</option>
|
||
{/volist}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="layui-form-item">
|
||
<label class="layui-form-label">文档标题</label>
|
||
<div class="layui-input-block">
|
||
<input type="text" name="title" placeholder="留空则自动从文件内容提取" autocomplete="off" class="layui-input">
|
||
</div>
|
||
</div>
|
||
<div class="layui-form-item">
|
||
<label class="layui-form-label">文档标识</label>
|
||
<div class="layui-input-block">
|
||
<input type="text" name="name" placeholder="留空自动生成,重名会自动加后缀" autocomplete="off" class="layui-input">
|
||
</div>
|
||
</div>
|
||
<div class="layui-form-item">
|
||
<label class="layui-form-label">排序</label>
|
||
<div class="layui-input-block">
|
||
<input type="number" name="sort" value="0" autocomplete="off" class="layui-input">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layui-form-item">
|
||
<label class="layui-form-label">选择文件</label>
|
||
<div class="layui-input-block">
|
||
<div class="import-drop" id="dropZone">
|
||
<i class="layui-icon layui-icon-upload-drag"></i>
|
||
<div>点击选择文件,或将文件拖拽到此处</div>
|
||
<div class="docs-tip" style="margin-top:6px;">支持 docx / md / html / txt,单个文件不超过 20MB</div>
|
||
<div class="import-file" id="fileName"></div>
|
||
</div>
|
||
<input type="file" id="fileInput" accept=".docx,.md,.markdown,.html,.htm,.txt" style="display:none;">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="layui-form-item">
|
||
<div class="layui-input-block">
|
||
<button type="button" class="layui-btn" id="btnImport">开始导入</button>
|
||
{if condition="$project"}
|
||
<a class="layui-btn layui-btn-primary" href="/docs/backend/doc?project_id={$project.id}">取消</a>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="layui-col-md4">
|
||
<div class="import-help">
|
||
<h3>导入说明</h3>
|
||
<ul>
|
||
<li><b>Word(.docx)</b>:自动识别标题层级与段落
|
||
{if condition="$hasPhpWord"}
|
||
<span class="layui-badge layui-bg-green">已装 PhpWord,解析更完整</span>
|
||
{else/}
|
||
<span class="layui-badge layui-bg-orange">当前使用内置解析</span>
|
||
{/if}
|
||
</li>
|
||
<li><b>Markdown(.md)</b>:支持标题、代码块、列表、表格、引用、链接与图片</li>
|
||
<li><b>HTML</b>:自动提取 body 内容并清洗脚本</li>
|
||
<li><b>纯文本</b>:按行转换为段落</li>
|
||
</ul>
|
||
|
||
<h3 style="margin-top:14px;">提升 Word 解析效果</h3>
|
||
<ul>
|
||
<li>执行 <code>composer require phpoffice/phpword</code> 后可完整还原表格与样式</li>
|
||
<li>Word 中的图片建议先上传到编辑器,或导入后在编辑页粘贴</li>
|
||
</ul>
|
||
|
||
<h3 style="margin-top:14px;">另一种方式</h3>
|
||
<ul>
|
||
<li>直接在编辑器中从 Word 复制粘贴,外链图片会自动抓取并保存到本地</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/if}
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
layui.use(['form', 'layer', 'jquery', 'ywxapp', 'http'], function () {
|
||
var http = layui.http;
|
||
var form = layui.form;
|
||
var layer = layui.layer;
|
||
var $ = layui.$;
|
||
|
||
// 切换项目需要重载版本与父级下拉
|
||
form.on('select(projectPick)', function (data) {
|
||
location.href = '/docs/backend/doc/import?project_id=' + data.value;
|
||
});
|
||
|
||
var $input = $('#fileInput');
|
||
var $zone = $('#dropZone');
|
||
|
||
$zone.on('click', function () { $input.trigger('click'); });
|
||
|
||
$input.on('change', function () {
|
||
var file = this.files && this.files[0];
|
||
$('#fileName').text(file ? '已选择:' + file.name : '');
|
||
});
|
||
|
||
// 拖拽上传
|
||
$zone.on('dragover', function (e) { e.preventDefault(); e.stopPropagation(); });
|
||
$zone.on('drop', function (e) {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
var files = e.originalEvent.dataTransfer.files;
|
||
if (files && files.length) {
|
||
$input[0].files = files;
|
||
$('#fileName').text('已选择:' + files[0].name);
|
||
}
|
||
});
|
||
|
||
$('#btnImport').on('click', function () {
|
||
var file = $input[0].files && $input[0].files[0];
|
||
if (!file) {
|
||
layer.msg('请先选择要导入的文件', { icon: 0 });
|
||
return;
|
||
}
|
||
|
||
var fd = new FormData();
|
||
fd.append('file', file);
|
||
$('#importForm').find('select, input[type=text], input[type=number]').each(function () {
|
||
fd.append(this.name, this.value);
|
||
});
|
||
|
||
var loading = layer.load(1);
|
||
// http 识别 FormData:自动去掉 Content-Type 让浏览器带上 boundary
|
||
http.post('/docs/backend/doc/import', fd).then(function (res) {
|
||
layer.close(loading);
|
||
if (res.code === 0) {
|
||
layer.msg(res.message, { icon: 1 }, function () {
|
||
location.href = '/docs/backend/doc/edit/' + res.data.id;
|
||
});
|
||
} else {
|
||
layer.msg(res.message || '导入失败', { icon: 2 });
|
||
}
|
||
}).catch(function () {
|
||
layer.close(loading);
|
||
layer.msg('请求失败,请检查文件大小与服务器配置', { icon: 2 });
|
||
});
|
||
});
|
||
});
|
||
</script>
|