registerThemeViewPaths(); } /** * Register theme view paths */ 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); } } /** * Get theme view path */ public function getThemeViewPath(string $theme): ?string { $themePath = $this->getThemePath($theme); if (!$themePath) { return null; } return $themePath . '/dashboard.blade.php'; } /** * Get all available themes */ public function getList(): array { $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; } /** * Get themes from specified path */ private function getThemesFromPath(string $path, bool $canDelete): array { return collect(File::directories($path)) ->mapWithKeys(function ($dir) use ($canDelete) { $name = basename($dir); if ( !File::exists($dir . '/' . self::CONFIG_FILE) || !File::exists($dir . '/dashboard.blade.php') ) { return []; } $config = $this->readConfigFile($name); if (!$config) { return []; } $config['can_delete'] = $canDelete && $name !== admin_setting('current_theme'); $config['is_system'] = !$canDelete; return [$name => $config]; })->toArray(); } /** * Upload new theme */ public function upload(UploadedFile $file): bool { $zip = new ZipArchive; $tmpPath = storage_path('tmp/' . uniqid()); try { if ($zip->open($file->path()) !== true) { throw new Exception('Invalid theme package'); } $configEntry = collect(range(0, $zip->numFiles - 1)) ->map(fn($i) => $zip->getNameIndex($i)) ->first(fn($name) => basename($name) === self::CONFIG_FILE); if (!$configEntry) { throw new Exception('Theme config file not found'); } $zip->extractTo($tmpPath); $zip->close(); $sourcePath = $tmpPath . '/' . rtrim(dirname($configEntry), '.'); $configFile = $sourcePath . '/' . self::CONFIG_FILE; if (!File::exists($configFile)) { throw new Exception('Theme config file not found'); } $config = json_decode(File::get($configFile), true); if (empty($config['name'])) { throw new Exception('Theme name not configured'); } if (in_array($config['name'], self::SYSTEM_THEMES)) { throw new Exception('Cannot upload theme with same name as system theme'); } if (!File::exists($sourcePath . '/dashboard.blade.php')) { throw new Exception('Missing required theme file: dashboard.blade.php'); } $userThemePath = base_path(self::USER_THEME_DIR); if (!File::exists($userThemePath)) { File::makeDirectory($userThemePath, 0755, true); } $targetPath = $userThemePath . $config['name']; if (File::exists($targetPath)) { $oldConfigFile = $targetPath . '/config.json'; if (!File::exists($oldConfigFile)) { throw new Exception('Existing theme missing config file'); } $oldConfig = json_decode(File::get($oldConfigFile), true); $oldVersion = $oldConfig['version'] ?? '0.0.0'; $newVersion = $config['version'] ?? '0.0.0'; if (version_compare($newVersion, $oldVersion, '>')) { $this->cleanupThemeFiles($config['name']); File::deleteDirectory($targetPath); File::copyDirectory($sourcePath, $targetPath); // 更新主题时保留用户配置 $this->initConfig($config['name'], true); return true; } else { throw new Exception('Theme exists and not a newer version'); } } File::copyDirectory($sourcePath, $targetPath); $this->initConfig($config['name']); return true; } catch (Exception $e) { throw $e; } finally { if (File::exists($tmpPath)) { File::deleteDirectory($tmpPath); } } } /** * Switch theme */ public function switch(string|null $theme): bool { if ($theme === null) { return true; } $currentTheme = admin_setting('current_theme'); try { $themePath = $this->getThemePath($theme); if (!$themePath) { throw new Exception('Theme not found'); } if (!File::exists($this->getThemeViewPath($theme))) { throw new Exception('Theme view file not found'); } if ($currentTheme && $currentTheme !== $theme) { $this->cleanupThemeFiles($currentTheme); } $targetPath = public_path('theme/' . $theme); if (!File::copyDirectory($themePath, $targetPath)) { throw new Exception('Failed to copy theme files'); } admin_setting(['current_theme' => $theme]); return true; } catch (Exception $e) { Log::error('Theme switch failed', ['theme' => $theme, 'error' => $e->getMessage()]); throw $e; } } /** * Delete theme */ public function delete(string $theme): bool { try { if (in_array($theme, self::SYSTEM_THEMES)) { throw new Exception('System theme cannot be deleted'); } if ($theme === admin_setting('current_theme')) { throw new Exception('Current theme cannot be deleted'); } $themePath = base_path(self::USER_THEME_DIR . $theme); if (!File::exists($themePath)) { throw new Exception('Theme not found'); } $this->cleanupThemeFiles($theme); File::deleteDirectory($themePath); admin_setting([self::SETTING_PREFIX . $theme => null]); return true; } catch (Exception $e) { Log::error('Theme deletion failed', ['theme' => $theme, 'error' => $e->getMessage()]); throw $e; } } /** * Check if theme exists */ public function exists(string $theme): bool { return $this->getThemePath($theme) !== null; } /** * Get theme path */ public 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; } /** * Get theme config */ 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); } return $config; } /** * Update theme config */ public function updateConfig(string $theme, array $config): bool { try { if (!$this->getThemePath($theme)) { throw new Exception('Theme not found'); } $schema = $this->readConfigFile($theme); if (!$schema) { throw new Exception('Invalid theme config file'); } $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; } } /** * Read theme config file */ private function readConfigFile(string $theme): ?array { $themePath = $this->getThemePath($theme); if (!$themePath) { return null; } $file = $themePath . '/' . self::CONFIG_FILE; return File::exists($file) ? json_decode(File::get($file), true) : null; } /** * Clean up theme files including public directory */ public function cleanupThemeFiles(string $theme): void { try { $publicThemePath = public_path('theme/' . $theme); if (File::exists($publicThemePath)) { File::deleteDirectory($publicThemePath); Log::info('Cleaned up public theme files', ['theme' => $theme, 'path' => $publicThemePath]); } $cacheKey = "theme_{$theme}_assets"; if (cache()->has($cacheKey)) { cache()->forget($cacheKey); Log::info('Cleaned up theme cache', ['theme' => $theme, 'cache_key' => $cacheKey]); } } catch (Exception $e) { Log::warning('Failed to cleanup theme files', [ 'theme' => $theme, 'error' => $e->getMessage() ]); } } /** * Force refresh current theme public files */ public function refreshCurrentTheme(): bool { try { $currentTheme = admin_setting('current_theme'); if (!$currentTheme) { return false; } $this->cleanupThemeFiles($currentTheme); $themePath = $this->getThemePath($currentTheme); if (!$themePath) { throw new Exception('Current theme path not found'); } $targetPath = public_path('theme/' . $currentTheme); if (!File::copyDirectory($themePath, $targetPath)) { throw new Exception('Failed to copy theme files'); } Log::info('Refreshed current theme files', ['theme' => $currentTheme]); return true; } catch (Exception $e) { Log::error('Failed to refresh current theme', [ 'theme' => $currentTheme, 'error' => $e->getMessage() ]); return false; } } /** * Initialize theme config * * @param string $theme 主题名称 * @param bool $preserveExisting 是否保留现有配置(更新主题时使用) */ private function initConfig(string $theme, bool $preserveExisting = false): void { $config = $this->readConfigFile($theme); if (!$config) { return; } $defaults = collect($config['configs'] ?? []) ->mapWithKeys(fn($col) => [$col['field_name'] => $col['default_value'] ?? '']) ->toArray(); if ($preserveExisting) { $existingConfig = admin_setting(self::SETTING_PREFIX . $theme) ?? []; $mergedConfig = array_merge($defaults, $existingConfig); admin_setting([self::SETTING_PREFIX . $theme => $mergedConfig]); } else { admin_setting([self::SETTING_PREFIX . $theme => $defaults]); } } }