跳到主要内容

混合模式

混合模式 = 拖拽搭骨架 + JsCode 补逻辑, 兼顾效率与灵活性, 是多数实战项目的首选工作流。用 FreeForm 快速放好所有可视控件, 再在 Code 模式 JsCode 段落里补 Alpine.data / darra.bind / 第三方库初始化 / 动态 DOM 生成。

工作流示意

┌──────────────────────────────────────────┐
│ FreeForm (80% 的工作量) │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 拖 <darra-tank> / <darra-gauge> │ │
│ │ 拖 <darra-button> / <darra-trend> │ │
│ │ 拖 <darra-alarm-list> │ │
│ │ Trait 填 var=... │ │
│ │ Style 面板调样式 │ │
│ └────────────────────────────────────┘ │
│ │
│ 保存 → HtmlCode/CssCode 自动生成 │
└────────────┬─────────────────────────────┘

↓ (切 Code 模式, 只改 JsCode)
┌──────────────────────────────────────────┐
│ Code 扩展 (20% 逻辑) │
│ │
│ Alpine.data / darra.on / Chart.js / │
│ 自定义状态机 / 第三方库 / 动画序列 │
└──────────────────────────────────────────┘

适用场景

场景为什么用混合
数据大屏KPI 卡片拖拽快, 联动逻辑需 JS
设备监控控件静态, 但条件渲染 / 告警弹窗需 Alpine
运动控制调试伺服参数表单拖拽, 点动节奏控制需 JS
报表页面表格 <darra-dx-grid> 拖一个, 数据源/导出用 JS
多 Tab 容器Tab 布局拖拽, 每 Tab 切换时数据懒加载用 JS

步骤 1: 拖拽骨架

以"设备总览"为例, FreeForm 放:

<!-- HtmlCode (自动生成) -->
<div class="page">
<darra-label class="title">设备总览</darra-label>

<div class="kpi-grid">
<div class="kpi-card" id="kpi-temp">
<div class="kpi-label">温度</div>
<darra-numeric var="DB1.Temp" format="0.0" unit=""></darra-numeric>
</div>
<div class="kpi-card" id="kpi-press">
<div class="kpi-label">压力</div>
<darra-numeric var="DB1.Pressure" format="0.00" unit="MPa"></darra-numeric>
</div>
<div class="kpi-card" id="kpi-level">
<div class="kpi-label">液位</div>
<darra-numeric var="DB1.Level" format="0.0" unit="%"></darra-numeric>
</div>
</div>

<darra-trend id="trend" vars="DB1.Temp,DB1.Pressure" max-points="200"></darra-trend>

<div id="alarm-region">
<darra-alarm-list max-rows="8"></darra-alarm-list>
</div>
</div>

步骤 2: 切 Code 模式补 JsCode

Ctrl+E 切 Code, 只编辑 JsCode 段:

// ===== 混合模式扩展: 设备总览联动 =====

darra.on('ready', () => {
console.log('[HMI] 就绪')
bootstrapAlertLogic()
bootstrapTrendLegend()
})

// ----- 1. KPI 卡片: 超阈值时变红, 伴随震动动画 -----
function bootstrapAlertLogic() {
darra.bindDB('DB1', (values) => {
setCardAlert('kpi-temp', values.Temperature > 80)
setCardAlert('kpi-press', values.Pressure > 8)
setCardAlert('kpi-level', values.Level > 95)
})
}

function setCardAlert(id, alert) {
const el = document.getElementById(id)
if (!el) return
el.classList.toggle('alert', alert)
if (alert) el.classList.add('shake'); else el.classList.remove('shake')
}

// ----- 2. 趋势图: 动态添加图例点击隐藏 -----
function bootstrapTrendLegend() {
// darra-trend 内部是 Chart.js, 可通过 DOM 找到
const trendEl = document.getElementById('trend')
trendEl.addEventListener('darra-trend-ready', (e) => {
const chart = e.detail.chart
chart.options.plugins.legend.onClick = (ev, item) => {
item.hidden = !item.hidden
chart.update()
}
})
}

// ----- 3. 右下角 toast: 每次报警推送时显示 -----
darra.on('alarm', (alarm) => {
const toast = document.createElement('div')
toast.className = 'toast toast-' + alarm.level
toast.innerHTML = `<strong>${alarm.source}</strong><br>${alarm.message}`
document.body.appendChild(toast)
setTimeout(() => toast.classList.add('show'), 10)
setTimeout(() => { toast.classList.remove('show'); setTimeout(() => toast.remove(), 300) }, 5000)
})

