安全(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(市场 / 开发者中心 / 审核 / 分润)
288 lines
8.0 KiB
JavaScript
288 lines
8.0 KiB
JavaScript
/**
|
||
* YwxApp 文档中心前台脚本
|
||
*
|
||
* 提供:目录折叠、移动端抽屉、代码复制、右侧锚点滚动高亮、版本切换。
|
||
* 纯原生实现,不依赖任何库。
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
/**
|
||
* 前台路由前缀(layout.html 注入):
|
||
* 路径模式为 '/docs';域名绑定/独立入口模式为 ''。
|
||
* 老缓存页面未注入时回退 '/docs'。
|
||
*/
|
||
var BASE = (typeof window.DOCS_BASE === 'string' && window.DOCS_BASE !== '')
|
||
? window.DOCS_BASE : '/docs';
|
||
|
||
/**
|
||
* 从当前 pathname 解析 [项目, 版本, 文档] 段:
|
||
* 绑定模式 /<项目>/<版本>/<文档>,路径模式 /docs/<项目>/... 自动剥掉前缀段。
|
||
*/
|
||
function parsePathParts() {
|
||
var parts = window.location.pathname.split('/').filter(Boolean);
|
||
var head = BASE.replace(/^\//, '');
|
||
if (head && parts[0] === head) {
|
||
parts = parts.slice(1);
|
||
}
|
||
return parts;
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
initTreeToggle();
|
||
initMobileDrawer();
|
||
initCodeCopy();
|
||
initTocScrollSpy();
|
||
initVersionSwitch();
|
||
scrollActiveIntoView();
|
||
});
|
||
|
||
/**
|
||
* 侧边目录展开与折叠
|
||
*/
|
||
function initTreeToggle() {
|
||
var tree = document.querySelector('.dc-tree');
|
||
if (!tree) {
|
||
return;
|
||
}
|
||
|
||
tree.addEventListener('click', function (e) {
|
||
var toggle = e.target.closest('.dc-tree-toggle');
|
||
var dirLink = e.target.closest('.dc-tree-link.is-dir');
|
||
var trigger = toggle || dirLink;
|
||
|
||
if (!trigger) {
|
||
return;
|
||
}
|
||
|
||
var li = trigger.closest('li');
|
||
if (li) {
|
||
li.classList.toggle('is-open');
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 移动端侧边栏抽屉
|
||
*/
|
||
function initMobileDrawer() {
|
||
var btn = document.getElementById('dcMenuToggle');
|
||
var sidebar = document.getElementById('dcSidebar');
|
||
var mask = document.getElementById('dcMask');
|
||
|
||
if (!btn || !sidebar) {
|
||
return;
|
||
}
|
||
|
||
function close() {
|
||
sidebar.classList.remove('is-open');
|
||
if (mask) {
|
||
mask.classList.remove('is-show');
|
||
}
|
||
}
|
||
|
||
btn.addEventListener('click', function () {
|
||
sidebar.classList.toggle('is-open');
|
||
if (mask) {
|
||
mask.classList.toggle('is-show', sidebar.classList.contains('is-open'));
|
||
}
|
||
});
|
||
|
||
if (mask) {
|
||
mask.addEventListener('click', close);
|
||
}
|
||
|
||
// 点击目录项后自动收起抽屉
|
||
sidebar.addEventListener('click', function (e) {
|
||
if (e.target.closest('a.dc-tree-link') && window.innerWidth <= 860) {
|
||
close();
|
||
}
|
||
});
|
||
|
||
window.addEventListener('resize', function () {
|
||
if (window.innerWidth > 860) {
|
||
close();
|
||
}
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 代码块复制按钮
|
||
*/
|
||
function initCodeCopy() {
|
||
var blocks = document.querySelectorAll('.dc-content pre');
|
||
|
||
Array.prototype.forEach.call(blocks, function (pre) {
|
||
var btn = document.createElement('button');
|
||
btn.className = 'dc-copy-btn';
|
||
btn.type = 'button';
|
||
btn.textContent = '复制';
|
||
|
||
btn.addEventListener('click', function () {
|
||
var code = pre.querySelector('code');
|
||
var text = code ? code.innerText : pre.innerText;
|
||
|
||
copyText(text, function (ok) {
|
||
btn.textContent = ok ? '已复制' : '复制失败';
|
||
btn.classList.toggle('is-done', ok);
|
||
setTimeout(function () {
|
||
btn.textContent = '复制';
|
||
btn.classList.remove('is-done');
|
||
}, 1600);
|
||
});
|
||
});
|
||
|
||
pre.appendChild(btn);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 复制文本到剪贴板,兼容不支持 Clipboard API 的环境
|
||
*
|
||
* @param {string} text 待复制文本
|
||
* @param {Function} cb 回调,参数为是否成功
|
||
*/
|
||
function copyText(text, cb) {
|
||
if (navigator.clipboard && window.isSecureContext) {
|
||
navigator.clipboard.writeText(text).then(function () {
|
||
cb(true);
|
||
}).catch(function () {
|
||
cb(fallbackCopy(text));
|
||
});
|
||
return;
|
||
}
|
||
cb(fallbackCopy(text));
|
||
}
|
||
|
||
/**
|
||
* 兜底复制方案
|
||
*
|
||
* @param {string} text 待复制文本
|
||
* @returns {boolean}
|
||
*/
|
||
function fallbackCopy(text) {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text;
|
||
ta.style.position = 'fixed';
|
||
ta.style.opacity = '0';
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
|
||
var ok = false;
|
||
try {
|
||
ok = document.execCommand('copy');
|
||
} catch (err) {
|
||
ok = false;
|
||
}
|
||
|
||
document.body.removeChild(ta);
|
||
return ok;
|
||
}
|
||
|
||
/**
|
||
* 右侧锚点滚动高亮
|
||
*/
|
||
function initTocScrollSpy() {
|
||
var toc = document.getElementById('dcToc');
|
||
var content = document.getElementById('dcContent');
|
||
|
||
if (!toc || !content) {
|
||
return;
|
||
}
|
||
|
||
var links = Array.prototype.slice.call(toc.querySelectorAll('a[data-anchor]'));
|
||
if (!links.length) {
|
||
return;
|
||
}
|
||
|
||
var headings = links.map(function (link) {
|
||
return document.getElementById(link.getAttribute('data-anchor'));
|
||
}).filter(Boolean);
|
||
|
||
if (!headings.length) {
|
||
return;
|
||
}
|
||
|
||
var offset = 90;
|
||
var ticking = false;
|
||
|
||
function update() {
|
||
var current = 0;
|
||
|
||
for (var i = 0; i < headings.length; i++) {
|
||
if (headings[i].getBoundingClientRect().top <= offset) {
|
||
current = i;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
|
||
links.forEach(function (link, index) {
|
||
link.classList.toggle('is-active', index === current);
|
||
});
|
||
|
||
ticking = false;
|
||
}
|
||
|
||
window.addEventListener('scroll', function () {
|
||
if (!ticking) {
|
||
window.requestAnimationFrame(update);
|
||
ticking = true;
|
||
}
|
||
}, { passive: true });
|
||
|
||
update();
|
||
}
|
||
|
||
/**
|
||
* 版本切换:保持当前文档路径,仅替换版本段
|
||
*/
|
||
function initVersionSwitch() {
|
||
var select = document.getElementById('dcVersionSelect');
|
||
if (!select) {
|
||
return;
|
||
}
|
||
|
||
select.addEventListener('change', function () {
|
||
var opt = select.options[select.selectedIndex];
|
||
var isDefault = opt.getAttribute('data-default') === '1';
|
||
|
||
// 地址形如 <BASE>/<项目>[/<版本>]/<文档>(BASE 由访问模式决定)
|
||
var parts = parsePathParts();
|
||
if (parts.length < 1) {
|
||
return;
|
||
}
|
||
|
||
var project = parts[0];
|
||
var docName = parts[parts.length - 1];
|
||
|
||
// 只有项目段时说明还未进入具体文档
|
||
if (parts.length === 1) {
|
||
window.location.href = BASE + '/' + project;
|
||
return;
|
||
}
|
||
|
||
window.location.href = isDefault
|
||
? BASE + '/' + project + '/' + docName
|
||
: BASE + '/' + project + '/' + select.value + '/' + docName;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 让当前选中的目录项进入可视区域
|
||
*/
|
||
function scrollActiveIntoView() {
|
||
var active = document.querySelector('.dc-tree-item.is-active');
|
||
var sidebar = document.getElementById('dcSidebar');
|
||
|
||
if (!active || !sidebar) {
|
||
return;
|
||
}
|
||
|
||
var top = active.offsetTop - sidebar.clientHeight / 2;
|
||
if (top > 0) {
|
||
sidebar.scrollTop = top;
|
||
}
|
||
}
|
||
})();
|