// +---------------------------------------------------------------------- namespace ywxapp\library; /** * 站点级 layui 主题变量层(整站换肤的唯一入口)。 * * 与 SkinVariables(模板级、仅插件上下文生效)的定位区别: * - SkinVariables:某套皮肤自己的变量(--primary / --secondary), * 由 template.json 声明,用于该皮肤自己的视图与 CSS。 * - SiteTheme:整站 layui 组件的底色(--ywx-primary / --ywx-accent 等), * 对所有页面生效(后台 / 会员 / 前台 / 插件前台),与皮肤层互不干扰。 * * 变量名对应见 config/theme.php;CSS 消费方是 public/static/theme/layui-theme.css * (由 layui.css 自动提取生成:主色硬编码已改写为 var(--ywx-*))。 * * 安全:变量值会被拼进 '; } /** * 保存后台配置的变量值到 config/theme.php(保留注释头),并同步内存配置。 * * @param array $vars ['primary' => '#5b8cff', ...];非法值直接丢弃 * @return int 实际写入的变量数 */ public static function save(array $vars): int { $clean = []; foreach (self::DEFAULTS as $key => $default) { if (! isset($vars[$key])) { continue; } $val = trim((string) $vars[$key]); if ($val === '') { // 显式留空 = 恢复该变量的默认值(raw()/values() 会自动回落) $clean[$key] = ''; continue; } if (! self::validate($key, $val)) { continue; // 非法值直接丢弃,防止 CSS 注入 } $clean[$key] = $val; } $current = config('theme', []); $current['variables'] = array_merge( is_array(config('theme.variables', [])) ? config('theme.variables', []) : [], $clean ); $current['enable'] = $current['enable'] ?? true; self::writeConfig($current); return count($clean); } /** * 值白名单校验:只允许「CSS 安全」取值,禁止 ; { } < > @ url( 等逃逸字符。 */ public static function validate(string $key, string $value): bool { $v = $value; // 通用禁止:CSS 上下文逃逸字符 if (preg_match('/[;{}<>]/', $v)) { return false; } if (stripos($v, 'url(') !== false || strpos($v, '@') !== false) { return false; } // 控制字符(换行可注入新行导致解析异常) if (preg_match('/[\x00-\x1F\x7F]/', $v)) { return false; } switch ($key) { case 'primary': case 'accent': // 颜色:hex / rgb() / rgba() / 颜色关键字 return (bool) preg_match('/^(#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|rgba?\(\s*[\d.,%\s]+\)|[a-zA-Z]{3,20})$/', $v); case 'radius': return (bool) preg_match('/^[\d.]+(?:px|rem|em|%)?$/', $v); case 'font': return (bool) preg_match('/^[\w\s,\'"\-]+$/u', $v); default: return false; } } /** * 写回 config/theme.php(含注释头),同步内存配置使当前请求立即生效。 */ protected static function writeConfig(array $data): void { $file = config_path() . 'theme.php'; $vars = is_array($data['variables'] ?? null) ? $data['variables'] : []; $lines = []; foreach (self::DEFAULTS as $key => $default) { $val = isset($vars[$key]) ? (string) $vars[$key] : ''; $lines[] = " '" . $key . "' => '" . addslashes($val) . "',"; } $linesStr = implode("\n", $lines); $enable = ! empty($data['enable']) ? 'true' : 'false'; $content = << --ywx-primary 主色(按钮/表单聚焦/进度条/时间轴) // | accent -> --ywx-accent 强调色(导航/分页当前页/上传/logo) // | radius -> --ywx-radius 统一圆角 // | font -> --ywx-font 正文字体 // | // | 改这里即可全站换肤,无需改动任何视图文件、也无需清理缓存。 // | 后台可在「模板/主题」界面写入本文件(见 SiteTheme::save())。 // +---------------------------------------------------------------------- return [ // 主题开关:关闭后不注入任何主题样式(用于排查样式冲突) 'enable' => {$enable}, // 站点主题变量(值为空表示使用 layui-theme.css 内的兜底默认值) 'variables' => [ {$linesStr} ], ]; PHP; file_put_contents($file, $content); // 刚写入的 PHP 配置可能被 OPcache 缓存 2 秒(revalidate_freq),显式失效保证即时换肤 if (function_exists('opcache_invalidate')) { @opcache_invalidate($file, true); } \think\facade\Config::set(['enable' => ! empty($data['enable']), 'variables' => $vars], 'theme'); } }