<?php
require __DIR__ . '/../../app/bootstrap.php';
require __DIR__ . '/../../app/database.php';
require __DIR__ . '/../../app/admin-sidebar.php';
require_login();

$adminName = isset($_SESSION['admin_name']) ? (string)$_SESSION['admin_name'] : 'Admin';
$adminUsername = isset($_SESSION['admin_username']) ? (string)$_SESSION['admin_username'] : 'admin';
$avatarText = strtoupper(substr($adminName !== '' ? $adminName : 'A', 0, 1));
$widgetTypes = module_widget_types();
$domains = array();
$modId = isset($_GET['id']) ? (int)$_GET['id'] : 0;
$isEdit = $modId > 0;
$error = '';
$success = '';
$created = isset($_GET['created']) && $_GET['created'] === '1';

$form = array(
    'mod_name' => '',
    'mod_code' => '',
    'domain_id' => 0,
    'sort_order' => 0,
    'status' => 'enabled',
    'mod_items' => array()
);

try {
    $domains = list_enabled_domain_options();
} catch (Exception $e) {}

if ($isEdit) {
    try {
        $existing = find_module_by_id($modId);
        if (!$existing) {
            $error = '未找到对应模块。';
            $isEdit = false;
        } else {
            $form['mod_name'] = (string)$existing['mod_name'];
            $form['mod_code'] = (string)$existing['mod_code'];
            $form['domain_id'] = (int)$existing['domain_id'];
            $form['sort_order'] = (int)$existing['sort_order'];
            $form['status'] = (string)$existing['status'];
            $decoded = json_decode((string)$existing['mod_items'], true);
            $form['mod_items'] = is_array($decoded) ? $decoded : array();
        }
    } catch (Exception $e) {
        $error = $e->getMessage();
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    require_csrf();
    $form['mod_name'] = isset($_POST['mod_name']) ? trim((string)$_POST['mod_name']) : '';
    $form['domain_id'] = isset($_POST['domain_id']) ? (int)$_POST['domain_id'] : 0;
    $form['sort_order'] = isset($_POST['sort_order']) ? (int)$_POST['sort_order'] : 0;
    $form['status'] = isset($_POST['status']) ? trim((string)$_POST['status']) : 'enabled';

    $modItems = array();
    // 优先使用单一 JSON 传输 (新方式)
    if (isset($_POST['mod_items_json']) && is_string($_POST['mod_items_json']) && $_POST['mod_items_json'] !== '') {
        $decoded = json_decode($_POST['mod_items_json'], true);
        if (is_array($decoded)) {
            foreach ($decoded as $raw) {
                $wt = isset($raw['widget']) ? trim((string)$raw['widget']) : '';
                if ($wt === '' || !isset($widgetTypes[$wt])) { continue; }
                $item = _sanitize_widget_item($raw, $widgetTypes);
                if ($item !== null) { $modItems[] = $item; }
            }
        }
    }
    $form['mod_items'] = $modItems;

    if (!$isEdit) {
        $form['mod_code'] = generate_unique_mod_code();
    }

    if ($form['mod_name'] === '') {
        $error = '请填写模块名称。';
    } elseif (empty($modItems)) {
        $error = '请至少添加一个组件。';
    } else {
        try {
            $saveData = array(
                'mod_name' => $form['mod_name'],
                'mod_code' => $form['mod_code'],
                'domain_id' => $form['domain_id'],
                'sort_order' => $form['sort_order'],
                'status' => $form['status'],
                'mod_items' => $modItems
            );
            $newId = save_module($saveData, $isEdit ? $modId : 0);
            if (!$isEdit) {
                header('Location: /admin/component/edit.php?id=' . $newId . '&created=1');
                exit;
            }
            $success = '模块保存成功。调用变量：{{mod_' . $form['mod_code'] . '}}';
        } catch (Exception $e) {
            $error = $e->getMessage();
        }
    }
}

$widgetTypesJson = json_encode($widgetTypes, JSON_UNESCAPED_UNICODE);
$formItemsJson = json_encode($form['mod_items'], JSON_UNESCAPED_UNICODE);
?><!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo e(APP_NAME); ?> - <?php echo $isEdit ? '编辑' : '新增'; ?>模块</title>
<link rel="stylesheet" href="/assets/css/admin-ui.css">
<style>
/* ─── 编辑器布局 ─── */
.me-meta { display:flex; align-items:center; flex-wrap:wrap; gap:10px; padding:0 0 14px; }
.me-meta .me-input { padding:7px 12px; border:1px solid #d1d5db; border-radius:8px; font-size:13px; background:#fff; min-width:160px; transition:border-color .2s; }
.me-meta .me-input:focus { outline:none; border-color:#6366f1; box-shadow:0 0 0 3px rgba(99,102,241,.1); }
.me-meta .me-select { padding:7px 10px; border:1px solid #d1d5db; border-radius:8px; font-size:13px; background:#fff; cursor:pointer; }
.me-meta .me-var { display:inline-flex; align-items:center; gap:4px; padding:5px 12px; background:#f0fdf4; border:1px solid #bbf7d0; border-radius:6px; font-size:12px; color:#15803d; font-weight:600; }

/* ─── 组件面板 (顶部横条) ─── */
.me-palette { display:flex; align-items:center; gap:6px; padding:10px 0; border-bottom:1px solid #e2e8f0; flex-shrink:0; overflow-x:auto; }
.me-palette-title { font-size:11px; font-weight:600; color:#94a3b8; text-transform:uppercase; letter-spacing:.5px; margin-right:4px; white-space:nowrap; }
.wp-btn { display:inline-flex; align-items:center; gap:4px; padding:6px 12px; border:1px solid #e2e8f0; border-radius:7px; background:#fff; cursor:grab; font-size:12px; font-weight:500; color:#475569; transition:all .15s; white-space:nowrap; user-select:none; }
.wp-btn:hover { border-color:#6366f1; color:#4f46e5; background:#eef2ff; transform:translateY(-1px); box-shadow:0 2px 6px rgba(99,102,241,.12); }
.wp-btn:active { cursor:grabbing; transform:scale(.96); }
.wp-btn .wp-ico { font-weight:700; min-width:16px; text-align:center; font-size:12px; }

/* ─── 主体：左列表 + 右预览 ─── */
.me-body { display:flex; height:calc(100vh - 280px); min-height:400px; overflow:hidden; border:1px solid #e2e8f0; border-radius:8px; }
.me-left { width:280px; min-width:240px; background:#fafbfc; border-right:1px solid #e2e8f0; display:flex; flex-direction:column; flex-shrink:0; }
.me-left-header { padding:10px 14px; border-bottom:1px solid #f1f5f9; display:flex; align-items:center; justify-content:space-between; }
.me-left-header h3 { font-size:13px; font-weight:600; color:#334155; margin:0; }
.me-left-header .me-count { font-size:11px; color:#94a3b8; }
.me-list { flex:1; overflow-y:auto; padding:10px; }
.me-list-empty { text-align:center; padding:40px 16px; color:#94a3b8; font-size:13px; }
.me-list-empty .me-empty-icon { font-size:32px; margin-bottom:10px; opacity:.3; }

/* ─── 组件卡片 ─── */
.wc { background:#fff; border:1px solid #e2e8f0; border-radius:8px; margin-bottom:6px; transition:all .18s; cursor:pointer; position:relative; }
.wc:hover { border-color:#c7d2fe; box-shadow:0 2px 8px rgba(99,102,241,.08); }
.wc.wc-active { border-color:#6366f1; box-shadow:0 0 0 2px rgba(99,102,241,.15); }
.wc.wc-dragging { opacity:.5; transform:scale(.97); }
.wc-top { display:flex; align-items:center; gap:6px; padding:8px 10px; }
.wc-drag { cursor:grab; color:#cbd5e1; font-size:13px; padding:2px; transition:color .15s; }
.wc-drag:hover { color:#6366f1; }
.wc-badge { display:inline-flex; align-items:center; gap:3px; padding:2px 6px; background:#f1f5f9; color:#475569; border-radius:4px; font-size:10px; font-weight:600; }
.wc-name { flex:1; font-size:12px; color:#334155; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.wc-del { background:none; border:none; color:#cbd5e1; cursor:pointer; font-size:15px; padding:2px 5px; border-radius:4px; transition:all .15s; line-height:1; }
.wc-del:hover { color:#dc2626; background:#fef2f2; }

/* ─── 拖拽占位线 ─── */
.me-drop-indicator { height:3px; background:#6366f1; border-radius:2px; margin:3px 0; transition:all .15s; }

/* ─── 右侧预览区 ─── */
.me-right { flex:1; display:flex; flex-direction:column; overflow:hidden; background:#fff; }
.me-right-header { display:flex; align-items:center; padding:10px 16px; background:#fafbfc; border-bottom:1px solid #e2e8f0; flex-shrink:0; font-size:13px; font-weight:600; color:#475569; }
.me-preview-wrap { flex:1; overflow-y:auto; padding:24px; }
.me-preview-frame { max-width:720px; margin:0 auto; background:#fff; border-radius:10px; border:1px solid #e2e8f0; min-height:160px; padding:24px; box-shadow:0 1px 3px rgba(0,0,0,.04); }

/* ─── 属性弹出层 ─── */
.me-popup-overlay { display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(15,23,42,.25); z-index:900; }
.me-popup-overlay.show { display:flex; align-items:center; justify-content:center; }
.me-popup { background:#fff; border-radius:12px; box-shadow:0 20px 60px rgba(0,0,0,.18); width:460px; max-width:92vw; max-height:80vh; display:flex; flex-direction:column; animation:mePopIn .2s ease; }
@keyframes mePopIn { from { transform:scale(.95) translateY(10px); opacity:0; } to { transform:scale(1) translateY(0); opacity:1; } }
.me-popup-head { display:flex; align-items:center; justify-content:space-between; padding:16px 20px; border-bottom:1px solid #e2e8f0; }
.me-popup-head h3 { font-size:14px; font-weight:700; color:#0f172a; margin:0; }
.me-popup-close { background:none; border:none; font-size:20px; color:#94a3b8; cursor:pointer; padding:4px 8px; border-radius:6px; line-height:1; transition:all .15s; }
.me-popup-close:hover { color:#dc2626; background:#fef2f2; }
.me-popup-body { flex:1; overflow-y:auto; padding:16px 20px; }
.me-field { margin-bottom:14px; }
.me-field label { display:block; font-size:12px; font-weight:600; color:#475569; margin-bottom:5px; }
.me-field input[type="text"], .me-field textarea, .me-field select { width:100%; padding:8px 12px; border:1px solid #d1d5db; border-radius:8px; font-size:13px; background:#fff; transition:border-color .2s; box-sizing:border-box; }
.me-field input:focus, .me-field textarea:focus { outline:none; border-color:#6366f1; box-shadow:0 0 0 3px rgba(99,102,241,.1); }
.me-field textarea { resize:vertical; min-height:70px; font-family:inherit; }
.me-field-row { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.me-style-section { margin-top:16px; padding-top:14px; border-top:1px solid #f1f5f9; }
.me-style-section h4 { font-size:11px; font-weight:600; color:#94a3b8; text-transform:uppercase; letter-spacing:.5px; margin:0 0 10px; }
.me-color-row { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
.me-color-row label { font-size:12px; color:#475569; min-width:50px; }
.me-color-row input[type="color"] { width:28px; height:28px; border:1px solid #e2e8f0; border-radius:6px; cursor:pointer; padding:2px; }
.me-color-row input[type="text"] { flex:1; padding:5px 8px; border:1px solid #e2e8f0; border-radius:6px; font-size:11px; font-family:monospace; }
.me-range-row { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
.me-range-row label { font-size:12px; color:#475569; min-width:50px; }
.me-range-row input[type="range"] { flex:1; }
.me-range-row .me-range-val { font-size:11px; color:#64748b; min-width:36px; text-align:right; }

/* ─── 预览内渲染 ─── */
.pv-mod > * { margin-bottom:10px; }
.pv-mod > *:last-child { margin-bottom:0; }
.pv-h1 { font-size:26px; font-weight:700; color:#0f172a; line-height:1.3; }
.pv-h2 { font-size:20px; font-weight:700; color:#1e293b; line-height:1.3; }
.pv-h3 { font-size:17px; font-weight:600; color:#334155; line-height:1.4; }
.pv-h4 { font-size:14px; font-weight:600; color:#475569; line-height:1.4; }
.pv-text { font-size:14px; line-height:1.7; color:#475569; white-space:pre-line; }
.pv-img { max-width:100%; border-radius:8px; cursor:pointer; transition:outline .15s; }
.pv-img:hover { outline:2px solid #6366f1; outline-offset:2px; }
.pv-video { width:100%; max-width:560px; border-radius:8px; }
.pv-link { color:#4f46e5; text-decoration:underline; font-size:14px; }
.pv-divider { border:none; border-top:1px solid #e2e8f0; margin:12px 0; }
.pv-placeholder { padding:20px; border:2px dashed #e2e8f0; border-radius:8px; text-align:center; color:#cbd5e1; font-size:13px; }

/* ─── 预览内联编辑 ─── */
.pv-editable { cursor:text; outline:none; border-radius:4px; transition:box-shadow .15s; }
.pv-editable:hover { box-shadow:0 0 0 2px rgba(99,102,241,.15); }
.pv-editable:focus { box-shadow:0 0 0 2px rgba(99,102,241,.35); }

/* ─── div 容器 ─── */
.pv-div { display:grid; gap:10px; border:1px dashed #cbd5e1; border-radius:8px; padding:12px; min-height:60px; position:relative; transition:border-color .15s; }
.pv-div:hover { border-color:#6366f1; }
.pv-div.pv-div-drop-over { border-color:#4f46e5; background:rgba(99,102,241,.03); box-shadow:0 0 0 2px rgba(79,70,229,.15); }
.pv-div-cell.pv-cell-drop-over { border-color:#6366f1; background:rgba(99,102,241,.06); }
.pv-div-label { position:absolute; top:-10px; left:10px; background:#fff; padding:0 6px; font-size:10px; color:#94a3b8; font-weight:600; letter-spacing:.3px; }
.pv-div-empty { grid-column:1/-1; text-align:center; padding:20px; color:#cbd5e1; font-size:12px; border:1px dashed #e2e8f0; border-radius:6px; }
.pv-div-cell { min-height:48px; border:1px dashed #e2e8f0; border-radius:6px; display:flex; align-items:center; justify-content:center; color:#cbd5e1; font-size:11px; transition:border-color .15s; position:relative; overflow:hidden; }
.pv-div-cell:hover { border-color:#a5b4fc; }
.pv-div-cell.pv-cell-filled { border-style:solid; border-color:#e2e8f0; }

/* ─── 左侧列表：子组件缩进 ─── */
.wc-children { padding:4px 4px 4px 20px; border-top:1px dashed #f1f5f9; }
.wc-children .wc { margin-bottom:4px; border-style:dashed; }
.wc-child-drop { min-height:30px; border:1px dashed #e2e8f0; border-radius:6px; margin:4px 4px 4px 20px; text-align:center; color:#cbd5e1; font-size:11px; padding:6px; }
.wc-child-drop.drag-over { border-color:#6366f1; background:rgba(99,102,241,.04); }

/* ─── 状态提示 ─── */
.me-toast { position:fixed; top:16px; right:20px; z-index:999; padding:12px 20px; border-radius:10px; font-size:13px; font-weight:500; box-shadow:0 8px 24px rgba(0,0,0,.12); transform:translateY(-20px); opacity:0; transition:all .3s; pointer-events:none; }
.me-toast.show { transform:translateY(0); opacity:1; }
.me-toast-ok { background:#f0fdf4; color:#166534; border:1px solid #bbf7d0; }
.me-toast-err { background:#fef2f2; color:#991b1b; border:1px solid #fecaca; }

#hiddenFields { display:none; }
</style>
</head>
<body>
<div class="admin-shell">
    <?php render_admin_sidebar('template_packs', $adminName, $adminUsername, $avatarText); ?>
    <main class="admin-content">
        <div class="content-wrap">
            <header class="topbar">
                <div>
                    <h1><?php echo $isEdit ? '编辑模块' : '新增模块'; ?></h1>
                    <p>通过拖拽组件构建模块内容，支持实时预览。</p>
                </div>
                <div class="topbar-actions">
                    <a class="btn-ghost" href="/admin/template-pack/list.php?tab=modules">取消</a>
                    <button type="button" class="btn-primary" id="btnSave"><?php echo $isEdit ? '保存' : '创建模块'; ?></button>
                </div>
            </header>
            <section class="page-card" style="padding:16px 20px;">
                <!-- 模块基本信息 -->
                <div class="me-meta">
                    <input class="me-input" type="text" id="fModName" value="<?php echo e($form['mod_name']); ?>" placeholder="模块名称 *" title="模块名称">
                    <select class="me-select" id="fDomain" title="适用域名">
                        <option value="0">全局</option>
                        <?php foreach ($domains as $d): ?>
                            <option value="<?php echo (int)$d['id']; ?>"<?php echo (int)$form['domain_id'] === (int)$d['id'] ? ' selected' : ''; ?>><?php echo e($d['domain']); ?></option>
                        <?php endforeach; ?>
                    </select>
                    <select class="me-select" id="fStatus" title="状态" style="width:80px;">
                        <option value="enabled"<?php echo $form['status'] === 'enabled' ? ' selected' : ''; ?>>启用</option>
                        <option value="disabled"<?php echo $form['status'] === 'disabled' ? ' selected' : ''; ?>>停用</option>
                    </select>
                    <?php if ($isEdit && $form['mod_code'] !== ''): ?>
                        <span class="me-var" title="模板调用变量">{{mod_<?php echo e($form['mod_code']); ?>}}</span>
                    <?php endif; ?>
                </div>

                <!-- 组件面板 -->
                <div class="me-palette">
                    <span class="me-palette-title">组件</span>
                    <?php foreach ($widgetTypes as $wk => $wv): ?>
                        <div class="wp-btn" draggable="true" data-widget="<?php echo e($wk); ?>">
                            <span class="wp-ico"><?php echo $wv['icon']; ?></span> <?php echo e($wv['label']); ?>
                        </div>
                    <?php endforeach; ?>
                </div>

                <!-- 主体 -->
                <div class="me-body">
                    <!-- 左：组件列表 -->
                    <div class="me-left">
                        <div class="me-left-header">
                            <h3>组件列表</h3>
                            <span class="me-count" id="wcCount">0 个组件</span>
                        </div>
                        <div class="me-list" id="wcList">
                            <div class="me-list-empty" id="wcEmpty">
                                <div class="me-empty-icon">🧩</div>
                                从上方拖拽组件到此处<br>或点击组件添加
                            </div>
                        </div>
                    </div>

                    <!-- 右：预览 -->
                    <div class="me-right">
                        <div class="me-right-header">实时预览</div>
                        <div class="me-preview-wrap" id="tabPreview">
                            <div class="me-preview-frame" id="pvFrame">
                                <div class="pv-placeholder" id="pvEmpty">添加组件后这里会实时显示预览效果</div>
                                <div class="pv-mod" id="pvContent" style="display:none;"></div>
                            </div>
                        </div>
                    </div>
                </div>
            </section>
        </div>

        <!-- 隐藏提交表单 -->
        <form method="post" action="/admin/component/edit.php<?php echo $isEdit ? '?id=' . $modId : ''; ?>" id="moduleForm" style="display:none;">
            <?php echo csrf_field(); ?>
            <input type="hidden" name="mod_name" id="hModName">
            <input type="hidden" name="domain_id" id="hDomain">
            <input type="hidden" name="sort_order" value="<?php echo (int)$form['sort_order']; ?>">
            <input type="hidden" name="status" id="hStatus">
            <input type="hidden" name="mod_items_json" id="hItems" value="">
            <div id="hiddenFields"></div>
        </form>
    </main>
</div>

<!-- 属性弹出层 -->
<div class="me-popup-overlay" id="popupOverlay">
    <div class="me-popup">
        <div class="me-popup-head">
            <h3 id="popupTitle">属性设置</h3>
            <button type="button" class="me-popup-close" id="popupClose">&times;</button>
        </div>
        <div class="me-popup-body" id="popupBody"></div>
    </div>
</div>

<!-- Toast -->
<div class="me-toast" id="toast"></div>

<script>
(function(){
'use strict';

var WT = <?php echo $widgetTypesJson; ?>;
var INIT_ITEMS = <?php echo $formItemsJson; ?>;

/* ── 状态 ── widgets 支持 children: [{id,type,label,value,style,children?}] */
var widgets = [];
var nextId = 1;
var activeId = null;

/* ── DOM ── */
var $list = document.getElementById('wcList');
var $count = document.getElementById('wcCount');
var $empty = document.getElementById('wcEmpty');
var $pvFrame = document.getElementById('pvContent');
var $pvEmpty = document.getElementById('pvEmpty');
var $popOverlay = document.getElementById('popupOverlay');
var $popTitle = document.getElementById('popupTitle');
var $popBody = document.getElementById('popupBody');
var $toast = document.getElementById('toast');

/* ════════ 工具 ════════ */
function esc(s){ if(typeof s!=='string') s=String(s||''); var d=document.createElement('div'); d.appendChild(document.createTextNode(s)); return d.innerHTML; }
function escAttr(s){ return esc(s).replace(/"/g,'&quot;'); }
function showToast(msg,ok){ $toast.textContent=msg; $toast.className='me-toast '+(ok?'me-toast-ok':'me-toast-err')+' show'; setTimeout(function(){$toast.classList.remove('show');},2500); }

/* 深度查找 widget by id (支持 children) */
function findWidget(id, arr){
    arr = arr || widgets;
    for(var i=0;i<arr.length;i++){
        if(arr[i].id===id) return arr[i];
        if(arr[i].children){ var c=findWidget(id,arr[i].children); if(c) return c; }
    }
    return null;
}
/* 查找 widget 所在数组及 index */
function findLoc(id, arr){
    arr = arr || widgets;
    for(var i=0;i<arr.length;i++){
        if(arr[i].id===id) return {arr:arr, idx:i};
        if(arr[i].children){ var c=findLoc(id,arr[i].children); if(c) return c; }
    }
    return null;
}
function findIndex(id){ for(var i=0;i<widgets.length;i++) if(widgets[i].id===id) return i; return -1; }
function countAll(arr){ var n=0; (arr||widgets).forEach(function(w){ n++; if(w.children) n+=countAll(w.children); }); return n; }
/* 检查 targetId 是否是 w 的后代 (防止把容器拖入自己的子孙) */
function isDescendant(w, targetId){
    if(!w.children) return false;
    for(var i=0;i<w.children.length;i++){
        if(w.children[i].id===targetId) return true;
        if(isDescendant(w.children[i], targetId)) return true;
    }
    return false;
}

/* ════════ 左侧列表 ════════ */
function renderList(){
    while($list.firstChild) $list.removeChild($list.firstChild);
    var total = countAll(widgets);
    if(total===0){ $list.appendChild($empty); $empty.style.display=''; $count.textContent='0 个组件'; return; }
    $empty.style.display='none';
    $count.textContent = total+' 个组件';
    widgets.forEach(function(w){ $list.appendChild(buildCard(w, false)); });
}
function buildCard(w, isChild){
    var wt = WT[w.type]||{label:'?',icon:'?'};
    var div = document.createElement('div');
    div.className = 'wc'+(activeId===w.id?' wc-active':'');
    div.setAttribute('data-id', w.id);
    div.draggable = true;
    var nameText = w.label||wt.label;
    var valP = '';
    if(w.type==='url') valP=(w.value&&w.value.text)||'';
    else if(w.type==='div') valP=(w.children?w.children.length:0)+'个子组件';
    else if(w.type!=='divider') valP=typeof w.value==='string'?w.value:'';
    if(valP.length>25) valP=valP.substring(0,25)+'…';
    div.innerHTML='<div class="wc-top">'
        +'<span class="wc-drag" title="拖拽排序">☰</span>'
        +'<span class="wc-badge">'+esc(wt.icon)+' '+esc(wt.label)+'</span>'
        +'<span class="wc-name">'+esc(nameText)+(valP?' <span style="color:#94a3b8;font-weight:400;">— '+esc(valP)+'</span>':'')+'</span>'
        +'<button type="button" class="wc-del" data-del="'+w.id+'" title="删除">×</button>'
        +'</div>';
    if(w.type==='div'){
        var ch = document.createElement('div');
        ch.className = 'wc-children';
        ch.setAttribute('data-parent', w.id);
        if(w.children && w.children.length){
            w.children.forEach(function(cw){ ch.appendChild(buildCard(cw, true)); });
        }
        var dropZone = document.createElement('div');
        dropZone.className = 'wc-child-drop';
        dropZone.setAttribute('data-parent', w.id);
        dropZone.textContent = '拖拽组件到此处';
        ch.appendChild(dropZone);
        div.appendChild(ch);
    }
    return div;
}

/* ════════ 预览渲染 ════════ */
function renderPreview(){
    if(widgets.length===0){ $pvFrame.style.display='none'; $pvEmpty.style.display=''; return; }
    $pvEmpty.style.display='none'; $pvFrame.style.display='';
    $pvFrame.innerHTML = renderWidgetList(widgets);
    bindPreviewEditing();
}
function renderWidgetList(arr){
    var html='';
    arr.forEach(function(w){ html += renderOneWidget(w); });
    return html;
}
function renderOneWidget(w){
    var st=w.style||{}, html='';
    switch(w.type){
        case 'heading': case 'h1': case 'h2': case 'h3': case 'h4':
            var hlvl=(w.type==='heading')?(st.headingLevel||1):(parseInt(w.type.charAt(1))||1);
            var htag='h'+hlvl;
            html+='<'+htag+' class="pv-heading pv-editable" contenteditable="true" data-wid="'+w.id+'"'+buildInlineStyle(st)+'>'+ esc(w.value||htag.toUpperCase()+' 标题')+'</'+htag+'>'; break;
        case 'text':
            html+='<div class="pv-text pv-editable" contenteditable="true" data-wid="'+w.id+'"'+buildInlineStyle(st)+'>'+ esc(w.value||'段落文本…').replace(/\n/g,'<br>')+'</div>'; break;
        case 'image':
            if(w.value){
                html+='<img class="pv-img" src="'+escAttr(w.value)+'" data-wid="'+w.id+'" alt=""'+buildImgStyle(st)+' onerror="this.style.opacity=0.3">';
                if(st.float==='left'||st.float==='right') html+='<div style="clear:both"></div>';
            } else {
                html+='<div class="pv-placeholder pv-img-ph" data-wid="'+w.id+'"'+buildInlineStyle(st)+'>📷 点击设置图片</div>';
            }
            break;
        case 'video':
            if(w.value){
                if(/\.(mp4|webm|ogg)(\?|$)/i.test(w.value)) html+='<video class="pv-video" src="'+escAttr(w.value)+'" controls preload="metadata"'+buildInlineStyle(st)+'></video>';
                else html+='<iframe class="pv-video" src="'+escAttr(w.value)+'" frameborder="0" allowfullscreen style="width:100%;height:320px;border-radius:8px"></iframe>';
            } else html+='<div class="pv-placeholder"'+buildInlineStyle(st)+'>▶ 请输入视频地址</div>';
            break;
        case 'url':
            var ut=(w.value&&w.value.text)||'链接文本';
            html+='<a class="pv-link pv-editable" contenteditable="true" data-wid="'+w.id+'" href="javascript:void(0)"'+buildInlineStyle(st)+'>'+esc(ut)+'</a>'; break;
        case 'button':
            var btnT=(w.value&&w.value.text)||'按钮';
            var btnBg=st.bgColor||'#6366f1', btnC=st.color||'#ffffff', btnR=st.borderRadius||6, btnFs=st.fontSize||14, btnPd=st.padding||'10px 24px';
            var btnS='display:inline-block;padding:'+(typeof btnPd==='number'?btnPd+'px':btnPd)+';background-color:'+btnBg+';color:'+btnC+';border-radius:'+btnR+'px;font-weight:600;font-size:'+btnFs+'px;text-decoration:none;border:none;cursor:pointer;line-height:1.5';
            var wrapS=st.textAlign?'text-align:'+st.textAlign:'';
            html+=(wrapS?'<div style="'+wrapS+'">':'')+'<a class="pv-btn" data-wid="'+w.id+'" href="javascript:void(0)" style="'+btnS+'">'+esc(btnT)+'</a>'+(wrapS?'</div>':'');
            break;
        case 'divider':
            html+='<hr class="pv-divider"'+buildInlineStyle(st)+'>'; break;
        case 'div':
            var cols=parseInt(st.columns)||1;
            var rows=parseInt(st.rows)||1;
            var totalCells=cols*rows;
            var ds='display:grid;grid-template-columns:repeat('+cols+',1fr);grid-template-rows:repeat('+rows+',auto);gap:'+(parseInt(st.gap)||10)+'px;';
            if(st.bgColor) ds+='background-color:'+st.bgColor+';';
            if(st.padding) ds+='padding:'+st.padding+'px;';
            if(st.margin) ds+='margin:'+st.margin+'px;';
            if(st.borderRadius) ds+='border-radius:'+st.borderRadius+'px;';
            if(st.textAlign) ds+='text-align:'+st.textAlign+';';
            var ctag=(st.containerTag==='p')?'p':'div';
            html+='<div class="pv-div" data-wid="'+w.id+'" style="'+ds+'">';
            html+='<span class="pv-div-label">&lt;'+ctag+'&gt; '+esc(w.label||'容器')+' ('+cols+'×'+rows+')</span>';
            var ch=w.children||[];
            for(var ci=0;ci<totalCells;ci++){
                if(ci<ch.length){
                    html+='<div class="pv-div-cell pv-cell-filled">'+renderOneWidget(ch[ci])+'</div>';
                } else {
                    html+='<div class="pv-div-cell">'+(ci<ch.length?'':'<span>'+(ci+1)+'</span>')+'</div>';
                }
            }
            if(ch.length>totalCells){
                for(var oi=totalCells;oi<ch.length;oi++){
                    html+='<div class="pv-div-cell pv-cell-filled">'+renderOneWidget(ch[oi])+'</div>';
                }
            }
            html+='</div>';
            break;
    }
    return html;
}
function buildInlineStyle(st){
    if(!st) return '';
    var p=[];
    if(st.color) p.push('color:'+st.color);
    if(st.bgColor) p.push('background-color:'+st.bgColor);
    if(st.fontSize) p.push('font-size:'+st.fontSize+'px');
    if(st.textAlign) p.push('text-align:'+st.textAlign);
    if(st.padding) p.push('padding:'+st.padding+'px');
    if(st.borderRadius) p.push('border-radius:'+st.borderRadius+'px');
    return p.length?' style="'+p.join(';')+'"':'';
}
function buildImgStyle(st){
    if(!st) return '';
    var p=[];
    if(st.float==='left'){p.push('float:left');p.push('margin:0 16px 12px 0');}
    else if(st.float==='right'){p.push('float:right');p.push('margin:0 0 12px 16px');}
    if(st.imgWidth) p.push('width:'+st.imgWidth+(String(st.imgWidth).indexOf('%')>=0?'':'px'));
    if(st.imgHeight) p.push('height:'+st.imgHeight+(String(st.imgHeight).indexOf('%')>=0?'':'px'));
    if(st.bgColor) p.push('background-color:'+st.bgColor);
    if(st.padding) p.push('padding:'+st.padding+'px');
    if(st.borderRadius) p.push('border-radius:'+st.borderRadius+'px;overflow:hidden');
    if(st.textAlign==='center'&&!st.float) p.push('display:block;margin-left:auto;margin-right:auto');
    return p.length?' style="'+p.join(';')+'"':'';
}

/* ════════ 预览内联编辑绑定 ════════ */
function bindPreviewEditing(){
    // contenteditable 文本
    $pvFrame.querySelectorAll('.pv-editable[data-wid]').forEach(function(el){
        el.addEventListener('blur', function(){
            var wid = parseInt(this.getAttribute('data-wid'));
            var w = findWidget(wid);
            if(!w) return;
            var txt = this.innerText.trim();
            if(w.type==='url'){ if(!w.value||typeof w.value!=='object') w.value={text:'',href:''}; w.value.text=txt; }
            else { w.value=txt; }
            renderList();
        });
        el.addEventListener('keydown', function(e){
            var _wt=findWidget(parseInt(this.getAttribute('data-wid'))); if(_wt&&['heading','h1','h2','h3','h4','url','button'].indexOf(_wt.type)>=0){ e.preventDefault(); this.blur(); }
        });
    });
    // 所有预览组件点击 → 弹出属性设置
    $pvFrame.querySelectorAll('[data-wid]').forEach(function(el){
        el.addEventListener('click', function(e){
            if(this.getAttribute('contenteditable')==='true') return;
            e.preventDefault(); e.stopPropagation();
            var wid = parseInt(this.getAttribute('data-wid'));
            if(wid) openPopup(wid);
        });
    });
}
/* ════════ 预览区拖放（事件委托 + 缓存高亮状态）════════ */
var pvDropTarget = { wid: null, cellIdx: -1 }; // dragover 时记住目标
function clearPvHighlights(){
    document.querySelectorAll('.pv-div-drop-over').forEach(function(d){d.classList.remove('pv-div-drop-over');});
    document.querySelectorAll('.pv-cell-drop-over').forEach(function(d){d.classList.remove('pv-cell-drop-over');});
}
function closestPvDiv(target){
    var el=target;
    while(el&&el!==$pvFrame){
        if(el.classList&&el.classList.contains('pv-div')&&el.hasAttribute('data-wid')) return el;
        el=el.parentElement;
    }
    return null;
}
function cellIndexInDiv(cell, pvDiv){
    if(!cell||!pvDiv||cell.parentElement!==pvDiv) return -1;
    var cells=pvDiv.querySelectorAll(':scope > .pv-div-cell');
    for(var i=0;i<cells.length;i++) if(cells[i]===cell) return i;
    return -1;
}
$pvFrame.addEventListener('dragover', function(e){
    var div=closestPvDiv(e.target);
    if(!div){ clearPvHighlights(); pvDropTarget.wid=null; return; }
    e.preventDefault();
    clearPvHighlights();
    div.classList.add('pv-div-drop-over');
    var wid=parseInt(div.getAttribute('data-wid'));
    /* 沿 e.target 向上找属于当前 div 的直接子 cell */
    var cellIdx=-1;
    var node=e.target;
    while(node&&node!==div){
        if(node.classList&&node.classList.contains('pv-div-cell')&&node.parentElement===div){
            cellIdx=cellIndexInDiv(node,div);
            node.classList.add('pv-cell-drop-over');
            break;
        }
        node=node.parentElement;
    }
    pvDropTarget.wid=wid;
    pvDropTarget.cellIdx=cellIdx;
});
$pvFrame.addEventListener('dragleave', function(e){
    if(!$pvFrame.contains(e.relatedTarget)){ clearPvHighlights(); pvDropTarget.wid=null; }
});
$pvFrame.addEventListener('drop', function(e){
    e.preventDefault();
    clearPvHighlights();
    /* 使用 dragover 阶段记住的目标，而非 drop 的 e.target */
    if(pvDropTarget.wid===null) return;
    var parent=findWidget(pvDropTarget.wid);
    if(!parent||parent.type!=='div') return;
    if(!parent.children) parent.children=[];
    var cellIdx=pvDropTarget.cellIdx;
    var data=e.dataTransfer.getData('text/plain');

    if(data.indexOf('palette:')===0){
        var type=data.substring(8);
        var nw=makeWidget(type);
        if(cellIdx>=0) parent.children.splice(Math.min(cellIdx, parent.children.length),0,nw);
        else parent.children.push(nw);
        activeId=nw.id;
        pvDropTarget.wid=null;
        renderAll(); openPopup(nw.id);
    } else if(data.indexOf('reorder:')===0){
        var srcId=parseInt(data.substring(8));
        var w=findWidget(srcId);
        if(!w||w.id===pvDropTarget.wid||isDescendant(w,pvDropTarget.wid)) return;
        var loc=findLoc(srcId);
        if(!loc) return;
        loc.arr.splice(loc.idx,1);
        if(cellIdx>=0) parent.children.splice(Math.min(cellIdx, parent.children.length),0,w);
        else parent.children.push(w);
        activeId=w.id;
        pvDropTarget.wid=null;
        renderAll();
    }
});

/* ════════ 属性弹出层 ════════ */
function openPopup(id){
    var w=findWidget(id);
    if(!w){ return; }
    activeId=id;
    renderList();
    var wt=WT[w.type]||{label:'?',icon:'?'}; var st=w.style||{};
    $popTitle.textContent=wt.icon+' '+wt.label+' 属性设置';
    var html='';
    html+='<div class="me-field"><label>备注名称</label><input type="text" data-prop="label" value="'+escAttr(w.label||'')+'" placeholder="'+escAttr(wt.label)+'"></div>';

    if(w.type==='url'){
        var urlT=(w.value&&w.value.text)||'', urlH=(w.value&&w.value.href)||'';
        html+='<div class="me-field-row"><div class="me-field"><label>链接文本</label><input type="text" data-prop="url_text" value="'+escAttr(urlT)+'" placeholder="点击查看"></div>';
        html+='<div class="me-field"><label>链接地址</label><input type="text" data-prop="url_href" value="'+escAttr(urlH)+'" placeholder="https://..."></div></div>';
    } else if(w.type==='button'){
        var bT=(w.value&&w.value.text)||'', bH=(w.value&&w.value.href)||'';
        html+='<div class="me-field"><label>按钮文本</label><input type="text" data-prop="url_text" value="'+escAttr(bT)+'" placeholder="立即查看"></div>';
        html+='<div class="me-field"><label>链接地址</label><input type="text" data-prop="url_href" value="'+escAttr(bH)+'" placeholder="https://..."></div>';
        html+='<div class="me-field"><label>打开方式</label><select data-style="target"><option value=""'+((!st.target||st.target==='_self')?' selected':'')+'>当前窗口</option><option value="_blank"'+(st.target==='_blank'?' selected':'')+'>新窗口</option></select></div>';
    } else if(w.type==='heading'){
        html+='<div class="me-field"><label>标题级别</label><select data-style="headingLevel"><option value="1"'+((!st.headingLevel||st.headingLevel==1)?' selected':'')+'>H1 一级标题</option><option value="2"'+(st.headingLevel==2?' selected':'')+'>H2 二级标题</option><option value="3"'+(st.headingLevel==3?' selected':'')+'>H3 三级标题</option><option value="4"'+(st.headingLevel==4?' selected':'')+'>H4 四级标题</option></select></div>';
    } else if(w.type==='div'){
        html+='<div class="me-field"><label>容器标签</label><select data-style="containerTag"><option value="div"'+(st.containerTag==='div'||!st.containerTag?' selected':'')+'>div</option><option value="p"'+(st.containerTag==='p'?' selected':'')+'>p</option></select></div>';
    } else if(w.type==='divider'){
        // 无内容
    } else if(wt.input==='textarea'){
        html+='<div class="me-field"><label>内容</label><textarea data-prop="value" rows="5" placeholder="输入内容…">'+esc(w.value||'')+'</textarea></div>';
    } else {
        html+='<div class="me-field"><label>内容</label><input type="text" data-prop="value" value="'+escAttr(w.value||'')+'" placeholder="'+(wt.placeholder||'输入内容')+'"></div>';
    }

    // ── 外观设置 ──
    html+='<div class="me-style-section"><h4>外观设置</h4>';
    if(w.type==='div'){
        html+='<div class="me-field-row">';
        html+='<div class="me-field"><label>列数</label><input type="number" data-style="columns" min="1" max="12" value="'+(st.columns||1)+'" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:13px;box-sizing:border-box;"></div>';
        html+='<div class="me-field"><label>行数</label><input type="number" data-style="rows" min="1" max="20" value="'+(st.rows||1)+'" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:13px;box-sizing:border-box;"></div>';
        html+='</div>';
        html+='<div class="me-field-row">';
        html+='<div class="me-field"><label>间距(px)</label><input type="number" data-style="gap" min="0" max="60" value="'+(st.gap||10)+'" style="width:100%;padding:8px 12px;border:1px solid #d1d5db;border-radius:8px;font-size:13px;box-sizing:border-box;"></div>';
        html+='<div class="me-field"></div>';
        html+='</div>';
        html+='<div class="me-color-row"><label>背景色</label><input type="color" data-style="bgColor" value="'+(st.bgColor||'#ffffff')+'"><input type="text" data-style="bgColor" value="'+escAttr(st.bgColor||'')+'" placeholder="无"></div>';
        html+='<div class="me-range-row"><label>内边距</label><input type="range" data-style="padding" min="0" max="60" value="'+(st.padding||0)+'"><span class="me-range-val">'+(st.padding||'0')+'px</span></div>';
        html+='<div class="me-range-row"><label>外边距</label><input type="range" data-style="margin" min="0" max="60" value="'+(st.margin||0)+'"><span class="me-range-val">'+(st.margin||'0')+'px</span></div>';
        html+='<div class="me-range-row"><label>圆角</label><input type="range" data-style="borderRadius" min="0" max="50" value="'+(st.borderRadius||0)+'"><span class="me-range-val">'+(st.borderRadius||'0')+'px</span></div>';
        html+='<div class="me-field"><label>对齐方式</label><select data-style="textAlign"><option value="">默认</option><option value="left"'+(st.textAlign==='left'?' selected':'')+'>左对齐</option><option value="center"'+(st.textAlign==='center'?' selected':'')+'>居中</option><option value="right"'+(st.textAlign==='right'?' selected':'')+'>右对齐</option></select></div>';
    } else if(w.type==='button'){
        html+='<div class="me-color-row"><label>文字色</label><input type="color" data-style="color" value="'+(st.color||'#ffffff')+'"><input type="text" data-style="color" value="'+escAttr(st.color||'')+'" placeholder="#ffffff"></div>';
        html+='<div class="me-color-row"><label>背景色</label><input type="color" data-style="bgColor" value="'+(st.bgColor||'#6366f1')+'"><input type="text" data-style="bgColor" value="'+escAttr(st.bgColor||'')+'" placeholder="#6366f1"></div>';
        var bfs=st.fontSize||14;
        html+='<div class="me-range-row"><label>字号</label><input type="range" data-style="fontSize" min="12" max="40" value="'+bfs+'"><span class="me-range-val">'+bfs+'px</span></div>';
        html+='<div class="me-range-row"><label>圆角</label><input type="range" data-style="borderRadius" min="0" max="50" value="'+(st.borderRadius||6)+'"><span class="me-range-val">'+(st.borderRadius||'6')+'px</span></div>';
        html+='<div class="me-range-row"><label>内边距</label><input type="range" data-style="padding" min="4" max="40" value="'+(st.padding||10)+'"><span class="me-range-val">'+(st.padding||'10')+'px</span></div>';
        html+='<div class="me-field"><label>位置</label><select data-style="textAlign"><option value="">默认(左)</option><option value="center"'+(st.textAlign==='center'?' selected':'')+'>居中</option><option value="right"'+(st.textAlign==='right'?' selected':'')+'>靠右</option></select></div>';
    } else if(w.type!=='divider'){
        if(['heading','text','url'].indexOf(w.type)>=0)
            html+='<div class="me-color-row"><label>文字色</label><input type="color" data-style="color" value="'+(st.color||'#334155')+'"><input type="text" data-style="color" value="'+escAttr(st.color||'')+'" placeholder="默认"></div>';
        html+='<div class="me-color-row"><label>背景色</label><input type="color" data-style="bgColor" value="'+(st.bgColor||'#ffffff')+'"><input type="text" data-style="bgColor" value="'+escAttr(st.bgColor||'')+'" placeholder="无"></div>';
        if(['heading','text','url'].indexOf(w.type)>=0){
            var fs=st.fontSize||'';
            html+='<div class="me-range-row"><label>字号</label><input type="range" data-style="fontSize" min="12" max="60" value="'+(fs||16)+'"><span class="me-range-val">'+(fs||'默认')+'</span></div>';
        }
        html+='<div class="me-field"><label>对齐方式</label><select data-style="textAlign"><option value="">默认</option><option value="left"'+(st.textAlign==='left'?' selected':'')+'>左对齐</option><option value="center"'+(st.textAlign==='center'?' selected':'')+'>居中</option><option value="right"'+(st.textAlign==='right'?' selected':'')+'>右对齐</option></select></div>';
        html+='<div class="me-range-row"><label>内边距</label><input type="range" data-style="padding" min="0" max="60" value="'+(st.padding||0)+'"><span class="me-range-val">'+(st.padding||'0')+'px</span></div>';
        if(['image','video','url'].indexOf(w.type)>=0)
            html+='<div class="me-range-row"><label>圆角</label><input type="range" data-style="borderRadius" min="0" max="50" value="'+(st.borderRadius||0)+'"><span class="me-range-val">'+(st.borderRadius||'0')+'px</span></div>';
        if(w.type==='image'){
            html+='<div class="me-field"><label>浮动方式</label><select data-style="float"><option value="">无</option><option value="left"'+(st.float==='left'?' selected':'')+'>左浮动</option><option value="right"'+(st.float==='right'?' selected':'')+'>右浮动</option></select></div>';
            html+='<div class="me-field-row"><div class="me-field"><label>宽度</label><input type="text" data-style="imgWidth" value="'+escAttr(st.imgWidth||'')+'" placeholder="300 或 50%"></div>';
            html+='<div class="me-field"><label>高度</label><input type="text" data-style="imgHeight" value="'+escAttr(st.imgHeight||'')+'" placeholder="200 或 auto"></div></div>';
        }
    }
    html+='</div>';

    $popBody.innerHTML=html;

    // 绑定 data-prop
    $popBody.querySelectorAll('[data-prop]').forEach(function(el){
        el.addEventListener('input', function(){
            var p=this.getAttribute('data-prop');
            if(p==='label') w.label=this.value;
            else if(p==='value') w.value=this.value;
            else if(p==='url_text'){if(!w.value||typeof w.value!=='object') w.value={text:'',href:''}; w.value.text=this.value;}
            else if(p==='url_href'){if(!w.value||typeof w.value!=='object') w.value={text:'',href:''}; w.value.href=this.value;}
            renderPreview(); renderList();
        });
    });
    // 绑定 data-style
    $popBody.querySelectorAll('[data-style]').forEach(function(el){
        var evt=(el.tagName==='SELECT')?'change':'input';
        el.addEventListener(evt, function(){
            var key=this.getAttribute('data-style');
            if(!w.style) w.style={};
            if(this.type==='range'){
                w.style[key]=parseInt(this.value);
                var vs=this.parentElement.querySelector('.me-range-val');
                if(vs) vs.textContent=this.value+(key==='fontSize'?'':'px');
            } else if(this.type==='color'){
                w.style[key]=this.value;
                var tw=this.parentElement.querySelector('input[type="text"][data-style="'+key+'"]');
                if(tw&&tw!==this) tw.value=this.value;
            } else if(this.type==='text'&&this.getAttribute('data-style')){
                w.style[key]=this.value;
                var tc=this.parentElement.querySelector('input[type="color"][data-style="'+key+'"]');
                if(tc&&tc!==this&&/^#[0-9a-f]{6}$/i.test(this.value)) tc.value=this.value;
            } else if(this.type==='number'){
                w.style[key]=parseInt(this.value)||0;
            } else {
                w.style[key]=this.value;
            }
            renderPreview();
        });
    });
    $popOverlay.classList.add('show');
}
function closePopup(){
    $popOverlay.classList.remove('show');
}
document.getElementById('popupClose').addEventListener('click', closePopup);
$popOverlay.addEventListener('click', function(e){ if(e.target===$popOverlay) closePopup(); });

/* ════════ 组件创建 ════════ */
function makeWidget(type, label, value, style, children){
    var wt=WT[type]; if(!wt) return null;
    var defVal = (type==='url'||type==='button') ? {text:'',href:''} : '';
    var w={id:nextId++, type:type, label:label||'', value:value||defVal, style:style||{}};
    if(type==='button'&&typeof w.value==='string'){ w.value={text:w.value,href:''}; }
    if(type==='div'){ w.children=children||[]; if(!w.style.columns) w.style.columns=2; if(!w.style.rows) w.style.rows=1; }
    return w;
}
function addWidget(type, label, value, style, atIndex, children){
    var w=makeWidget(type, label, value, style, children);
    if(!w) return;
    if(typeof atIndex==='number'&&atIndex>=0&&atIndex<=widgets.length) widgets.splice(atIndex,0,w);
    else widgets.push(w);
    activeId=w.id;
    renderAll(); openPopup(w.id);
}
function removeWidget(id){
    var loc=findLoc(id);
    if(!loc) return;
    // 如果是 div，子组件一起删除
    loc.arr.splice(loc.idx,1);
    if(activeId===id) activeId=null;
    renderAll();
}
function renderAll(){ renderList(); renderPreview(); }

/* ════════ 左侧列表事件 ════════ */
$list.addEventListener('click', function(e){
    var delBtn=e.target.closest('.wc-del');
    if(delBtn){ e.stopPropagation(); removeWidget(parseInt(delBtn.getAttribute('data-del'))); return; }
    var card=e.target.closest('.wc');
    if(card){ var wid=parseInt(card.getAttribute('data-id')); openPopup(wid); }
});

/* ════════ 面板按钮 ════════ */
document.querySelectorAll('.wp-btn').forEach(function(btn){
    btn.addEventListener('click', function(){ addWidget(this.getAttribute('data-widget')); });
    btn.addEventListener('dragstart', function(e){
        e.dataTransfer.setData('text/plain','palette:'+this.getAttribute('data-widget'));
        e.dataTransfer.effectAllowed='copy';
    });
});

/* ════════ 拖拽排序 ════════ */
var dragSrcId=null;
$list.addEventListener('dragstart', function(e){
    var card=e.target.closest('.wc'); if(!card) return;
    dragSrcId=parseInt(card.getAttribute('data-id'));
    card.classList.add('wc-dragging');
    e.dataTransfer.setData('text/plain','reorder:'+dragSrcId);
    e.dataTransfer.effectAllowed='move';
});
$list.addEventListener('dragend', function(){
    dragSrcId=null;
    $list.querySelectorAll('.wc').forEach(function(c){c.classList.remove('wc-dragging');});
    $list.querySelectorAll('.me-drop-indicator').forEach(function(d){d.remove();});
    $list.querySelectorAll('.drag-over').forEach(function(d){d.classList.remove('drag-over');});
});
$list.addEventListener('dragover', function(e){
    e.preventDefault();
    $list.querySelectorAll('.me-drop-indicator').forEach(function(d){d.remove();});
    $list.querySelectorAll('.drag-over').forEach(function(d){d.classList.remove('drag-over');});
    // 检测是否拖到子组件区域
    var childDrop=e.target.closest('.wc-child-drop');
    if(childDrop){ childDrop.classList.add('drag-over'); return; }
    var childArea=e.target.closest('.wc-children');
    if(childArea){
        var childCard=e.target.closest('.wc');
        if(childCard&&childArea.contains(childCard)){
            var rect=childCard.getBoundingClientRect();
            var ind=document.createElement('div'); ind.className='me-drop-indicator';
            if(e.clientY<rect.top+rect.height/2) childCard.parentNode.insertBefore(ind,childCard);
            else childCard.parentNode.insertBefore(ind,childCard.nextSibling);
        }
        return;
    }
    var card=e.target.closest('.wc');
    if(card&&!card.closest('.wc-children')){
        var rect=card.getBoundingClientRect();
        var ind=document.createElement('div'); ind.className='me-drop-indicator';
        if(e.clientY<rect.top+rect.height/2) card.parentNode.insertBefore(ind,card);
        else card.parentNode.insertBefore(ind,card.nextSibling);
    }
});
$list.addEventListener('dragleave', function(e){
    if(!$list.contains(e.relatedTarget)){
        $list.querySelectorAll('.me-drop-indicator').forEach(function(d){d.remove();});
        $list.querySelectorAll('.drag-over').forEach(function(d){d.classList.remove('drag-over');});
    }
});
$list.addEventListener('drop', function(e){
    e.preventDefault();
    $list.querySelectorAll('.me-drop-indicator').forEach(function(d){d.remove();});
    $list.querySelectorAll('.drag-over').forEach(function(d){d.classList.remove('drag-over');});
    var data=e.dataTransfer.getData('text/plain');

    // 拖到 div 子区域
    var childDrop=e.target.closest('.wc-child-drop');
    var childArea=e.target.closest('.wc-children');
    if(childDrop||childArea){
        var pid=parseInt((childDrop||childArea).getAttribute('data-parent'));
        var parent=findWidget(pid);
        if(!parent||parent.type!=='div') return;
        if(!parent.children) parent.children=[];
        if(data.indexOf('palette:')===0){
            var type=data.substring(8);
            var nw=makeWidget(type);
            // 确定插入位置
            var childCard=e.target.closest('.wc');
            if(childCard&&childArea&&childArea.contains(childCard)){
                var cid=parseInt(childCard.getAttribute('data-id'));
                var ci=-1;
                for(var k=0;k<parent.children.length;k++) if(parent.children[k].id===cid){ci=k;break;}
                if(ci>=0){
                    var cr=childCard.getBoundingClientRect();
                    var cIdx=(e.clientY<cr.top+cr.height/2)?ci:ci+1;
                    parent.children.splice(cIdx,0,nw);
                } else parent.children.push(nw);
            } else parent.children.push(nw);
            activeId=nw.id; renderAll(); openPopup(nw.id);
        } else if(data.indexOf('reorder:')===0){
            var srcId=parseInt(data.substring(8));
            var sw=findWidget(srcId);
            if(!sw||sw.id===pid||isDescendant(sw,pid)) return;
            var loc=findLoc(srcId); if(!loc) return;
            loc.arr.splice(loc.idx,1);
            parent.children.push(sw);
            activeId=sw.id; renderAll();
        }
        return;
    }

    // 顶层排序/添加
    var targetCard=e.target.closest('.wc');
    var dropIdx=widgets.length;
    if(targetCard&&!targetCard.closest('.wc-children')){
        var tid=parseInt(targetCard.getAttribute('data-id'));
        var tIdx=findIndex(tid);
        if(tIdx>=0){
            var rect=targetCard.getBoundingClientRect();
            dropIdx=(e.clientY<rect.top+rect.height/2)?tIdx:tIdx+1;
        }
    }
    if(data.indexOf('palette:')===0){
        addWidget(data.substring(8),'','',{},dropIdx);
    } else if(data.indexOf('reorder:')===0){
        var srcId2=parseInt(data.substring(8));
        var loc2=findLoc(srcId2); if(!loc2) return;
        var item=loc2.arr.splice(loc2.idx,1)[0];
        // 重新计算 index (可能 splice 影响了)
        var newIdx=Math.min(dropIdx, widgets.length);
        widgets.splice(newIdx,0,item);
        renderAll();
    }
});

/* ════════ 保存 ════════ */
document.getElementById('btnSave').addEventListener('click', function(){
    var modName=document.getElementById('fModName').value.trim();
    if(!modName){ showToast('请输入模块名称',false); document.getElementById('fModName').focus(); return; }
    if(widgets.length===0){ showToast('请至少添加一个组件',false); return; }

    document.getElementById('hModName').value=modName;
    document.getElementById('hDomain').value=document.getElementById('fDomain').value;
    document.getElementById('hStatus').value=document.getElementById('fStatus').value;

    // 序列化整个 widgets 为单一 JSON
    var items=[];
    function ser(w){
        var o={widget:w.type, label:w.label||'', value:w.value, style:JSON.parse(JSON.stringify(w.style||{}))};
        if(w.type==='div'&&w.children){
            o.children=[];
            w.children.forEach(function(c){ o.children.push(ser(c)); });
        }
        return o;
    }
    widgets.forEach(function(w){ items.push(ser(w)); });
    document.getElementById('hItems').value=JSON.stringify(items);
    console.log('[save] mod_items_json:', document.getElementById('hItems').value);
    document.getElementById('moduleForm').submit();
});
function flattenWidgets(arr, out){
    arr.forEach(function(w){
        var flat={type:w.type, label:w.label, value:w.value, style:w.style};
        if(w.type==='div'){
            flat.children=[];
            if(w.children) w.children.forEach(function(c){
                flat.children.push(serializeWidget(c));
            });
        }
        out.push(flat);
    });
}
function serializeWidget(w){
    var obj={widget:w.type, label:w.label, value:w.value, style:w.style};
    if(w.type==='div'&&w.children){
        obj.children=[];
        w.children.forEach(function(c){ obj.children.push(serializeWidget(c)); });
    }
    return obj;
}

/* ════════ 初始化 ════════ */
function loadItem(item){
    var type=item.widget||'';
    var w=makeWidget(type, item.label||'', item.value||'', item.style||{});
    if(!w) return null;
    if(type==='div'&&Array.isArray(item.children)){
        w.children=[];
        item.children.forEach(function(ci){
            var cw=loadItem(ci);
            if(cw) w.children.push(cw);
        });
    }
    return w;
}
console.log('[load] INIT_ITEMS:', JSON.stringify(INIT_ITEMS));
if(Array.isArray(INIT_ITEMS)){
    INIT_ITEMS.forEach(function(item){
        var w=loadItem(item);
        if(w) widgets.push(w);
    });
    if(widgets.length>0){ activeId=null; }
}
renderAll();

<?php if ($created): ?>
showToast('模块创建成功，可继续编辑', true);
<?php elseif ($success !== ''): ?>
showToast(<?php echo json_encode($success, JSON_UNESCAPED_UNICODE); ?>, true);
<?php elseif ($error !== ''): ?>
showToast(<?php echo json_encode($error, JSON_UNESCAPED_UNICODE); ?>, false);
<?php endif; ?>

})();
</script>
</body>
</html>
