Browse Source

可视化编辑器

moshaorui 3 months ago
parent
commit
4598e7632f

+ 53 - 0
app/Distributor/Controllers/VisualEditorController.php

@@ -0,0 +1,53 @@
+<?php
+
+namespace App\Distributor\Controllers;
+
+use App\Distributor\Metrics\DistMessage;
+use App\Distributor\Repositories\SiteMenu;
+use App\Http\Controllers\Controller;
+use Dcat\Admin\Layout\Column;
+use Dcat\Admin\Layout\Content;
+use Dcat\Admin\Layout\Row;
+
+
+class VisualEditorController extends Controller
+{
+
+    public function index()
+    {
+        $mid = isset($_GET['mid'])? intval($_GET['mid']) : 0;
+        $uri = isset($_GET['uri'])? $_GET['uri'] : '';
+        //网站html内容
+        $url = getSiteDomain();
+        $html = $this->getURlHTML($url,$uri);
+        //菜单
+        $siteMenu = SiteMenu::showAllSelectOptions();
+        //
+        return view('distributor.pages-custom.visual-editor',['html'=>$html,'siteMenu'=>json_encode($siteMenu),'mid'=>$mid]);
+    }
+
+    private function getURlHTML($url,$uri)
+    {
+        //echo $url;exit;
+        $siteUrl = $url . $uri;
+        $ch = curl_init($siteUrl);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 返回原生的(Raw)输出
+        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查(在生产环境中,应将其设置为true并指定CA证书路径)
+        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); // 跳过证书主机名验证
+
+        $html = curl_exec($ch);
+        if (curl_errno($ch)) {
+            echo 'cURL error: ' . curl_error($ch);
+        }
+        curl_close($ch);
+        $pattern = '/<(link|script|img)[^>]*\s+(href|src)=["\'](\/[^"\']+)["\'][^>]*>/i';
+        $replacement = function($matches) use ($url) {
+            // $matches[3] 是匹配到的以 / 开头的路径
+            return str_replace($matches[3], $url . $matches[3], $matches[0]);
+        };
+        $html = preg_replace_callback($pattern, $replacement, $html);
+        return $html;
+    }
+
+
+}

+ 39 - 0
app/Distributor/Repositories/SiteMenu.php

@@ -38,6 +38,45 @@ class SiteMenu extends EloquentRepository
         return $selectOptions;
     }
 
+    public static function showAllSelectOptions($siteDomain = '') {
+        $model = new Model();
+        $allMenu = $model->where('dist_id', getDistributorId())->orderBy('order', 'asc')->get();
+        $arrayMenu = [];
+        foreach ($allMenu as $menu) {
+            $arrayMenu[$menu->id] = $menu;
+        }
+
+        $topSelectOptions =  Model::class::selectOptions(function ($query) {
+            $query =  $query->where('dist_id', getDistributorId())->orderBy('order', 'asc');
+            return $query;
+        });
+
+        $bottomSelectOptions =  Model::class::selectOptions(function ($query) {
+            $query =  $query->where('menu_location',1)->where('dist_id', getDistributorId())->orderBy('order', 'asc');
+            return $query;
+        });
+
+        $selectOptions = [];
+        foreach ($topSelectOptions as $key => $value) {
+            if ($key == 0) {
+                $selectOptions[] = ['id'=>-1,'title'=>'顶部菜单','url'=>''];
+            } else {
+                $url = $siteDomain == '' ? $arrayMenu[$key]['uri'] : $siteDomain.$arrayMenu[$key]['uri'];
+                $selectOptions[] = ['id'=>$key,'title'=>$value,'url'=>$url];
+            }
+        }
+
+        foreach ($bottomSelectOptions as $key => $value) {
+            if ($key == 0) {
+                $selectOptions[] = ['id'=>-2,'title'=>'底部菜单','url'=>''];
+            } else {
+                $url = $siteDomain == '' ? $arrayMenu[$key]['uri'] : $siteDomain.$arrayMenu[$key]['uri'];
+                $selectOptions[] = ['id'=>$key,'title'=>$value,'url'=>$url];
+            }
+        }
+        return $selectOptions;
+    }
+
     /*
     * 获取一个标签
     */

+ 2 - 1
app/Distributor/routes.php

