diff --git a/app/admin/controller/Cms/Demand/DemandController.php b/app/admin/controller/Cms/Demand/DemandController.php new file mode 100644 index 0000000..a07bf19 --- /dev/null +++ b/app/admin/controller/Cms/Demand/DemandController.php @@ -0,0 +1,180 @@ +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(), + ]); + } + } +} diff --git a/app/admin/controller/ThemeController.php b/app/admin/controller/ThemeController.php new file mode 100644 index 0000000..2328961 --- /dev/null +++ b/app/admin/controller/ThemeController.php @@ -0,0 +1,120 @@ +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' => '保存失败' + ]); + } +} diff --git a/app/admin/route/routes/demand.php b/app/admin/route/routes/demand.php new file mode 100644 index 0000000..b4bc1b1 --- /dev/null +++ b/app/admin/route/routes/demand.php @@ -0,0 +1,8 @@ +themeService = new ThemeService(); + } + public function 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' => '企业官网' + ] + ] + ]); + } + /** * 获取日志列表 */ diff --git a/app/index/route/app.php b/app/index/route/app.php index d9e3f03..043f905 100644 --- a/app/index/route/app.php +++ b/app/index/route/app.php @@ -5,6 +5,9 @@ use think\facade\Route; Route::get('/', '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'); diff --git a/app/model/Cms/Demand.php b/app/model/Cms/Demand.php new file mode 100644 index 0000000..7a90997 --- /dev/null +++ b/app/model/Cms/Demand.php @@ -0,0 +1,44 @@ + +// +---------------------------------------------------------------------- + +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', + ]; + + +} + diff --git a/app/model/Cms/DemandCategory.php b/app/model/Cms/DemandCategory.php new file mode 100644 index 0000000..964c688 --- /dev/null +++ b/app/model/Cms/DemandCategory.php @@ -0,0 +1,43 @@ + +// +---------------------------------------------------------------------- + +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', + ]; + + +} + diff --git a/app/service/ThemeService.php b/app/service/ThemeService.php new file mode 100644 index 0000000..2776b19 --- /dev/null +++ b/app/service/ThemeService.php @@ -0,0 +1,258 @@ +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; + } + } +} diff --git a/public/themes/default/config.json b/public/themes/default/config.json new file mode 100644 index 0000000..b99497b --- /dev/null +++ b/public/themes/default/config.json @@ -0,0 +1,13 @@ +{ + "name": "默认模板", + "description": "标准企业官网模板,适用于各类企业展示", + "version": "1.0.0", + "author": "System", + "fields": { + "site_name": "网站名称", + "banner": "轮播图列表", + "news": "新闻列表", + "solutions": "解决方案", + "partners": "合作伙伴" + } +} diff --git a/public/themes/default/index.html b/public/themes/default/index.html new file mode 100644 index 0000000..00239f1 --- /dev/null +++ b/public/themes/default/index.html @@ -0,0 +1,82 @@ + + + + + + 企业官网 + + + + +
+
+

企业官网

+ +
+
+ + + + + +
+
+

新闻资讯

+
+
+

公司动态

+

这里是新闻内容...

+
+
+
+
+ + +
+
+

解决方案

+
+
+

解决方案一

+

详细描述...

+
+
+
+
+ + +
+
+

合作伙伴

+
+ +
+
+
+ + + + + + + + diff --git a/public/themes/default/js/theme-loader.js b/public/themes/default/js/theme-loader.js new file mode 100644 index 0000000..4a845e3 --- /dev/null +++ b/public/themes/default/js/theme-loader.js @@ -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(); + } +})(); diff --git a/public/themes/default/styles/main.css b/public/themes/default/styles/main.css new file mode 100644 index 0000000..0df8c03 --- /dev/null +++ b/public/themes/default/styles/main.css @@ -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; +}