- 前端界面:一个所见即所得的编辑器,左侧是组件面板,中间是画布,右侧是属性面板。
- 后端逻辑:
- 使用PHP和MySQL来存储网站数据。
- 提供API接口来保存和加载页面数据。
- 一个预览功能,可以实时看到网站效果。
- 核心组件:
- 页面管理:创建、编辑、删除页面。
- 组件库、段落、图片、按钮等基础组件。
- 拖拽编辑:可以将组件从左侧拖到画布上,并在画布上调整位置。
- 属性编辑:选中画布上的组件后,可以在右侧修改其文本、颜色、链接等属性。
项目结构
我们创建一个清晰的项目目录结构:

(图片来源网络,侵删)
lm-page-builder/
├── assets/ # 静态资源 (CSS, JS, 图片)
│ ├── css/
│ │ └── style.css
│ └── js/
│ └── builder.js
├── config/
│ └── database.php # 数据库配置
├── core/
│ ├── Database.php # 数据库连接类
│ └── Page.php # 页面数据模型
├── index.php # 主入口文件,显示编辑器界面
├── save_page.php # 处理保存页面数据的API
├── load_page.php # 处理加载页面数据的API
├── preview.php # 预览页面
└── install.sql # 数据库初始化SQL文件
第1步:数据库准备 (install.sql)
创建一个数据库,然后执行以下SQL文件来创建必要的表。
-- install.sql CREATE DATABASE IF NOT EXISTS `lm_page_builder` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE `lm_page_builder`; -- 存储网站/项目信息 CREATE TABLE `websites` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 存储页面信息 CREATE TABLE `pages` ( `id` int(11) NOT NULL AUTO_INCREMENT, `website_id` int(11) NOT NULL, `name` varchar(255) NOT NULL, `slug` varchar(255) NOT NULL, `created_at` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`id`), UNIQUE KEY `slug` (`slug`), KEY `website_id` (`website_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 存储页面的组件数据(以JSON格式) CREATE TABLE `page_components` ( `id` int(11) NOT NULL AUTO_INCREMENT, `page_id` int(11) NOT NULL, `component_type` varchar(50) NOT NULL, -- e.g., 'heading', 'paragraph', 'image' `content` text NOT NULL, -- 存储组件的所有属性,如JSON `order_index` int(11) NOT NULL DEFAULT 0, -- 用于排序 PRIMARY KEY (`id`), KEY `page_id` (`page_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
第2步:后端配置与核心类
config/database.php
数据库连接配置。
<?php
// config/database.php
define('DB_HOST', 'localhost');
define('DB_USER', 'root'); // 你的数据库用户名
define('DB_PASS', ''); // 你的数据库密码
define('DB_NAME', 'lm_page_builder'); // 你的数据库名
core/Database.php
一个简单的单例数据库连接类。
<?php
// core/Database.php
class Database {
private static $instance = null;
private $conn;
private function __construct() {
try {
$this->conn = new PDO(
"mysql:host=" . DB_HOST . ";dbname=" . DB_NAME . ";charset=utf8mb4",
DB_USER,
DB_PASS
);
$this->conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch (PDOException $e) {
die("Database connection failed: " . $e->getMessage());
}
}
public static function getInstance() {
if (self::$instance == null) {
self::$instance = new Database();
}
return self::$instance;
}
public function getConnection() {
return $this->conn;
}
}
core/Page.php
处理页面相关的数据逻辑。

(图片来源网络,侵删)
<?php
// core/Page.php
require_once 'Database.php';
class Page {
private $db;
public function __construct() {
$this->db = Database::getInstance()->getConnection();
}
// 创建一个新网站(项目)
public function createWebsite($name) {
$stmt = $this->db->prepare("INSERT INTO websites (name) VALUES (:name)");
$stmt->bindParam(':name', $name);
$stmt->execute();
return $this->db->lastInsertId();
}
// 创建一个新页面
public function createPage($website_id, $name, $slug) {
$stmt = $this->db->prepare("INSERT INTO pages (website_id, name, slug) VALUES (:website_id, :name, :slug)");
$stmt->bindParam(':website_id', $website_id);
$stmt->bindParam(':name', $name);
$stmt->bindParam(':slug', $slug);
$stmt->execute();
return $this->db->lastInsertId();
}
// 保存页面组件
public function savePageComponents($page_id, $components) {
// 先删除旧组件
$this->db->prepare("DELETE FROM page_components WHERE page_id = :page_id")->execute(['page_id' => $page_id]);
// 插入新组件
$stmt = $this->db->prepare("INSERT INTO page_components (page_id, component_type, content, order_index) VALUES (:page_id, :type, :content, :order)");
foreach ($components as $index => $component) {
$stmt->execute([
'page_id' => $page_id,
'type' => $component['type'],
'content' => json_encode($component),
'order' => $index
]);
}
}
// 加载页面组件
public function loadPageComponents($page_id) {
$stmt = $this->db->prepare("SELECT content FROM page_components WHERE page_id = :page_id ORDER BY order_index ASC");
$stmt->execute(['page_id' => $page_id]);
$results = $stmt->fetchAll(PDO::FETCH_COLUMN);
$components = [];
foreach ($results as $json) {
$components[] = json_decode($json, true);
}
return $components;
}
}
第3步:API接口
save_page.php
处理来自前端的保存请求。
<?php
// save_page.php
header('Content-Type: application/json');
require_once 'core/Page.php';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$page_id = $_POST['page_id'] ?? null;
$components = json_decode($_POST['components'] ?? '[]', true);
if ($page_id && is_array($components)) {
$page = new Page();
$page->savePageComponents($page_id, $components);
echo json_encode(['success' => true, 'message' => 'Page saved successfully.']);
} else {
echo json_encode(['success' => false, 'message' => 'Invalid data.']);
}
} else {
echo json_encode(['success' => false, 'message' => 'Invalid request method.']);
}
load_page.php
处理从前端加载页面的请求。
<?php
// load_page.php
header('Content-Type: application/json');
require_once 'core/Page.php';
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$page_id = $_GET['page_id'] ?? null;
if ($page_id) {
$page = new Page();
$components = $page->loadPageComponents($page_id);
echo json_encode(['success' => true, 'components' => $components]);
} else {
echo json_encode(['success' => false, 'message' => 'Page ID is required.']);
}
} else {
echo json_encode(['success' => false, 'message' => 'Invalid request method.']);
}
第4步:前端界面与逻辑
assets/css/style.css
编辑器的基本样式。
/* assets/css/style.css */
body, html {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
height: 100%;
overflow: hidden;
}
.builder-container {
display: flex;
height: 100vh;
}
/* 左侧组件面板 */
.component-panel {
width: 250px;
background-color: #2c3e50;
color: white;
padding: 20px;
overflow-y: auto;
}
.component-panel h3 {
margin-top: 0;
border-bottom: 1px solid #34495e;
padding-bottom: 10px;
}
.component-item {
background-color: #34495e;
padding: 10px;
margin-bottom: 10px;
border-radius: 5px;
cursor: move;
text-align: center;
}
.component-item:hover {
background-color: #4a5f7a;
}
/* 中间画布区域 */
.canvas-area {
flex: 1;
background-color: #ecf0f1;
padding: 20px;
overflow-y: auto;
position: relative;
}
.canvas {
background-color: white;
box-shadow: 0 0 10px rgba(0,0,0,0.1);
min-height: 500px;
padding: 20px;
position: relative;
}
.canvas .component {
border: 1px dashed transparent;
padding: 5px;
margin-bottom: 10px;
cursor: move;
position: relative;
}
.canvas .component:hover {
border-color: #bdc3c7;
}
.canvas .component.selected {
border-color: #3498db;
background-color: #f8f9fa;
}
/* 右侧属性面板 */
.properties-panel {
width: 250px;
background-color: #34495e;
color: white;
padding: 20px;
overflow-y: auto;
}
.properties-panel h3 {
margin-top: 0;
border-bottom: 1px solid #2c3e50;
padding-bottom: 10px;
}
.properties-panel input, .properties-panel textarea {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: none;
border-radius: 4px;
}
.properties-panel input[type="color"] {
height: 40px;
cursor: pointer;
}
.toolbar {
background-color: #2c3e50;
color: white;
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.toolbar button {
background-color: #3498db;
color: white;
border: none;
padding: 8px 15px;
border-radius: 4px;
cursor: pointer;
}
.toolbar button:hover {
background-color: #2980b9;
}
assets/js/builder.js
核心的拖拽和编辑逻辑。
// assets/js/builder.js
document.addEventListener('DOMContentLoaded', function() {
const componentItems = document.querySelectorAll('.component-item');
const canvas = document.querySelector('.canvas');
const propertiesPanel = document.querySelector('.properties-panel');
let selectedComponent = null;
let draggedElement = null;
// --- 组件拖拽逻辑 ---
componentItems.forEach(item => {
item.addEventListener('dragstart', handleDragStart);
});
canvas.addEventListener('dragover', handleDragOver);
canvas.addEventListener('drop', handleDrop);
function handleDragStart(e) {
draggedElement = e.target;
e.dataTransfer.effectAllowed = 'copy';
e.dataTransfer.setData('componentType', e.target.dataset.type);
}
function handleDragOver(e) {
if (e.preventDefault) {
e.preventDefault();
}
e.dataTransfer.dropEffect = 'copy';
return false;
}
function handleDrop(e) {
if (e.stopPropagation) {
e.stopPropagation();
}
const componentType = e.dataTransfer.getData('componentType');
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// 创建新的组件元素
const newComponent = createComponentElement(componentType);
canvas.appendChild(newComponent);
// 添加到选中状态
selectComponent(newComponent);
return false;
}
// --- 画布内组件选择和属性编辑 ---
canvas.addEventListener('click', function(e) {
if (e.target.classList.contains('component')) {
selectComponent(e.target);
} else if (e.target.classList.contains('delete-btn')) {
e.target.parentElement.remove();
propertiesPanel.innerHTML = '<p>请选择一个组件进行编辑。</p>';
} else {
deselectAll();
}
});
function selectComponent(component) {
deselectAll();
component.classList.add('selected');
selectedComponent = component;
showProperties(component);
}
function deselectAll() {
document.querySelectorAll('.component.selected').forEach(c => c.classList.remove('selected'));
selectedComponent = null;
propertiesPanel.innerHTML = '<p>请选择一个组件进行编辑。</p>';
}
function showProperties(component) {
const type = component.dataset.type;
const content = JSON.parse(component.dataset.content || '{}');
let html = `<h3>编辑 ${type}</h3>`;
switch(type) {
case 'heading':
html += `
<label>文本内容</label>
<input type="text" id="prop-text" value="${content.text || ''}">
<label>层级</label>
<select id="prop-level">
<option value="1" ${content.level == 1 ? 'selected' : ''}>H1</option>
<option value="2" ${content.level == 2 ? 'selected' : ''}>H2</option>
<option value="3" ${content.level == 3 ? 'selected' : ''}>H3</option>
</select>
`;
break;
case 'paragraph':
html += `
<label>文本内容</label>
<textarea id="prop-text">${content.text || ''}</textarea>
`;
break;
case 'button':
html += `
<label>按钮文本</label>
<input type="text" id="prop-text" value="${content.text || ''}">
<label>链接</label>
<input type="text" id="prop-link" value="${content.link || ''}">
<label>背景色</label>
<input type="color" id="prop-bgcolor" value="${content.bgcolor || '#3498db'}">
`;
break;
}
html += `<button class="delete-btn" style="background-color: #e74c3c; margin-top: 10px;">删除组件</button>`;
propertiesPanel.innerHTML = html;
// 绑定属性输入事件
propertiesPanel.querySelectorAll('input, textarea, select').forEach(input => {
input.addEventListener('input', function() {
updateComponentFromProperties();
});
});
}
function updateComponentFromProperties() {
if (!selectedComponent) return;
const type = selectedComponent.dataset.type;
const content = JSON.parse(selectedComponent.dataset.content || '{}');
switch(type) {
case 'heading':
content.text = document.getElementById('prop-text').value;
content.level = document.getElementById('prop-level').value;
selectedComponent.innerHTML = `<h${content.level}>${content.text}</h${content.level}>`;
break;
case 'paragraph':
content.text = document.getElementById('prop-text').value;
selectedComponent.innerHTML = `<p>${content.text}</p>`;
break;
case 'button':
content.text = document.getElementById('prop-text').value;
content.link = document.getElementById('prop-link').value;
content.bgcolor = document.getElementById('prop-bgcolor').value;
selectedComponent.innerHTML = `<a href="${content.link}" style="background-color: ${content.bgcolor}; color: white; padding: 10px 15px; text-decoration: none; border-radius: 5px; display: inline-block;">${content.text}</a>`;
break;
}
selectedComponent.dataset.content = JSON.stringify(content);
}
function createComponentElement(type) {
const id = 'comp-' + Date.now();
let content = {};
let innerHTML = '';
switch(type) {
case 'heading':
content = { text: '新标题', level: 2 };
innerHTML = `<h${content.level}>${content.text}</h${content.level}>`;
break;
case 'paragraph':
content = { text: '这是一个段落。' };
innerHTML = `<p>${content.text}</p>`;
break;
case 'button':
content = { text: '按钮', link: '#', bgcolor: '#3498db' };
innerHTML = `<a href="${content.link}" style="background-color: ${content.bgcolor}; color: white; padding: 10px 15px; text-decoration: none; border-radius: 5px; display: inline-block;">${content.text}</a>`;
break;
}
const div = document.createElement('div');
div.className = 'component';
div.id = id;
div.dataset.type = type;
div.dataset.content = JSON.stringify(content);
div.draggable = true;
div.innerHTML = innerHTML;
// 为画布内的组件添加拖拽事件(用于移动位置)
div.addEventListener('dragstart', function(e) {
draggedElement = e.target;
e.dataTransfer.effectAllowed = 'move';
});
return div;
}
// --- 工具栏按钮逻辑 ---
document.getElementById('save-btn').addEventListener('click', savePage);
document.getElementById('preview-btn').addEventListener('click', previewPage);
// 假设我们有一个页面ID,实际应用中可能从URL或会话中获取
const currentPageId = 1;
function savePage() {
const components = [];
document.querySelectorAll('.canvas .component').forEach(comp => {
components.push({
type: comp.dataset.type,
content: JSON.parse(comp.dataset.content)
});
});
const formData = new FormData();
formData.append('page_id', currentPageId);
formData.append('components', JSON.stringify(components));
fetch('save_page.php', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('页面保存成功!');
} else {
alert('保存失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('保存请求失败。');
});
}
function previewPage() {
// 收集所有组件数据
const components = [];
document.querySelectorAll('.canvas .component').forEach(comp => {
components.push({
type: comp.dataset.type,
content: JSON.parse(comp.dataset.content)
});
});
// 存储到localStorage,供preview.php页面读取
localStorage.setItem('preview_components', JSON.stringify(components));
// 打开预览窗口
window.open('preview.php', '_blank');
}
});
第5步:主页面和预览页面
index.php
编辑器的主界面。
<?php
// index.php
// 在实际应用中,这里应该有登录和会话管理
// 为了演示,我们假设用户已登录,并有一个默认的网站和页面
require_once 'config/database.php';
require_once 'core/Database.php';
require_once 'core/Page.php';
$page = new Page();
$website_id = 1; // 假设的网站ID
$page_id = 1; // 假设的页面ID
// 如果网站或页面不存在,则创建一个
$stmt = Database::getInstance()->getConnection()->prepare("SELECT id FROM websites WHERE id = ?");
$stmt->execute([$website_id]);
if (!$stmt->fetch()) {
$website_id = $page->createWebsite('我的第一个网站');
$page_id = $page->createPage($website_id, '首页', 'home');
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">LM 在线网页制作器</title>
<link rel="stylesheet" href="assets/css/style.css">
</head>
<body>
<div class="toolbar">
<h1>LM 页面制作器</h1>
<div>
<button id="save-btn">保存页面</button>
<button id="preview-btn">预览页面</button>
</div>
</div>
<div class="builder-container">
<!-- 左侧组件面板 -->
<div class="component-panel">
<h3>组件库</h3>
<div class="component-item" draggable="true" data-type="heading">
<h2>标题</h2>
</div>
<div class="component-item" draggable="true" data-type="paragraph">
<p>段落</p>
</div>
<div class="component-item" draggable="true" data-type="button">
<button>按钮</button>
</div>
</div>
<!-- 中间画布区域 -->
<div class="canvas-area">
<div class="canvas" id="canvas">
<!-- 页面内容将在这里动态加载 -->
<?php
// 加载已保存的组件
$components = $page->loadPageComponents($page_id);
foreach ($components as $comp) {
$type = htmlspecialchars($comp['type']);
$content = htmlspecialchars(json_encode($comp, JSON_UNESCAPED_UNICODE));
$innerHtml = '';
switch($type) {
case 'heading':
$level = $comp['content']['level'] ?? 2;
$text = htmlspecialchars($comp['content']['text'] ?? '标题');
$innerHtml = "<h{$level}>{$text}</h{$level}>";
break;
case 'paragraph':
$text = htmlspecialchars($comp['content']['text'] ?? '段落');
$innerHtml = "<p>{$text}</p>";
break;
case 'button':
$text = htmlspecialchars($comp['content']['text'] ?? '按钮');
$link = htmlspecialchars($comp['content']['link'] ?? '#');
$bgColor = htmlspecialchars($comp['content']['bgcolor'] ?? '#3498db');
$innerHtml = "<a href='{$link}' style='background-color: {$bgColor}; color: white; padding: 10px 15px; text-decoration: none; border-radius: 5px; display: inline-block;'>{$text}</a>";
break;
}
echo "<div class='component' data-type='{$type}' data-content='{$content}'>{$innerHtml}</div>";
}
?>
</div>
</div>
<!-- 右侧属性面板 -->
<div class="properties-panel">
<p>请选择一个组件进行编辑。</p>
</div>
</div>
<script src="assets/js/builder.js"></script>
</body>
</html>
preview.php
纯展示页面,不包含编辑功能。
<?php
// preview.php
// 从localStorage获取组件数据(在实际部署中,应该从数据库加载)
$componentsJson = $_GET['data'] ?? null; // 为了演示,也可以通过URL参数传递
if (!$componentsJson) {
// 如果没有数据,尝试从localStorage(仅限前端JS能访问,PHP不行)
// 这里我们假设数据是通过JS在打开新窗口时塞进URL的
// 或者,在真实场景中,这里应该根据页面ID从数据库加载
echo "没有预览数据。";
exit;
}
$components = json_decode($componentsJson, true);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">页面预览</title>
<style>
body { font-family: sans-serif; margin: 40px; }
h1, h2, h3 { color: #333; }
p { line-height: 1.6; }
a { display: inline-block; padding: 10px 15px; background-color: #3498db; color: white; text-decoration: none; border-radius: 5px; }
</style>
</head>
<body>
<h1>页面预览</h1>
<div id="preview-content">
<?php
foreach ($components as $comp) {
$type = $comp['type'];
$content = $comp['content'];
switch($type) {
case 'heading':
$level = $content['level'] ?? 2;
$text = $content['text'] ?? '标题';
echo "<h{$level}>{$text}</h{$level}>";
break;
case 'paragraph':
$text = $content['text'] ?? '段落';
echo "<p>{$text}</p>";
break;
case 'button':
$text = $content['text'] ?? '按钮';
$link = $content['link'] ?? '#';
$bgColor = $content['bgcolor'] ?? '#3498db';
echo "<a href='{$link}' style='background-color: {$bgColor};'>{$text}</a>";
break;
}
}
?>
</div>
</body>
</html>
如何运行这个项目
- 环境准备:确保你有一个支持PHP和MySQL的环境,比如XAMPP、WAMP或MAMP。
- 创建数据库:在你的MySQL中创建一个数据库,并将
install.sql文件导入。 - 修改配置:打开
config/database.php,填入你的数据库用户名和密码。 - 放置文件:将整个
lm-page-builder文件夹放到你的Web服务器根目录下(htdocs或www)。 - 访问:通过浏览器访问
http://localhost/lm-page-builder/index.php。
你将看到一个功能完整的在线网页编辑器,你可以从左侧拖拽组件到中间的画布,点击画布上的组件来编辑其属性,然后点击“保存页面”或“预览页面”来查看效果。
下一步可以扩展的功能
- 用户认证系统:登录、注册,让每个用户都有自己的网站空间。
- 多页面管理:在编辑器中增加一个页面列表,可以切换、添加、删除页面。
- 组件高级化:增加图片上传、视频、表单、布局容器(行、列)等更复杂的组件。
- 主题和样式:增加CSS编辑器,让用户可以自定义网站的整体样式。
- 响应式设计:在编辑器中增加移动端预览模式。
- 部署功能:将制作好的网站一键导出为静态HTML文件或部署到服务器。
