2023-11-17 01:44:01 -05:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use Illuminate\Support\Facades\File;
|
2025-01-06 12:20:11 -05:00
|
|
|
|
use Illuminate\Support\Facades\Log;
|
2025-01-10 08:57:26 -05:00
|
|
|
|
use Illuminate\Support\Facades\View;
|
2025-01-06 12:20:11 -05:00
|
|
|
|
use Illuminate\Http\UploadedFile;
|
|
|
|
|
use Exception;
|
|
|
|
|
use ZipArchive;
|
2023-11-17 01:44:01 -05:00
|
|
|
|
|
|
|
|
|
class ThemeService
|
|
|
|
|
{
|
2025-01-10 08:57:26 -05:00
|
|
|
|
private const SYSTEM_THEME_DIR = 'theme/';
|
|
|
|
|
private const USER_THEME_DIR = '/storage/theme/';
|
2025-01-06 12:20:11 -05:00
|
|
|
|
private const CONFIG_FILE = 'config.json';
|
|
|
|
|
private const SETTING_PREFIX = 'theme_';
|
2025-01-10 08:57:26 -05:00
|
|
|
|
private const SYSTEM_THEMES = ['Xboard', 'v2board'];
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->registerThemeViewPaths();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 注册主题视图路径
|
|
|
|
|
*/
|
|
|
|
|
private function registerThemeViewPaths(): void
|
|
|
|
|
{
|
|
|
|
|
// 系统主题路径
|
|
|
|
|
$systemPath = base_path(self::SYSTEM_THEME_DIR);
|
|
|
|
|
if (File::exists($systemPath)) {
|
|
|
|
|
View::addNamespace('theme', $systemPath);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 用户主题路径
|
|
|
|
|
$userPath = base_path(self::USER_THEME_DIR);
|
|
|
|
|
if (File::exists($userPath)) {
|
|
|
|
|
View::prependNamespace('theme', $userPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取主题视图路径
|
|
|
|
|
*/
|
|
|
|
|
public function getThemeViewPath(string $theme): ?string
|
|
|
|
|
{
|
|
|
|
|
$themePath = $this->getThemePath($theme);
|
|
|
|
|
if (!$themePath) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return $themePath . '/dashboard.blade.php';
|
|
|
|
|
}
|
2023-11-17 01:44:01 -05:00
|
|
|
|
|
2025-01-06 12:20:11 -05:00
|
|
|
|
/**
|
|
|
|
|
* 获取所有可用主题列表
|
|
|
|
|
*/
|
|
|
|
|
public function getList(): array
|
2023-11-17 01:44:01 -05:00
|
|
|
|
{
|
2025-01-10 08:57:26 -05:00
|
|
|
|
$themes = [];
|
|
|
|
|
|
|
|
|
|
// 获取系统主题
|
|
|
|
|
$systemPath = base_path(self::SYSTEM_THEME_DIR);
|
|
|
|
|
if (File::exists($systemPath)) {
|
|
|
|
|
$themes = $this->getThemesFromPath($systemPath, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取用户主题
|
|
|
|
|
$userPath = base_path(self::USER_THEME_DIR);
|
|
|
|
|
if (File::exists($userPath)) {
|
|
|
|
|
$themes = array_merge($themes, $this->getThemesFromPath($userPath, true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $themes;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 从指定路径获取主题列表
|
|
|
|
|
*/
|
|
|
|
|
private function getThemesFromPath(string $path, bool $canDelete): array
|
|
|
|
|
{
|
2025-01-06 12:20:11 -05:00
|
|
|
|
return collect(File::directories($path))
|
2025-01-10 08:57:26 -05:00
|
|
|
|
->mapWithKeys(function ($dir) use ($canDelete) {
|
2025-01-06 12:20:11 -05:00
|
|
|
|
$name = basename($dir);
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 检查必要文件是否存在
|
|
|
|
|
if (
|
|
|
|
|
!File::exists($dir . '/' . self::CONFIG_FILE) ||
|
|
|
|
|
!File::exists($dir . '/dashboard.blade.php')
|
|
|
|
|
) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
$config = $this->readConfigFile($name);
|
2025-01-10 08:57:26 -05:00
|
|
|
|
if (!$config) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$config['can_delete'] = $canDelete && $name !== admin_setting('current_theme');
|
|
|
|
|
$config['is_system'] = !$canDelete;
|
|
|
|
|
return [$name => $config];
|
2025-01-06 12:20:11 -05:00
|
|
|
|
})->toArray();
|
2023-11-17 01:44:01 -05:00
|
|
|
|
}
|
|
|
|
|
|
2025-01-06 12:20:11 -05:00
|
|
|
|
/**
|
|
|
|
|
* 上传新主题
|
|
|
|
|
*/
|
|
|
|
|
public function upload(UploadedFile $file): bool
|
2023-11-17 01:44:01 -05:00
|
|
|
|
{
|
2025-01-06 12:20:11 -05:00
|
|
|
|
$zip = new ZipArchive;
|
|
|
|
|
$tmpPath = storage_path('tmp/' . uniqid());
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if ($zip->open($file->path()) !== true) {
|
2025-01-10 08:57:26 -05:00
|
|
|
|
throw new Exception('无效的主题包');
|
2025-01-06 12:20:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 查找配置文件
|
|
|
|
|
$configEntry = collect(range(0, $zip->numFiles - 1))
|
|
|
|
|
->map(fn($i) => $zip->getNameIndex($i))
|
|
|
|
|
->first(fn($name) => basename($name) === self::CONFIG_FILE);
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
if (!$configEntry) {
|
|
|
|
|
throw new Exception('主题配置文件不存在');
|
2025-01-06 12:20:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 解压并读取配置
|
2025-01-06 12:20:11 -05:00
|
|
|
|
$zip->extractTo($tmpPath);
|
|
|
|
|
$zip->close();
|
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
$sourcePath = $tmpPath . '/' . rtrim(dirname($configEntry), '.');
|
|
|
|
|
$configFile = $sourcePath . '/' . self::CONFIG_FILE;
|
|
|
|
|
|
|
|
|
|
if (!File::exists($configFile)) {
|
|
|
|
|
throw new Exception('主题配置文件不存在');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$config = json_decode(File::get($configFile), true);
|
|
|
|
|
if (empty($config['name'])) {
|
|
|
|
|
throw new Exception('主题名称未配置');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否为系统主题
|
|
|
|
|
if (in_array($config['name'], self::SYSTEM_THEMES)) {
|
|
|
|
|
throw new Exception('不能上传与系统主题同名的主题');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查必要文件
|
|
|
|
|
if (!File::exists($sourcePath . '/dashboard.blade.php')) {
|
|
|
|
|
throw new Exception('缺少必要的主题文件:dashboard.blade.php');
|
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 确保目标目录存在
|
|
|
|
|
$userThemePath = base_path(self::USER_THEME_DIR);
|
|
|
|
|
if (!File::exists($userThemePath)) {
|
|
|
|
|
File::makeDirectory($userThemePath, 0755, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$targetPath = $userThemePath . $config['name'];
|
2025-01-06 12:20:11 -05:00
|
|
|
|
if (File::exists($targetPath)) {
|
2025-01-10 08:57:26 -05:00
|
|
|
|
throw new Exception('主题已存在');
|
2025-01-06 12:20:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
File::moveDirectory($sourcePath, $targetPath);
|
|
|
|
|
$this->initConfig($config['name']);
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
} catch (Exception $e) {
|
2025-01-10 08:57:26 -05:00
|
|
|
|
throw $e;
|
|
|
|
|
} finally {
|
|
|
|
|
// 清理临时文件
|
2025-01-06 12:20:11 -05:00
|
|
|
|
if (File::exists($tmpPath)) {
|
|
|
|
|
File::deleteDirectory($tmpPath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 切换主题
|
|
|
|
|
*/
|
|
|
|
|
public function switch(string $theme): bool
|
|
|
|
|
{
|
|
|
|
|
$currentTheme = admin_setting('current_theme');
|
|
|
|
|
if ($theme === $currentTheme) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 验证主题是否存在
|
|
|
|
|
$themePath = $this->getThemePath($theme);
|
|
|
|
|
if (!$themePath) {
|
|
|
|
|
throw new Exception('主题不存在');
|
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 验证视图文件是否存在
|
|
|
|
|
if (!File::exists($this->getThemeViewPath($theme))) {
|
|
|
|
|
throw new Exception('主题视图文件不存在');
|
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 复制主题文件到public目录
|
|
|
|
|
$targetPath = public_path('theme/' . $theme);
|
|
|
|
|
if (!File::copyDirectory($themePath, $targetPath)) {
|
|
|
|
|
throw new Exception('复制主题文件失败');
|
2025-01-06 12:20:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 清理旧主题文件
|
|
|
|
|
if ($currentTheme) {
|
2025-01-10 08:57:26 -05:00
|
|
|
|
$oldPath = public_path('theme/' . $currentTheme);
|
|
|
|
|
if (File::exists($oldPath)) {
|
|
|
|
|
File::deleteDirectory($oldPath);
|
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
admin_setting(['current_theme' => $theme]);
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
Log::error('Theme switch failed', ['theme' => $theme, 'error' => $e->getMessage()]);
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 删除主题
|
|
|
|
|
*/
|
|
|
|
|
public function delete(string $theme): bool
|
|
|
|
|
{
|
2023-11-17 01:44:01 -05:00
|
|
|
|
try {
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 检查是否为系统主题
|
|
|
|
|
if (in_array($theme, self::SYSTEM_THEMES)) {
|
|
|
|
|
throw new Exception('系统主题不能删除');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查是否为当前使用的主题
|
|
|
|
|
if ($theme === admin_setting('current_theme')) {
|
|
|
|
|
throw new Exception('当前使用的主题不能删除');
|
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 获取主题路径
|
|
|
|
|
$themePath = base_path(self::USER_THEME_DIR . $theme);
|
2025-01-06 12:20:11 -05:00
|
|
|
|
if (!File::exists($themePath)) {
|
2025-01-10 08:57:26 -05:00
|
|
|
|
throw new Exception('主题不存在');
|
2025-01-06 12:20:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 删除主题文件
|
2025-01-06 12:20:11 -05:00
|
|
|
|
File::deleteDirectory($themePath);
|
2025-01-10 08:57:26 -05:00
|
|
|
|
|
|
|
|
|
// 删除public目录下的主题文件
|
|
|
|
|
$publicPath = public_path('theme/' . $theme);
|
|
|
|
|
if (File::exists($publicPath)) {
|
|
|
|
|
File::deleteDirectory($publicPath);
|
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
|
|
|
|
// 清理主题配置
|
|
|
|
|
admin_setting([self::SETTING_PREFIX . $theme => null]);
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
Log::error('Theme deletion failed', ['theme' => $theme, 'error' => $e->getMessage()]);
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-10 08:57:26 -05:00
|
|
|
|
/**
|
|
|
|
|
* 检查主题是否存在
|
|
|
|
|
*/
|
|
|
|
|
public function exists(string $theme): bool
|
|
|
|
|
{
|
|
|
|
|
return $this->getThemePath($theme) !== null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取主题路径
|
|
|
|
|
*/
|
|
|
|
|
private function getThemePath(string $theme): ?string
|
|
|
|
|
{
|
|
|
|
|
$systemPath = base_path(self::SYSTEM_THEME_DIR . $theme);
|
|
|
|
|
if (File::exists($systemPath)) {
|
|
|
|
|
return $systemPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$userPath = base_path(self::USER_THEME_DIR . $theme);
|
|
|
|
|
if (File::exists($userPath)) {
|
|
|
|
|
return $userPath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-06 12:20:11 -05:00
|
|
|
|
/**
|
|
|
|
|
* 获取主题配置
|
|
|
|
|
*/
|
|
|
|
|
public function getConfig(string $theme): ?array
|
|
|
|
|
{
|
|
|
|
|
$config = admin_setting(self::SETTING_PREFIX . $theme);
|
|
|
|
|
if ($config === null) {
|
|
|
|
|
$this->initConfig($theme);
|
|
|
|
|
$config = admin_setting(self::SETTING_PREFIX . $theme);
|
2023-11-17 01:44:01 -05:00
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
return $config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 更新主题配置
|
|
|
|
|
*/
|
|
|
|
|
public function updateConfig(string $theme, array $config): bool
|
|
|
|
|
{
|
|
|
|
|
try {
|
2025-01-10 08:57:26 -05:00
|
|
|
|
// 验证主题是否存在
|
|
|
|
|
if (!$this->getThemePath($theme)) {
|
|
|
|
|
throw new Exception('主题不存在');
|
|
|
|
|
}
|
|
|
|
|
|
2025-01-06 12:20:11 -05:00
|
|
|
|
$schema = $this->readConfigFile($theme);
|
2025-01-10 08:57:26 -05:00
|
|
|
|
if (!$schema) {
|
|
|
|
|
throw new Exception('主题配置文件无效');
|
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
|
|
|
|
// 只保留有效的配置字段
|
|
|
|
|
$validFields = collect($schema['configs'] ?? [])->pluck('field_name')->toArray();
|
|
|
|
|
$validConfig = collect($config)
|
|
|
|
|
->only($validFields)
|
|
|
|
|
->toArray();
|
|
|
|
|
|
|
|
|
|
$currentConfig = $this->getConfig($theme) ?? [];
|
|
|
|
|
$newConfig = array_merge($currentConfig, $validConfig);
|
|
|
|
|
|
|
|
|
|
admin_setting([self::SETTING_PREFIX . $theme => $newConfig]);
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
} catch (Exception $e) {
|
|
|
|
|
Log::error('Config update failed', ['theme' => $theme, 'error' => $e->getMessage()]);
|
|
|
|
|
throw $e;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 读取主题配置文件
|
|
|
|
|
*/
|
|
|
|
|
private function readConfigFile(string $theme): ?array
|
|
|
|
|
{
|
2025-01-10 08:57:26 -05:00
|
|
|
|
$themePath = $this->getThemePath($theme);
|
|
|
|
|
if (!$themePath) {
|
|
|
|
|
return null;
|
2025-01-06 12:20:11 -05:00
|
|
|
|
}
|
2025-01-10 08:57:26 -05:00
|
|
|
|
|
|
|
|
|
$file = $themePath . '/' . self::CONFIG_FILE;
|
|
|
|
|
return File::exists($file) ? json_decode(File::get($file), true) : null;
|
2025-01-06 12:20:11 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 初始化主题配置
|
|
|
|
|
*/
|
|
|
|
|
private function initConfig(string $theme): void
|
|
|
|
|
{
|
|
|
|
|
$config = $this->readConfigFile($theme);
|
2025-01-10 08:57:26 -05:00
|
|
|
|
if (!$config) {
|
2025-01-06 12:20:11 -05:00
|
|
|
|
return;
|
2025-01-10 08:57:26 -05:00
|
|
|
|
}
|
2025-01-06 12:20:11 -05:00
|
|
|
|
|
|
|
|
|
$defaults = collect($config['configs'] ?? [])
|
|
|
|
|
->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? ''])
|
|
|
|
|
->toArray();
|
|
|
|
|
admin_setting([self::SETTING_PREFIX . $theme => $defaults]);
|
2023-11-17 01:44:01 -05:00
|
|
|
|
}
|
|
|
|
|
}
|