@@ -68,7 +68,8 @@ Route::group([
     $router->get('captcha','CaptchaController@generate');
 
     $router->resource('messages', 'DistMessageController');
-    //配置
+    //可视化编辑器
+    $router->get('visual-editor', 'VisualEditorController@index');
 });
 
 /*

File diff suppressed because it is too large
+ 5 - 0
public/vendor/grapes/font-awesome.min.css


+ 273 - 0
public/vendor/grapes/grapes.init.js

@@ -0,0 +1,273 @@
+const panelsConfig = {
+    stylePrefix: 'pn-', // 面板的样式前缀
+    defaults: [
+        {
+            id: 'commands',
+            buttons: [{}],
+        },
+        {
+            id: 'options',
+            buttons: [
+                {
+                    active: false,
+                    id: 'sw-visibility',
+                    className: 'fa fa-square-o',
+                    command: 'core:component-outline',
+                    context: 'sw-visibility',
+                    attributes: { title: 'View components' },
+                },
+                {
+                    id: 'preview',
+                    className: 'fa fa-eye',
+                    command: 'preview',
+                    context: 'preview',
+                    attributes: { title: 'Preview' },
+                },
+                {
+                    id: 'fullscreen',
+                    className: 'fa fa-arrows-alt',
+                    command: 'fullscreen',
+                    context: 'fullscreen',
+                    attributes: { title: 'Fullscreen' },
+                },
+                // {
+                //     id: 'export-template',
+                //     className: 'fa fa-code',
+                //     command: 'export-template',
+                //     attributes: { title: 'View code' },
+                // },
+            ],
+        },
+        {
+            id: 'views',
+            buttons: [
+                // {
+                //     id: 'open-sm',
+                //     className: 'fa fa-paint-brush',
+                //     command: 'open-sm',
+                //     active: false,
+                //     togglable: false,
+                //     attributes: { title: 'Open Style Manager' },
+                // },
+                {
+                    id: 'open-tm',
+                    className: 'fa fa-cog',
+                    command: 'open-tm',
+                    active: true,
+                    togglable: true,
+                    attributes: { title: 'Settings' },
+                },
+                // {
+                //     id: 'open-layers',
+                //     className: 'fa fa-bars',
+                //     command: 'open-layers',
+                //     togglable: false,
+                //     attributes: { title: 'Open Layer Manager' },
+                // },
+                // {
+                //     id: 'open-blocks',
+                //     className: 'fa fa-th-large',
+                //     command: 'open-blocks',
+                //     togglable: false,
+                //     attributes: { title: 'Open Blocks' },
+                // },
+            ],
+        },
+    ],
+};
+
+// 定义自定义组件类型
+const myNewComponentTypes = (editor) => {
+    //连接属性
+    // editor.Components.addType('a', {
+    //     isComponent: (el) => el.tagName === 'A',
+    //     model: {
+    //         defaults: {
+    //             traits: [
+    //                 {
+    //                     type: 'text', // Type of the trait
+    //                     name: 'href', // (required) The name of the attribute/property to use on component
+    //                     label: 'URL', // The label you will see in Settings
+    //                 }
+    //             ],
+    //         },
+    //     },
+    // });
+
+    //图片属性
+    editor.Components.addType('img', {
+        isComponent: (el) => el.tagName === 'IMG',
+        model: {
+            defaults: {
+                traits: [
+                    {
+                        type: 'text', // Type of the trait
+                        name: 'width', // (required) The name of the attribute/property to use on component
+                        label: 'width', // The label you will see in Settings
+                    },
+                    {
+                        type: 'text', // Type of the trait
+                        name: 'height', // (required) The name of the attribute/property to use on component
+                        label: 'height', // The label you will see in Settings
+                    },
+                ],
+            },
+        },
+    });
+};
+
+// 创建 GrapesJS 编辑器实例
+const editor = grapesjs.init({
+    container: '#gjs', // 指定编辑器容器的 DOM 元素
+    fromElement: true, // 从 DOM 元素中加载初始内容
+    height: '100%', // 设置编辑器的高度
+    width: '100%', // 设置宽度自适应
+    storageManager: false,
+    panels: panelsConfig,
+    plugins: [myNewComponentTypes],
+    canvas: {
+        // 禁用对所有元素的选择
+        selectorManager: {
+            // 允许选择特定类名的元素
+            selector: '.your-class-name',
+        }
+    }
+});
+const am = editor.AssetManager;
+// 锁定整个画布
+editor.getWrapper().set({ locked: true });
+
+// 遍历所有组件并解锁具有edit属性的组件
+const edit_classes = ['gjs_edit','btn-container']; // 定义需要解锁的组件类名
+editor.getComponents().each(component => {
+    component.onAll(component => {
+        //解销CLASS
+        let html = component.toHTML();
+        const classes = component.getClasses();
+        // 判断是否有交集
+        const hasIntersection = classes.some(item => {
+            // 如果是数组,则递归检查子数组
+            if (Array.isArray(item)) {
+                return item.some(subItem => edit_classes.includes(subItem));
+            }
+            // 如果是字符串,直接检查是否在 edit_classes 中
+            return edit_classes.includes(item);
+        });
+
+        // 如果有交集,解锁组件
+        if (hasIntersection || component.get('attributes').gjs_edit == 'true') {
+            component.set({ locked: false });
+            component.setStyle({
+                border: '2px dashed  #3b97e3  '
+            });
+        }
+
+    })
+
+});
+
+
+
+// 监听组件选择事件
+editor.on('component:selected', (comp) => {
+    //去掉选中组件后的按钮
+    const selectedComponent = editor.getSelected();
+    const defaultToolbar = selectedComponent.get('toolbar');
+    selectedComponent.set({
+        //  toolbar: [ ...defaultToolbar, {  attributes: {class: commandIcon}, command: commandToAdd}]
+        toolbar: [ ]
+    });
+
+    //如果是连接与图片,显示右边的属性面板
+    let tags = ['a', 'img'];
+    const hasScheduleAttribute = selectedComponent.attributes;
+    if (tags.includes(selectedComponent.attributes.tagName)) {
+        editor.runCommand('open-tm');
+        // 获取 .gjs-cv-canvas-bg 元素
+        canvasBg = document.querySelector('.gjs-cv-canvas');
+        canvasBg.classList.remove('gjs-cv-canvas-bg');
+
+        var elements = document.querySelectorAll('.gjs-pn-views-container');
+        elements.forEach(function(element) {
+            element.style.display = 'block'; // 或者 'flex',取决于你希望如何显示这些元素
+        });
+    } else {
+        canvasBg = document.querySelector('.gjs-cv-canvas');
+        canvasBg.classList.add('gjs-cv-canvas-bg');
+        var elements = document.querySelectorAll('.gjs-pn-views-container');
+        elements.forEach(function(element) {
+            element.style.display = 'none'; // 或者 'flex',取决于你希望如何显示这些元素
+        });
+    }
+    // 双击图片时弹出图片管理器
+    if (selectedComponent.attributes.tagName == 'img') {
+        //双击图片时弹出图片管理器
+        const el = selectedComponent.getEl();
+        // 添加双击事件监听器
+        el.addEventListener('dblclick', () => {
+            am.open({
+                types: ['image'], // This is the default option
+                // Without select, nothing will happen on asset selection
+                select(asset, complete) {
+                    const selected = editor.getSelected();
+                    if (selected) {
+                        selected.addAttributes({ src: asset.getSrc() });
+                        // The default AssetManager UI will trigger `select(asset, false)`
+                        // on asset click and `select(asset, true)` on double-click
+                        complete && am.close();
+                    }
+                },
+            });
+        });
+    }
+    //其他
+});
+
+// 监听 load 事件
+editor.on('load', () => {
+    // 在这里执行你的初始化逻辑
+    // 获取目标元素
+    const targetElement = document.querySelector('.fa-arrows-alt');
+    // 创建下拉选择框
+    const dropdown = document.createElement('select');
+    dropdown.className = 'custom-select'; // 添加自定义样式类
+    dropdown.id = 'dropdown'; // 添加 id 属性
+    // 构建 option 标签
+    var options = '';
+    siteMenu.forEach(function (item) {
+        if (mid == item.id) {
+            options += `<option value="${item.id}" uri="${item.url}" selected>${item.title}</option>`;
+        } else {
+            options += `<option value="${item.id}" uri="${item.url}">${item.title}</option>`;
+        }
+    });
+    // 将生成的 option 标签添加到 dropdown 中
+    dropdown.innerHTML = options;
+    // 将下拉选择框插入到目标元素之后
+    targetElement.insertAdjacentElement('afterend', dropdown);
+    //
+    dropdownel = document.getElementById('dropdown');
+    // 监听 change 事件
+    dropdownel.addEventListener('change', function () {
+        // 获取选中的 option
+        var selectedOption = dropdownel.options[dropdownel.selectedIndex];
+        // 获取选中的 id 和 uri
+        var id = selectedOption.value;
+        var uri = selectedOption.getAttribute('uri');
+
+        if (id > 0) {
+            // 生成跳转的 URL
+            var redirectUrl = `?mid=${id}&uri=${encodeURIComponent(uri)}`;
+            // 跳转到生成的 URL
+            window.location.href = redirectUrl;
+        }
+    });
+
+});
+
+//更新时调用接口保存数据
+editor.on('update', () => {
+    const html = editor.getHtml();
+    // 你可以在这里添加自定义逻辑
+    console.log('GrapesJS updated the editor content', html);
+});

File diff suppressed because it is too large
+ 0 - 0
public/vendor/grapes/grapes.min.css


File diff suppressed because it is too large
+ 1 - 0
public/vendor/grapes/grapes.min.js


+ 90 - 0
resources/views/distributor/pages-custom/visual-editor.blade.php

@@ -0,0 +1,90 @@
+
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>Editor</title>
+    <link rel="stylesheet" href="/vendor/grapes/grapes.min.css">
+    <script src="/vendor/grapes/grapes.min.js"></script>
+    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/font-awesome.min.css">
+    <style>
+        body, html {
+            height: 100%;
+            margin: 0;
+        }
+        .gjs-mdl-dialog {
+            background-color: white;
+            color: #333;
+        }
+        .gjs-cv-canvas-bg {
+            width: 100%;
+        }
+        .gjs-pn-views-container {
+            display: none;
+        }
+        .gjs-pn-views {
+            border: none;
+        }
+
+        .gjs-pn-options {
+            right: unset;
+            left: 45%;
+        }
+        .gjs-pn-buttons {
+            text-align: right;
+            display:block;
+        }
+
+        /* 美化下拉选择框 */
+        .custom-select {
+            margin-left: 10px;
+            padding: 3px 16px; /* 增加内边距,为下拉内容留出空位 */
+            font-size: 14px; /* 字体大小 */
+            color: #444; /* 字体颜色 */
+            background-color: #fff; /* 背景颜色 */
+            border: 1px solid #ddd; /* 边框 */
+            border-radius: 4px; /* 圆角 */
+            outline: none; /* 去掉聚焦时的默认边框 */
+            cursor: pointer; /* 鼠标指针样式 */
+            transition: all 0.3s ease; /* 过渡效果 */
+            appearance: none; /* 移除默认的下拉箭头 */
+            position: relative; /* 为下拉箭头定位 */
+        }
+
+        /* 悬停效果 */
+        .custom-select:hover {
+            background-color: #f8f8f8; /* 悬停时的背景颜色 */
+            border-color: #ccc; /* 悬停时的边框颜色 */
+        }
+
+        /* 聚焦效果 */
+        .custom-select:focus {
+            border-color: #ccc; /* 聚焦时的边框颜色 */
+            box-shadow: 0 0 5px rgba(0, 123, 255, 0.2); /* 聚焦时的阴影 */
+        }
+
+        /* 下拉箭头样式 */
+        .custom-select::after {
+            content: '\f0d7'; /* FontAwesome 下拉箭头图标 */
+            font-family: 'FontAwesome';
+            position: absolute;
+            right: 8px; /* 调整箭头位置,使其与右边距对齐 */
+            top: 50%;
+            transform: translateY(-50%);
+            pointer-events: none; /* 防止点击事件 */
+            color: #444; /* 下拉箭头颜色 */
+        }
+
+    </style>
+</head>
+<body>
+<div id="gjs">
+    {!!$html!!}
+</div>
+<script>
+    var siteMenu = {!!$siteMenu!!};
+    var mid= {!!$mid!!};
+</script>
+<script src="/vendor/grapes/grapes.init.js"></script>
+</body>
+</html>

Some files were not shown because too many files changed in this diff