chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
+330
View File
@@ -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;
}
}
// 清洗 settingsallow_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());
}
}
}