// ----- 4. F1 快捷键: 弹出帮助 -----
document.addEventListener('keydown', (e) => {
if (e.key === 'F1') {
e.preventDefault()
alert('设备总览页\n\nF2: 切换详情\nF3: 手动采集')
}
})

配套 CssCode 追加:

.kpi-card.alert {
border-color: var(--darra-danger);
color: var(--darra-danger);
box-shadow: 0 0 20px rgba(239,68,68,0.4);
}
.kpi-card.shake { animation: shake .4s ease; }
@keyframes shake {
0%,100% { transform: translateX(0); }
25% { transform: translateX(-4px); }
75% { transform: translateX(4px); }
}

.toast {
position: fixed;
right: 24px;
bottom: 24px;
padding: 12px 16px;
border-radius: 6px;
background: var(--darra-panel-bg);
box-shadow: var(--darra-shadow);
transform: translateX(400px);
transition: transform .3s ease;
}
.toast.show { transform: translateX(0); }
.toast-error { border-left: 4px solid var(--darra-danger); }
.toast-warn { border-left: 4px solid var(--darra-warning); }

步骤 3: Alpine 混入 (不替换拖拽生成的 HTML)

在 HtmlCode 根 <div> 追加 x-data, 不动 <darra-*>:

<div class="page" x-data="pageState()" x-init="bootstrap()">

<button @click="showSettings = !showSettings">设置</button>

<!-- 原拖拽内容保持不变 -->
<darra-tank var="DB1.Level"></darra-tank>
<darra-gauge var="DB1.Pressure"></darra-gauge>

<!-- Alpine 控制的悬浮面板 -->
<div class="settings-panel" x-show="showSettings" x-transition>
<darra-input var="DB2.SetpointTemp" min="20" max="100"></darra-input>
<darra-input var="DB2.SetpointPress" min="0" max="10" step="0.1"></darra-input>
</div>
</div>

<script>
function pageState() {
return {
showSettings: false,
bootstrap() {
darra.bind('M0.0', v => { if (!v) this.showSettings = false })
}
}
}
</script>

步骤 4: 动态 DOM (不能拖拽, 只能 JS)

动态生成的场景:

  • 设备列表 (数量由 PLC 决定)
  • 报警行的详情展开
  • 循环生成多个 <darra-axis>
darra.on('ready', async () => {
// 从 PLC DB 块读设备清单 (DB_Devices 是工程师建的设备元信息 DB 块)
const dbDef = await (await fetch('/api/hmi/db/DB_Devices')).json()
// dbDef.fields = [{ name: 'Pump1_Status', ... }, { name: 'Pump1_Speed', ... }, ...]
// 按字段前缀 Pump1/Pump2 分组, 每组一个设备卡片
const list = groupDevicesByPrefix(dbDef.fields)

const container = document.getElementById('device-container')
container.innerHTML = '' // 清空

list.forEach(dev => {
const card = document.createElement('div')
card.className = 'device-card'
card.innerHTML = `
<h3>${dev.name}</h3>
<darra-led var="${dev.statusVar}" color-on="#22C55E"></darra-led>
<darra-numeric var="${dev.speedVar}" unit="RPM"></darra-numeric>
`
container.appendChild(card)
})

// 通知 Alpine/darra 重新扫描新 DOM
Alpine.initTree(container)
})

约定: 拖拽 = 结构, JS = 行为

应拖拽应 JS
固定坐标的控件动态数量的元素
静态 var 绑定条件显示/隐藏
CSS 样式 (Style 面板)状态驱动的 class 切换
导航按钮业务流程控制
DevExtreme 基础配置DevExtreme 数据源 / 自定义 cell

重新切回 FreeForm 的风险

混合模式保存后, 如果再切 FreeForm:

  • GrapesJS 会保留 <darra-*> (能识别的)
  • 不能保留 JsCode 里动态插入的 DOM
  • Elements[]<darra-*> 位置重建, 动态元素被忽略

建议: 进入混合模式后只改 JsCode, HtmlCode / CssCode 的大结构调整也在 Code 模式里手改, 不回 FreeForm。

排错清单

问题原因解决
JS 里 document.getElementById 返回 null执行时机早于 DOM 挂载DOMContentLoadeddarra.on('ready', ...)
Alpine x-data 未响应动态插入的 DOM Alpine 未扫描Alpine.initTree(newEl)
<darra-*> 动态插入后不显示值Alpine 未初始化组件同上, 或手动 darra.bind 回调
切回 FreeForm 后部分元素消失无法反解析保持 Code 模式, 不回切

相关文档