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

项目结构

我们创建一个清晰的项目目录结构:

LM在线网页制作PHP源码
(图片来源网络,侵删)
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

处理页面相关的数据逻辑。

LM在线网页制作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>

如何运行这个项目

  1. 环境准备:确保你有一个支持PHP和MySQL的环境,比如XAMPP、WAMP或MAMP。
  2. 创建数据库:在你的MySQL中创建一个数据库,并将install.sql文件导入。
  3. 修改配置:打开config/database.php,填入你的数据库用户名和密码。
  4. 放置文件:将整个lm-page-builder文件夹放到你的Web服务器根目录下(htdocswww)。
  5. 访问:通过浏览器访问 http://localhost/lm-page-builder/index.php

你将看到一个功能完整的在线网页编辑器,你可以从左侧拖拽组件到中间的画布,点击画布上的组件来编辑其属性,然后点击“保存页面”或“预览页面”来查看效果。

下一步可以扩展的功能

  • 用户认证系统:登录、注册,让每个用户都有自己的网站空间。
  • 多页面管理:在编辑器中增加一个页面列表,可以切换、添加、删除页面。
  • 组件高级化:增加图片上传、视频、表单、布局容器(行、列)等更复杂的组件。
  • 主题和样式:增加CSS编辑器,让用户可以自定义网站的整体样式。
  • 响应式设计:在编辑器中增加移动端预览模式。
  • 部署功能:将制作好的网站一键导出为静态HTML文件或部署到服务器。