Files
YwxAppThink/app/backend/view/template/index.html
T
yangwx 18101cf11a feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(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(市场 / 开发者中心 / 审核 / 分润)
2026-10-22 00:53:39 +08:00

242 lines
11 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div class="layui-card">
<div class="layui-card-header">已安装模板 <span id="gdTip" class="layui-badge layui-bg-blue" style="margin-left:8px;"></span></div>
<div class="layui-card-body">
<table id="tplTable" lay-filter="tplTable"></table>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">插件模板绑定(整站皮肤按插件独立选择)</div>
<div class="layui-card-body">
<table class="layui-table">
<thead>
<tr><th>插件</th><th>当前激活</th><th>选择模板</th><th>操作</th></tr>
</thead>
<tbody id="bindBody"></tbody>
</table>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">安装新模板(上传 zip 包)</div>
<div class="layui-card-body">
<button type="button" class="layui-btn" id="uploadBtn"><i class="layui-icon">&#xe67c;</i>上传模板包</button>
<div style="color:#999;margin-top:8px;line-height:1.6;">
zip 内须含 <code>templates/&lt;name&gt;/template.json</code> 与 <code>static/</code> 资源,安装后落到
<code>templates/&lt;name&gt;/</code> 与 <code>public/static/templates/&lt;name&gt;/</code>。
</div>
</div>
</div>
<div class="layui-card">
<div class="layui-card-header">配置备份(导入 / 导出)</div>
<div class="layui-card-body">
<button type="button" class="layui-btn layui-btn-normal" id="exportBtn"><i class="layui-icon">&#xe67d;</i>导出配置</button>
<button type="button" class="layui-btn" id="importBtn"><i class="layui-icon">&#xe681;</i>导入配置</button>
<div style="color:#999;margin-top:8px;line-height:1.6;">
导出含「插件模板绑定 + 界面设置 + 配色覆盖」的 JSON;导入会安全跳过未安装模板的绑定,可跨站点克隆皮肤方案。
</div>
</div>
</div>
<script type="text/html" id="tplOperate">
<a class="layui-btn layui-btn-xs" lay-event="setDefault">设默认</a>
<a class="layui-btn layui-btn-xs {{d.enabled ? 'layui-btn-warm' : 'layui-btn-normal'}}" lay-event="toggle">{{d.enabled ? '禁用' : '启用'}}</a>
{{# if (d.variable_defs && Object.keys(d.variable_defs).length > 0) { }}
<a class="layui-btn layui-btn-xs layui-btn-normal" lay-event="vars">配色</a>
{{# } }}
<a class="layui-btn layui-btn-xs layui-btn-danger" lay-event="uninstall">卸载</a>
</script>
<script>
layui.use(['table', 'form', 'layer', 'upload', 'jquery', 'http'], function () {
var table = layui.table, form = layui.form, layer = layui.layer, upload = layui.upload
, $ = layui.jquery, http = layui.http;
var state = { installed: [], addon: [], active_map: {} };
// 统一走集成的 http(/assets/ywxapp/ywxapp.js 提供),不再用 $.ajax
function api(url, data) {
return http.post(url, data || {});
}
function load() {
api('index').then(function (res) {
if (res.code !== 0) { layer.msg(res.message || '加载失败', { icon: 2 }); return; }
state.installed = res.data.installed || [];
state.addon = res.data.addon || [];
state.active_map = res.data.active_map || {};
state.global_default = res.data.global_default || 'default';
$('#gdTip').text('全站默认:' + (state.global_default === 'default' ? '自带视图' : state.global_default));
renderTpl();
renderBind();
}, function () { layer.msg('请求失败', { icon: 2 }); });
}
function renderTpl() {
table.render({
elem: '#tplTable',
data: state.installed,
page: false,
cols: [[
{ field: 'preview', title: '预览', width: 90, templet: function (d) {
if (! d.preview_url) { return '<span style="color:#999">无</span>'; }
var badge = (d.preview_urls && d.preview_urls.length > 1)
? ' <span class="layui-badge layui-bg-orange" style="vertical-align:top;">' + d.preview_urls.length + '图</span>' : '';
return '<img src="' + d.preview_url + '" style="max-height:40px;cursor:zoom-in;" onclick="openPreview(\'' + d.name + '\')">' + badge;
} },
{ field: 'name', title: '标识', width: 120 },
{ field: 'title', title: '名称', minWidth: 140 },
{ field: 'version', title: '版本', width: 80 },
{ field: 'target_addon', title: '适用插件', minWidth: 120, templet: function (d) {
return (d.target_addon || []).join(', ');
} },
{ field: 'applied_to', title: '已应用', minWidth: 120, templet: function (d) {
return (d.applied_to || []).join(', ') || '<span style="color:#999">未绑定</span>';
} },
{ field: 'enabled', title: '状态', width: 80, templet: function (d) {
return d.enabled
? '<span class="layui-badge layui-bg-green">启用</span>'
: '<span class="layui-badge layui-bg-gray">禁用</span>';
} },
{ title: '操作', width: 260, toolbar: '#tplOperate', fixed: 'right' }
]]
});
}
function renderBind() {
var html = '';
state.addon.forEach(function (a) {
var cur = state.active_map[a.name] || 'default';
var opts = '<option value="default">默认(自带视图)</option>';
state.installed.forEach(function (t) {
if ((t.target_addon || []).indexOf(a.name) >= 0) {
var sel = (cur === t.name) ? 'selected' : '';
opts += '<option value="' + t.name + '" ' + sel + '>' + t.title + ' (' + t.version + ')</option>';
}
});
html += '<tr>'
+ '<td>' + a.title + ' <span style="color:#999">' + a.name + '</span></td>'
+ '<td>' + (cur === 'default' ? '默认' : cur) + '</td>'
+ '<td><select class="layui-input-inline" data-addon="' + a.name + '" style="width:220px;">' + opts + '</select></td>'
+ '<td><button type="button" class="layui-btn layui-btn-xs" onclick="saveBind(\'' + a.name + '\', this)">保存</button></td>'
+ '</tr>';
});
$('#bindBody').html(html);
form.render('select');
}
window.saveBind = function (addon, btn) {
var sel = $(btn).closest('tr').find('select').val();
api('setActive', { addon: addon, template: sel }).then(function (res) {
layer.msg(res.message || '已保存', { icon: 1 });
load();
}, function () { layer.msg('保存失败', { icon: 2 }); });
};
// 预览灯箱:多图走 layer.photos 画廊,单图直接放大
window.openPreview = function (name) {
var tpl = null;
for (var i = 0; i < state.installed.length; i++) {
if (state.installed[i].name === name) { tpl = state.installed[i]; break; }
}
if (! tpl || ! tpl.preview_urls || ! tpl.preview_urls.length) { return; }
if (tpl.preview_urls.length === 1) {
layer.open({ type: 1, title: tpl.title || name, area: 'auto',
content: '<img src="' + tpl.preview_urls[0] + '" style="max-width:90vw;max-height:80vh;">' });
} else {
var data = tpl.preview_urls.map(function (u) { return { src: u, thumb: u, alt: tpl.title || name }; });
layer.photos({ photos: { title: tpl.title || name, data: data }, anim: 5 });
}
};
table.on('tool(tplTable)', function (obj) {
if (obj.event === 'setDefault') {
api('setDefault', { template: obj.data.name }).then(function (res) {
layer.msg(res.message || '已设默认', { icon: 1 });
load();
}, function () { layer.msg('操作失败', { icon: 2 }); });
} else if (obj.event === 'toggle') {
var enabled = obj.data.enabled ? 0 : 1;
api('toggle', { name: obj.data.name, enabled: enabled }).then(function (res) {
layer.msg(res.message || '已更新', { icon: 1 });
load();
}, function () { layer.msg('操作失败', { icon: 2 }); });
} else if (obj.event === 'vars') {
openVarsModal(obj.data);
} else if (obj.event === 'uninstall') {
layer.confirm('确认卸载模板「' + obj.data.name + '」?将删除其文件并解除绑定。', function (i) {
api('uninstall', { name: obj.data.name }).then(function (res) {
layer.msg(res.message || '已卸载', { icon: 1 });
load();
}, function () { layer.msg('卸载失败', { icon: 2 }); });
});
}
});
function openVarsModal(tpl) {
var defs = tpl.variable_defs || {};
var overrides = tpl.variable_overrides || {};
var rows = '';
Object.keys(defs).forEach(function (k) {
var d = defs[k];
var cur = overrides[k] != null ? overrides[k] : (d.default || '');
rows += '<div class="layui-form-item">'
+ '<label class="layui-form-label">' + (d.label || k) + '</label>'
+ '<div class="layui-input-block">'
+ '<input type="text" class="layui-input" data-k="' + k + '" value="' + cur + '" placeholder="默认:' + (d.default || '') + '">'
+ (d.type === 'color' ? ' <span style="color:#999;font-size:12px;">颜色填 #xxxxxx</span>' : '')
+ '</div></div>';
});
if (rows === '') { rows = '<div style="padding:10px;color:#999;">该模板未声明可配置变量</div>'; }
var html = '<form class="layui-form" style="padding:15px;" id="varsForm">'
+ '<div style="color:#999;margin-bottom:10px;">模板:' + tpl.title + '(' + tpl.name + ')— 保存后无需改 HTML 即生效</div>'
+ rows + '</form>';
layer.open({
type: 1, title: '风格配色 - ' + tpl.title, area: ['520px', 'auto'], content: html,
btn: ['保存', '取消'],
yes: function (idx) {
var data = {};
$('#varsForm input[data-k]').each(function () {
data[$(this).data('k')] = $(this).val();
});
api('saveVariables', { name: tpl.name, vars: data }).then(function (res) {
layer.msg(res.message || '已保存', { icon: 1 });
layer.close(idx);
load();
}, function () { layer.msg('保存失败', { icon: 2 }); });
}
});
}
upload.render({
elem: '#uploadBtn',
url: 'upload',
accept: 'file',
exts: 'zip',
done: function (res) {
if (res.code === 0) { layer.msg('安装成功', { icon: 1 }); load(); }
else { layer.msg(res.message || '安装失败', { icon: 2 }); }
},
error: function () { layer.msg('上传失败', { icon: 2 }); }
});
// 导出配置:直接触发浏览器下载
$('#exportBtn').on('click', function () {
window.location.href = 'exportConfig';
});
// 导入配置:复用 upload 组件,仅接受 .json
upload.render({
elem: '#importBtn',
url: 'importConfig',
accept: 'file',
exts: 'json',
done: function (res) {
if (res.code === 0) { layer.msg(res.message || '导入成功', { icon: 1 }); load(); }
else { layer.msg(res.message || '导入失败', { icon: 2 }); }
},
error: function () { layer.msg('上传失败', { icon: 2 }); }
});
load();
});
</script>