更新显示架构

This commit is contained in:
扫地僧 2026-03-09 09:40:36 +08:00
parent 99fc99f8ad
commit 4cbc52c51b
14 changed files with 1162 additions and 1 deletions

View File

@ -0,0 +1,180 @@
<?php
declare(strict_types=1);
namespace app\admin\controller\Cms\Demand;
use app\index\BaseController;
use Symfony\Component\VarDumper\VarDumper;
use think\exception\ValidateException;
use think\facade\Request;
use think\facade\Session;
use think\response\Json;
use think\db\exception\DbException;
use app\model\Cms\Demand;
use app\model\Cms\DemandCategory;
class DemandController extends BaseController
{
/**
* 获取需求列表
* @return Json
*/
public function getDemandList(): Json
{
// 查询分类
$demandList = Demand::where('delete_time', null)
->order('id', 'desc')
->select();
if (!$demandList) {
return json([
'code' => 200,
'msg' => 'success',
'list' => [],
]);
}
return json([
'code' => 200,
'msg' => 'success',
'list' => $demandList,
]);
}
/**
* 增加需求
* @return Json
*/
public function addDemand(): Json
{
try {
$data = Request::only(['title', 'desc', 'applicant', 'status']);
// 验证数据
if (empty($data['title']) || empty($data['desc'])) {
return json([
'code' => 400,
'msg' => '标题和描述不能为空',
]);
}
// 创建需求
$demand = Demand::create([
'title' => $data['title'],
'desc' => $data['desc'],
'applicant' => $data['applicant'] ?? '',
'status' => $data['status'] ?? 'pending',
]);
return json([
'code' => 200,
'msg' => '添加成功',
'data' => $demand,
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '添加失败:' . $e->getMessage(),
]);
}
}
/**
* 编辑需求
* @return Json
*/
public function editDemand(): Json
{
try {
$id = Request::param('id');
$data = Request::only(['title', 'desc', 'applicant', 'status']);
// 验证数据
if (empty($id)) {
return json([
'code' => 400,
'msg' => '需求ID不能为空',
]);
}
if (empty($data['title']) || empty($data['desc'])) {
return json([
'code' => 400,
'msg' => '标题和描述不能为空',
]);
}
// 查找需求
$demand = Demand::find($id);
if (!$demand) {
return json([
'code' => 404,
'msg' => '需求不存在',
]);
}
// 更新需求
$demand->save([
'title' => $data['title'],
'desc' => $data['desc'],
'applicant' => $data['applicant'] ?? '',
'status' => $data['status'] ?? 'pending',
]);
return json([
'code' => 200,
'msg' => '编辑成功',
'data' => $demand,
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '编辑失败:' . $e->getMessage(),
]);
}
}
/**
* 删除需求
* @return Json
*/
public function deleteDemand(): Json
{
try {
$id = Request::param('id');
// 验证数据
if (empty($id)) {
return json([
'code' => 400,
'msg' => '需求ID不能为空',
]);
}
// 查找需求
$demand = Demand::find($id);
if (!$demand) {
return json([
'code' => 404,
'msg' => '需求不存在',
]);
}
// 软删除
$demand->delete();
return json([
'code' => 200,
'msg' => '删除成功',
]);
} catch (\Exception $e) {
return json([
'code' => 500,
'msg' => '删除失败:' . $e->getMessage(),
]);
}
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace app\admin\controller;
use app\admin\BaseController;
use app\service\ThemeService;
use think\facade\Request;
/**
* 模板管理控制器
*/
class ThemeController extends BaseController
{
private ThemeService $themeService;
public function __construct()
{
$this->themeService = new ThemeService();
}
/**
* 获取模板列表(后台管理)
* @return \think\response\Json
*/
public function index()
{
$themes = $this->themeService->getThemeList();
$currentTheme = $this->themeService->getCurrentTheme();
return json([
'code' => 200,
'msg' => 'success',
'data' => [
'list' => $themes,
'currentTheme' => $currentTheme
]
]);
}
/**
* 切换模板
* @return \think\response\Json
*/
public function switch()
{
$themeKey = Request::post('theme_key', '');
if (empty($themeKey)) {
return json([
'code' => 400,
'msg' => '模板标识不能为空'
]);
}
$result = $this->themeService->switchTheme($themeKey);
if ($result) {
return json([
'code' => 200,
'msg' => '切换成功'
]);
}
return json([
'code' => 400,
'msg' => '切换失败,模板不存在'
]);
}
/**
* 获取模板字段数据
* @return \think\response\Json
*/
public function getData()
{
$themeKey = Request::get('theme_key', '');
$themeData = $this->themeService->getThemeData($themeKey ?: null);
return json([
'code' => 200,
'msg' => 'success',
'data' => $themeData
]);
}
/**
* 保存模板字段数据
* @return \think\response\Json
*/
public function saveData()
{
$themeKey = Request::post('theme_key', '');
$fieldKey = Request::post('field_key', '');
$fieldValue = Request::post('field_value', '');
if (empty($themeKey) || empty($fieldKey)) {
return json([
'code' => 400,
'msg' => '参数不完整'
]);
}
$result = $this->themeService->saveThemeField($themeKey, $fieldKey, $fieldValue);
if ($result) {
return json([
'code' => 200,
'msg' => '保存成功'
]);
}
return json([
'code' => 400,
'msg' => '保存失败'
]);
}
}

View File

@ -0,0 +1,8 @@
<?php
use think\facade\Route;
// 需求路由
Route::get('demandList', 'app\\admin\\controller\\Cms\\Demand\\DemandController@getDemandList');
Route::post('addDemand', 'app\\admin\\controller\\Cms\\Demand\\DemandController@addDemand');
Route::post('editDemand/:id', 'app\\admin\\controller\\Cms\\Demand\\DemandController@editDemand');
Route::post('deleteDemand/:id', 'app\\admin\\controller\\Cms\\Demand\\DemandController@deleteDemand');

View File

@ -0,0 +1,8 @@
<?php
use think\facade\Route;
// 模板管理路由
Route::get('theme', 'app\admin\controller\ThemeController@index');
Route::post('theme/switch', 'app\admin\controller\ThemeController@switch');
Route::get('theme/data', 'app\admin\controller\ThemeController@getData');
Route::post('theme/data', 'app\admin\controller\ThemeController@saveData');

View File

@ -0,0 +1,8 @@
<?php
use think\facade\Route;
// 模板相关路由
Route::get('theme', 'app\admin\controller\ThemeController@index');
Route::post('theme/switch', 'app\admin\controller\ThemeController@switch');
Route::get('theme/data', 'app\admin\controller\ThemeController@getData');
Route::post('theme/data', 'app\admin\controller\ThemeController@saveData');

View File

@ -8,17 +8,45 @@ use app\model\Banner;
use app\index\BaseController; use app\index\BaseController;
use app\model\FrontMenu; use app\model\FrontMenu;
use app\model\OnePage; use app\model\OnePage;
use app\model\System\SystemSiteSettings;
use app\service\ThemeService;
use think\db\exception\DbException; use think\db\exception\DbException;
use think\facade\Env; use think\facade\Env;
use app\model\System\SystemSiteSettings;
class Index extends BaseController class Index extends BaseController
{ {
private ThemeService $themeService;
public function __construct()
{
$this->themeService = new ThemeService();
}
public function index() public function index()
{ {
return view('index/index'); return view('index/index');
} }
/**
* 前端初始化接口 - 返回当前模板和填充数据
* @return \think\response\Json
*/
public function init()
{
// 直接返回默认模板数据
return json([
'code' => 200,
'msg' => 'success',
'data' => [
'theme_key' => 'default',
'theme_path' => '/themes/default/index.html',
'data' => [
'site_name' => '企业官网'
]
]
]);
}
/** /**
* 获取日志列表 * 获取日志列表
*/ */

View File

@ -5,6 +5,9 @@ use think\facade\Route;
Route::get('/', 'app\index\controller\Index@index'); Route::get('/', 'app\index\controller\Index@index');
Route::get('index/index', 'app\index\controller\Index@index'); Route::get('index/index', 'app\index\controller\Index@index');
// --- 模板初始化接口 ---
Route::get('init', 'app\index\controller\Index@init');
// --- 前端底部数据路由 --- // --- 前端底部数据路由 ---
Route::get('footerdata', 'app\index\controller\Index@getFooterData'); Route::get('footerdata', 'app\index\controller\Index@getFooterData');

44
app/model/Cms/Demand.php Normal file
View File

@ -0,0 +1,44 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: Liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace app\model\Cms;
use think\Model;
use think\model\concern\SoftDelete;
/**
* 文章分类模型
*/
class Demand extends Model
{
// 启用软删除
use SoftDelete;
// 数据库表名
protected $name = 'mete_demand';
// 字段类型转换
protected $type = [
'id' => 'integer',
'tid' => 'integer',
'title' => 'string',
'desc' => 'string',
'applicant' => 'string',
'phone' => 'string',
'status' => 'integer',
'create_time' => 'datetime',
'update_time' => 'datetime',
'delete_time' => 'datetime',
];
}

View File

@ -0,0 +1,43 @@
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: Liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace app\model\Cms;
use think\Model;
use think\model\concern\SoftDelete;
/**
* 文章分类模型
*/
class DemandCategory extends Model
{
// 启用软删除
use SoftDelete;
// 数据库表名
protected $name = 'mete_demand_category';
// 字段类型转换
protected $type = [
'id' => 'integer',
'title' => 'string',
'desc' => 'string',
'applicant' => 'string',
'phone' => 'string',
'status' => 'integer',
'create_time' => 'datetime',
'update_time' => 'datetime',
'delete_time' => 'datetime',
];
}

View File

@ -0,0 +1,258 @@
<?php
declare(strict_types=1);
namespace app\service;
use think\facade\Db;
use think\facade\Config;
/**
* 模板服务类
* 负责扫描 public/themes 目录,管理模板配置
*/
class ThemeService
{
private string $themesPath;
public function __construct()
{
$this->themesPath = root_path() . 'public' . DIRECTORY_SEPARATOR . 'themes';
}
/**
* 获取所有可用模板列表
* @return array
*/
public function getThemeList(): array
{
$themes = [];
$dirs = $this->scanThemeDirs();
foreach ($dirs as $dir) {
$config = $this->readThemeConfig($dir);
$preview = $this->getThemePreview($dir);
$themes[] = [
'key' => $dir,
'name' => $config['name'] ?? $dir,
'description'=> $config['description'] ?? '',
'version' => $config['version'] ?? '1.0.0',
'author' => $config['author'] ?? '',
'preview' => $preview,
'path' => '/themes/' . $dir . '/index.html',
'fields' => $config['fields'] ?? [],
];
}
return $themes;
}
/**
* 扫描模板目录
* @return array
*/
private function scanThemeDirs(): array
{
$dirs = [];
if (!is_dir($this->themesPath)) {
return $dirs;
}
$items = scandir($this->themesPath);
foreach ($items as $item) {
if ($item === '.' || $item === '..') {
continue;
}
$fullPath = $this->themesPath . DIRECTORY_SEPARATOR . $item;
if (is_dir($fullPath) && is_file($fullPath . DIRECTORY_SEPARATOR . 'index.html')) {
$dirs[] = $item;
}
}
return $dirs;
}
/**
* 读取模板配置文件
* @param string $themeDir
* @return array
*/
private function readThemeConfig(string $themeDir): array
{
$configPath = $this->themesPath . DIRECTORY_SEPARATOR . $themeDir . DIRECTORY_SEPARATOR . 'config.json';
if (!is_file($configPath)) {
return [];
}
$content = file_get_contents($configPath);
$config = json_decode($content, true);
return $config ?? [];
}
/**
* 获取模板预览图
* @param string $themeDir
* @return string
*/
private function getThemePreview(string $themeDir): string
{
$previewPath = '/themes/' . $themeDir . '/preview.png';
// 如果 preview.png 不存在,使用默认占位图
$fullPath = $this->themesPath . DIRECTORY_SEPARATOR . $themeDir . DIRECTORY_SEPARATOR . 'preview.png';
if (!is_file($fullPath)) {
return 'https://picsum.photos/300/200?random=' . ord($themeDir[0]);
}
return $previewPath;
}
/**
* 获取当前激活的模板Key
* @return string
*/
public function getCurrentTheme(): string
{
try {
$config = Db::name('mete_template_site_config')
->where('key', 'current_theme')
->where('delete_time', null)
->find();
return $config['value'] ?? 'default';
} catch (\Exception $e) {
return 'default';
}
}
/**
* 切换当前模板
* @param string $themeKey
* @return bool
*/
public function switchTheme(string $themeKey): bool
{
// 验证模板是否存在
$themes = $this->getThemeList();
$exists = false;
foreach ($themes as $theme) {
if ($theme['key'] === $themeKey) {
$exists = true;
break;
}
}
if (!$exists) {
return false;
}
try {
// 查找或创建配置记录
$config = Db::name('mete_template_site_config')
->where('key', 'current_theme')
->where('delete_time', null)
->find();
$now = date('Y-m-d H:i:s');
if ($config) {
Db::name('mete_template_site_config')->where('id', $config['id'])->update([
'value' => $themeKey,
'update_time' => $now
]);
} else {
Db::name('mete_template_site_config')->insert([
'key' => 'current_theme',
'value' => $themeKey,
'create_time' => $now,
'update_time' => $now
]);
}
return true;
} catch (\Exception $e) {
return false;
}
}
/**
* 获取模板数据(用于前端渲染)
* @param string|null $themeKey
* @return array
*/
public function getThemeData(?string $themeKey = null): array
{
$themeKey = $themeKey ?? $this->getCurrentTheme();
try {
$themeData = Db::name('mete_template_theme_data')
->where('theme_key', $themeKey)
->where('delete_time', null)
->select()
->toArray();
$data = [];
foreach ($themeData as $item) {
$data[$item['field_key']] = $item['field_value'];
}
return [
'theme_key' => $themeKey,
'theme_path' => '/themes/' . $themeKey . '/index.html',
'data' => $data
];
} catch (\Exception $e) {
return [
'theme_key' => $themeKey,
'theme_path' => '/themes/' . $themeKey . '/index.html',
'data' => []
];
}
}
/**
* 保存模板字段数据
* @param string $themeKey
* @param string $fieldKey
* @param mixed $fieldValue
* @return bool
*/
public function saveThemeField(string $themeKey, string $fieldKey, $fieldValue): bool
{
try {
$existing = Db::name('mete_template_theme_data')
->where('theme_key', $themeKey)
->where('field_key', $fieldKey)
->where('delete_time', null)
->find();
$value = is_array($fieldValue) ? json_encode($fieldValue, JSON_UNESCAPED_UNICODE) : $fieldValue;
$now = date('Y-m-d H:i:s');
if ($existing) {
Db::name('mete_template_theme_data')
->where('id', $existing['id'])
->update([
'field_value' => $value,
'update_time' => $now
]);
} else {
Db::name('mete_template_theme_data')->insert([
'theme_key' => $themeKey,
'field_key' => $fieldKey,
'field_value' => $value,
'create_time' => $now,
'update_time' => $now
]);
}
return true;
} catch (\Exception $e) {
return false;
}
}
}

View File

@ -0,0 +1,13 @@
{
"name": "默认模板",
"description": "标准企业官网模板,适用于各类企业展示",
"version": "1.0.0",
"author": "System",
"fields": {
"site_name": "网站名称",
"banner": "轮播图列表",
"news": "新闻列表",
"solutions": "解决方案",
"partners": "合作伙伴"
}
}

View File

@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title data-field="site_name">企业官网</title>
<link rel="stylesheet" href="./styles/main.css">
</head>
<body>
<!-- 头部 -->
<header class="header">
<div class="container">
<h1 data-field="site_name">企业官网</h1>
<nav class="nav">
<a href="/">首页</a>
<a href="/news">新闻资讯</a>
<a href="/solutions">解决方案</a>
<a href="/contact">联系我们</a>
</nav>
</div>
</header>
<!-- 轮播图 -->
<section class="banner">
<div class="banner-slides">
<div class="slide active" data-field="banner">
<img src="./images/banner1.jpg" alt="Banner 1">
<div class="banner-content">
<h2>欢迎来到我们的网站</h2>
<p>专业的企业数字化解决方案提供商</p>
</div>
</div>
</div>
</section>
<!-- 新闻资讯 -->
<section class="news section">
<div class="container">
<h2 class="section-title">新闻资讯</h2>
<div class="news-list" data-field="news">
<div class="news-item">
<h3>公司动态</h3>
<p>这里是新闻内容...</p>
</div>
</div>
</div>
</section>
<!-- 解决方案 -->
<section class="solutions section">
<div class="container">
<h2 class="section-title">解决方案</h2>
<div class="solutions-grid" data-field="solutions">
<div class="solution-card">
<h3>解决方案一</h3>
<p>详细描述...</p>
</div>
</div>
</div>
</section>
<!-- 合作伙伴 -->
<section class="partners section">
<div class="container">
<h2 class="section-title">合作伙伴</h2>
<div class="partners-grid" data-field="partners">
<div class="partner-logo">Partner Logo</div>
</div>
</div>
</section>
<!-- 底部 -->
<footer class="footer">
<div class="container">
<p>&copy; 2024 <span data-field="site_name">企业官网</span>. All rights reserved.</p>
</div>
</footer>
<!-- 数据注入脚本 -->
<script src="./js/theme-loader.js"></script>
</body>
</html>

View File

@ -0,0 +1,213 @@
/**
* 模板数据注入脚本
* 功能监听 postMessage 事件当收到 SET_SITE_DATA
* 自动寻找带有 data-field 属性的 HTML 标签替换为对应的数据
*/
(function() {
'use strict';
// 日志开关
const DEBUG = false;
function log(...args) {
if (DEBUG) {
console.log('[ThemeLoader]', ...args);
}
}
/**
* 替换元素内容或属性
* @param {HTMLElement} element - 目标元素
* @param {string} field - 字段名
* @param {any} value - 数据值
*/
function applyDataToElement(element, field, value) {
if (value === undefined || value === null) {
log(`字段 ${field} 值为空,跳过`);
return;
}
// 如果是数组或对象,尝试解析
if (typeof value === 'string') {
try {
const parsed = JSON.parse(value);
if (parsed) {
value = parsed;
}
} catch (e) {
// 保持原值
}
}
// 处理数组类型(用于轮播图、列表等)
if (Array.isArray(value)) {
handleArrayField(element, field, value);
return;
}
// 处理对象类型
if (typeof value === 'object') {
handleObjectField(element, field, value);
return;
}
// 处理基础类型(字符串、数字)
// 1. 如果是 input/textarea/select设置为 value
const tagName = element.tagName.toLowerCase();
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
element.value = value;
return;
}
// 2. 如果有 src 属性,设置为 src图片等
if (element.hasAttribute('src') && !element.hasAttribute('data-keep-src')) {
// 检查是否是占位符图片
const currentSrc = element.getAttribute('src');
if (!currentSrc || currentSrc.indexOf('placeholder') > -1) {
element.src = value;
}
return;
}
// 3. 否则设置为 innerText
element.innerText = value;
}
/**
* 处理数组类型的字段如轮播图列表
*/
function handleArrayField(container, field, dataList) {
// 查找模板元素(带有 data-template 属性的元素)
const templateElement = container.querySelector('[data-template]');
if (!templateElement) {
log(`字段 ${field} 未找到模板元素`);
return;
}
const template = templateElement.cloneNode(true);
template.removeAttribute('data-template');
template.style.display = '';
// 清空容器
container.innerHTML = '';
// 渲染每个数据项
dataList.forEach((item, index) => {
const itemElement = template.cloneNode(true);
applyDataToObject(itemElement, item, index);
container.appendChild(itemElement);
});
}
/**
* 处理对象类型的字段
*/
function handleObjectField(element, field, data) {
// 递归处理对象属性
applyDataToObject(element, data);
}
/**
* 将数据应用到元素及其子元素
*/
function applyDataToObject(element, data, index = 0) {
// 处理 data-field 属性
const fieldElements = element.querySelectorAll('[data-field]');
fieldElements.forEach(el => {
const field = el.getAttribute('data-field');
// 支持点号分隔的路径,如 "banner.0.image"
const value = getNestedValue(data, field);
if (value !== undefined) {
applyDataToElement(el, field, value);
}
});
// 也处理元素本身的 data-field
if (element.hasAttribute('data-field')) {
const field = element.getAttribute('data-field');
const value = getNestedValue(data, field);
if (value !== undefined) {
applyDataToElement(element, field, value);
}
}
}
/**
* 获取嵌套属性值
* @param {object} obj - 数据对象
* @param {string} path - 属性路径 "banner.0.image"
* @returns {any}
*/
function getNestedValue(obj, path) {
if (!path) return obj;
const keys = path.split('.');
let value = obj;
for (const key of keys) {
if (value === null || value === undefined) {
return undefined;
}
value = value[key];
}
return value;
}
/**
* 初始化数据注入
*/
function init() {
log('ThemeLoader 初始化');
// 监听来自父窗口的消息
window.addEventListener('message', function(event) {
log('收到消息:', event.data);
// 验证消息类型
if (!event.data || event.data.type !== 'SET_SITE_DATA') {
return;
}
const siteData = event.data.data;
if (!siteData) {
log('未收到有效数据');
return;
}
log('开始注入数据:', siteData);
// 查找所有带有 data-field 属性的元素
const elements = document.querySelectorAll('[data-field]');
elements.forEach(element => {
const field = element.getAttribute('data-field');
const value = siteData[field];
if (value !== undefined) {
applyDataToElement(element, field, value);
}
});
// 触发自定义事件,通知数据已加载
window.dispatchEvent(new CustomEvent('themeDataLoaded', {
detail: siteData
}));
log('数据注入完成');
});
// 发送就绪消息给父窗口
window.parent.postMessage({
type: 'THEME_READY'
}, '*');
}
// DOM 加载完成后初始化
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -0,0 +1,153 @@
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.section {
padding: 60px 0;
}
.section-title {
text-align: center;
font-size: 32px;
margin-bottom: 40px;
color: #333;
}
/* 头部 */
.header {
background: #fff;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
.header .container {
display: flex;
justify-content: space-between;
align-items: center;
height: 70px;
}
.header h1 {
font-size: 24px;
color: #1890ff;
}
.nav a {
margin-left: 30px;
text-decoration: none;
color: #333;
transition: color 0.3s;
}
.nav a:hover {
color: #1890ff;
}
/* 轮播图 */
.banner {
margin-top: 70px;
height: 500px;
overflow: hidden;
position: relative;
}
.banner-slides .slide {
display: none;
}
.banner-slides .slide.active {
display: block;
}
.banner-slides .slide img {
width: 100%;
height: 500px;
object-fit: cover;
}
.banner-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
color: #fff;
}
.banner-content h2 {
font-size: 48px;
margin-bottom: 20px;
}
.banner-content p {
font-size: 20px;
}
/* 新闻 */
.news-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 30px;
}
.news-item {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
}
/* 解决方案 */
.solutions-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
}
.solution-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 40px 20px;
border-radius: 8px;
text-align: center;
color: #fff;
}
/* 合作伙伴 */
.partners-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20px;
}
.partner-logo {
background: #f5f5f5;
padding: 30px;
text-align: center;
border-radius: 8px;
}
/* 底部 */
.footer {
background: #333;
color: #fff;
padding: 30px 0;
text-align: center;
}