chore: 重写初始提交(清空历史,整理后全量提交)
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
<?php
|
||||
// +----------------------------------------------------------------------
|
||||
// | YwxApp [ WE CAN DO IT JUST THINK ]
|
||||
// +----------------------------------------------------------------------
|
||||
// | Copyright (c) 2026-2036 http://ywxapp.cn All rights reserved.
|
||||
// +----------------------------------------------------------------------
|
||||
// | Author: ywxapp <admin@ywxapp.cn>
|
||||
// +----------------------------------------------------------------------
|
||||
declare (strict_types = 1);
|
||||
|
||||
namespace app\backend\controller;
|
||||
|
||||
use think\facade\View;
|
||||
use Exception;
|
||||
use ywxapp\controller\BackendBase;
|
||||
use ywxapp\library\TemplateManager;
|
||||
use ywxapp\library\TemplateInstaller;
|
||||
|
||||
/**
|
||||
* 模板管理(核心后台内置模块,参考 Discuz! 后台「界面」分类)。
|
||||
* 站点级皮肤能力由 ywxapp/library/TemplateManager、TemplateInstaller 提供,
|
||||
* 与插件解耦,本控制器仅承载后台管理 UI。
|
||||
*/
|
||||
class Template extends BackendBase
|
||||
{
|
||||
protected $noNeedLogin = [];
|
||||
protected $noNeedVerify = [];
|
||||
|
||||
/**
|
||||
* 模板中心页面 + 数据接口。
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
if ($this->request->isAjax()) {
|
||||
$map = TemplateManager::getActiveMap();
|
||||
$settings = TemplateManager::getSettings();
|
||||
$data = [
|
||||
'installed' => TemplateManager::listInstalled(),
|
||||
'addon' => TemplateManager::listaddon(),
|
||||
'active_map' => $map,
|
||||
'global_default' => $map['*'] ?? 'default',
|
||||
'allow_member_select' => ! empty($settings['allow_member_select']),
|
||||
];
|
||||
return $this->result->success($data, '获取成功');
|
||||
}
|
||||
return View::fetch('template/index');
|
||||
}
|
||||
|
||||
/**
|
||||
* 界面设置(Discuz 式:全站默认模板 + 会员自选开关)。
|
||||
* GET 渲染页面;POST 保存。
|
||||
*/
|
||||
public function setting()
|
||||
{
|
||||
if ($this->request->isAjax() && $this->request->isPost()) {
|
||||
try {
|
||||
$default = (string) input('default_template', 'default');
|
||||
$allow = (string) input('allow_member_select', '0');
|
||||
if (! preg_match('/^[a-zA-Z0-9_]*$/', $default)) {
|
||||
return $this->result->error('默认模板标识非法');
|
||||
}
|
||||
// 写入全站默认('default' 表示回退自带视图)
|
||||
TemplateManager::setActive('*', $default);
|
||||
$settings = TemplateManager::getSettings();
|
||||
$settings['allow_member_select'] = ($allow === '1' || $allow === 'on');
|
||||
TemplateManager::setSettings($settings);
|
||||
return $this->result->success([], '已保存');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
return View::fetch('template/setting');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设为全站默认模板(写入 active_map['*'])。
|
||||
*/
|
||||
public function setDefault()
|
||||
{
|
||||
try {
|
||||
$template = (string) input('template', '');
|
||||
if (! preg_match('/^[a-zA-Z0-9_]*$/', $template)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
TemplateManager::setActive('*', $template);
|
||||
TemplateManager::clearOverlayCache();
|
||||
$label = $template === 'default' ? '默认(自带视图)' : $template;
|
||||
return $this->result->success([], '已设全站默认:' . $label);
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用/禁用模板(禁用后即使被绑定也不生效)。
|
||||
*/
|
||||
public function toggle()
|
||||
{
|
||||
try {
|
||||
$name = (string) input('name', '');
|
||||
$enabled = (int) input('enabled', 1);
|
||||
if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
TemplateManager::setEnabled($name, $enabled === 1);
|
||||
TemplateManager::clearOverlayCache();
|
||||
return $this->result->success([], $enabled === 1 ? '已启用' : '已禁用');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置某插件激活的模板。
|
||||
* POST {addon, template};template='default'|'' 表示回退自带视图。
|
||||
*/
|
||||
public function setActive()
|
||||
{
|
||||
try {
|
||||
$addon = (string) input('addon', '');
|
||||
$template = (string) input('template', '');
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $addon)) {
|
||||
return $this->result->error('插件标识非法');
|
||||
}
|
||||
if (!preg_match('/^[a-zA-Z0-9_]*$/', $template)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
TemplateManager::setActive($addon, $template);
|
||||
return $this->result->success([], '已保存');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存某模板的变量(配色)覆盖值。
|
||||
* POST {name, vars: {primary:'#fff', ...}}
|
||||
*/
|
||||
public function saveVariables()
|
||||
{
|
||||
try {
|
||||
$name = (string) input('name', '');
|
||||
if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
$raw = input('vars/a', []);
|
||||
if (! is_array($raw)) {
|
||||
$raw = [];
|
||||
}
|
||||
TemplateManager::saveVariables($name, $raw);
|
||||
return $this->result->success([], '配色已保存');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传并安装模板 zip 包(由 TemplateInstaller 落地文件)。
|
||||
*/
|
||||
public function upload()
|
||||
{
|
||||
try {
|
||||
if (! $this->request->isPost()) {
|
||||
return $this->result->error('仅允许 POST 上传');
|
||||
}
|
||||
$file = $this->request->file('file');
|
||||
if (empty($file)) {
|
||||
return $this->result->error('未收到上传文件');
|
||||
}
|
||||
// 来源校验(同源):非同源直接拒绝,轻量防 CSRF
|
||||
$host = $this->request->host();
|
||||
$referer = $this->request->server('HTTP_REFERER', '');
|
||||
if ($referer !== '' && $host !== '' && stripos($referer, $host) === false) {
|
||||
return $this->result->error('来源校验失败');
|
||||
}
|
||||
// 仅允许 zip 包(扩展名 + MIME 双重校验)
|
||||
$ext = strtolower($file->getOriginalExtension());
|
||||
if ($ext !== 'zip') {
|
||||
return $this->result->error('仅支持 .zip 模板包');
|
||||
}
|
||||
if (! $file->checkMime(['application/zip', 'application/x-zip-compressed', 'application/octet-stream'])) {
|
||||
return $this->result->error('文件类型不合法');
|
||||
}
|
||||
$tmpDir = runtime_path() . 'templates' . DIRECTORY_SEPARATOR . '_upload_' . time() . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($tmpDir)) {
|
||||
@mkdir($tmpDir, 0755, true);
|
||||
}
|
||||
$moved = $file->move($tmpDir, $file->getOriginalName());
|
||||
if (!$moved) {
|
||||
return $this->result->error('文件保存失败:' . $file->getError());
|
||||
}
|
||||
$zipPath = $tmpDir . $moved->getSaveName();
|
||||
$info = TemplateInstaller::install($zipPath, root_path());
|
||||
// 全量清理渲染缓存
|
||||
TemplateManager::clearOverlayCache();
|
||||
@unlink($zipPath);
|
||||
@rmdir($tmpDir);
|
||||
return $this->result->success($info, '模板已安装:' . ($info['title'] ?? $info['name']));
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载模板(删除文件 + 解除绑定)。
|
||||
*/
|
||||
public function uninstall()
|
||||
{
|
||||
try {
|
||||
$name = (string) input('name', '');
|
||||
if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
|
||||
return $this->result->error('模板标识非法');
|
||||
}
|
||||
TemplateManager::uninstall($name);
|
||||
return $this->result->success([], '已卸载');
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出当前模板配置(active_map + settings + variables)为 JSON 文件下载。
|
||||
* 用于备份 / 跨站点克隆皮肤配置。
|
||||
*/
|
||||
public function exportConfig()
|
||||
{
|
||||
$config = [
|
||||
'type' => 'ywxapp-template-config',
|
||||
'version' => '1.0',
|
||||
'exported_at' => date('Y-m-d H:i:s'),
|
||||
'active_map' => TemplateManager::getActiveMap(),
|
||||
'settings' => TemplateManager::getSettings(),
|
||||
];
|
||||
$json = json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||
$filename = 'template-config-' . date('Ymd-His') . '.json';
|
||||
return response($json)->header([
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入模板配置文件(JSON,由 exportConfig 生成)。
|
||||
* 仅采纳「已安装模板」的绑定与变量;未安装的模板绑定会被安全跳过,避免脏数据。
|
||||
*/
|
||||
public function importConfig()
|
||||
{
|
||||
try {
|
||||
if (! $this->request->isPost()) {
|
||||
return $this->result->error('仅允许 POST 上传');
|
||||
}
|
||||
// 同源校验(轻量防 CSRF)
|
||||
$host = $this->request->host();
|
||||
$referer = $this->request->server('HTTP_REFERER', '');
|
||||
if ($referer !== '' && $host !== '' && stripos($referer, $host) === false) {
|
||||
return $this->result->error('来源校验失败');
|
||||
}
|
||||
$file = $this->request->file('file');
|
||||
if (empty($file)) {
|
||||
return $this->result->error('未收到上传文件');
|
||||
}
|
||||
if (strtolower($file->getOriginalExtension()) !== 'json') {
|
||||
return $this->result->error('仅支持 .json 配置文件');
|
||||
}
|
||||
$cfg = json_decode(file_get_contents($file->getRealPath()), true);
|
||||
if (! is_array($cfg) || ($cfg['type'] ?? '') !== 'ywxapp-template-config') {
|
||||
return $this->result->error('不是有效的模板配置文件');
|
||||
}
|
||||
$installed = array_column(TemplateManager::listInstalled(), 'name');
|
||||
$installedSet = array_fill_keys($installed, true);
|
||||
|
||||
// 清洗 active_map:仅保留合法标识且已安装的模板
|
||||
$map = [];
|
||||
$srcMap = $cfg['active_map'] ?? [];
|
||||
foreach ($srcMap as $addon => $tpl) {
|
||||
if ($addon !== '*' && ! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', (string) $addon)) {
|
||||
continue;
|
||||
}
|
||||
$tpl = (string) $tpl;
|
||||
if ($tpl === '' || $tpl === 'default') {
|
||||
$map[$addon] = 'default';
|
||||
} elseif (preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $tpl) && isset($installedSet[$tpl])) {
|
||||
$map[$addon] = $tpl;
|
||||
}
|
||||
}
|
||||
|
||||
// 清洗 settings:allow_member_select / disabled / variables
|
||||
$settings = TemplateManager::getSettings();
|
||||
$src = $cfg['settings'] ?? [];
|
||||
if (is_array($src)) {
|
||||
$settings['allow_member_select'] = ! empty($src['allow_member_select']);
|
||||
$disabled = [];
|
||||
foreach (($src['disabled'] ?? []) as $d) {
|
||||
$d = (string) $d;
|
||||
if (preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $d) && isset($installedSet[$d])) {
|
||||
$disabled[] = $d;
|
||||
}
|
||||
}
|
||||
$settings['disabled'] = $disabled;
|
||||
$variables = [];
|
||||
foreach (($src['variables'] ?? []) as $name => $vars) {
|
||||
$name = (string) $name;
|
||||
if (! preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name) || ! isset($installedSet[$name]) || ! is_array($vars)) {
|
||||
continue;
|
||||
}
|
||||
$clean = [];
|
||||
foreach ($vars as $k => $v) {
|
||||
$k = (string) $k;
|
||||
if (preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $k)) {
|
||||
$clean[$k] = (string) $v;
|
||||
}
|
||||
}
|
||||
$variables[$name] = $clean;
|
||||
}
|
||||
$settings['variables'] = $variables;
|
||||
}
|
||||
|
||||
TemplateManager::writeConfig($map, $settings);
|
||||
TemplateManager::clearOverlayCache();
|
||||
|
||||
$msg = '配置已导入';
|
||||
if (count($map) < count($srcMap)) {
|
||||
$msg .= '(部分未安装模板的绑定已跳过)';
|
||||
}
|
||||
return $this->result->success([], $msg);
|
||||
} catch (Exception $e) {
|
||||
return $this->result->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user