跳到主要内容

代码式制作

Code 模式是 Darra HMI 的 纯代码编辑方式, 在 IDE 内嵌 Monaco 编辑器里直接写 HTML + CSS + JavaScript 三段, 适合复杂动态逻辑、第三方库集成、自定义控件开发。darra 全局对象已在页面运行时注入, 所有 <darra-*> Custom Element 可直接使用。

进入 Code 模式

  • 设计器顶部 切换到 Code 按钮
  • 快捷键 Ctrl+E
┌─────────────────────────────────────────────────┐
│ HTML | CSS | JS 预览 | 保存 │
├─────────────────────────────────────────────────┤
│ │
│ <!DOCTYPE html> │
│ <html lang="zh-CN"> │
│ <head>... │
│ <body class="theme-modern"> │
│ <darra-led var="M0.0"></darra-led> │
│ <div x-data="{ n:0 }"> │
│ <button @click="n++">+</button> │
│ <span x-text="n"></span> │
│ </div> │
│ </body> │
│ </html> │
│ │
└─────────────────────────────────────────────────┘

三段式结构

HtmlCode (标准 HTML5)

一个完整的 <!DOCTYPE html> 文档或一个 <div> 片段。建议以完整文档起手, HmiWebServer 会自动注入 <script src="/static/alpine.min.js"> 等:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>反应釜</title>

<!-- 框架脚本由 HmiWebServer 注入, 可省略 -->
<link rel="stylesheet" href="/static/darra-hmi.css" />
<script src="/static/alpine.min.js" defer></script>
<script src="/static/darra-plc.js"></script>

<!-- 可选: 按需引入 DevExtreme / Chart.js -->
<link rel="stylesheet" href="/static/devextreme/dx.common.css" />
<link rel="stylesheet" href="/static/devextreme/dx.light.css" />
<script src="/static/devextreme/dx.all.js"></script>
</head>
<body class="theme-modern">

<h1>1# 反应釜</h1>

<!-- Darra 自定义控件 -->
<darra-tank var="DB1.Level" max="100"></darra-tank>

<!-- Alpine 声明式响应 -->
<div x-data="{ showDetail: false }">
<button @click="showDetail = !showDetail">详情</button>
<div x-show="showDetail">
<darra-gauge var="DB1.Pressure" min="0" max="10"></darra-gauge>
</div>
</div>

</body>
</html>

CssCode

纯 CSS, 会被内联到 <head> 里:

.page {
padding: 24px;
background: var(--darra-bg);
color: var(--darra-text);
font-family: var(--darra-font);
}

.kpi-card {
background: var(--darra-panel-bg);
border: 1px solid var(--darra-border);
border-radius: var(--darra-radius);
padding: 16px;
box-shadow: var(--darra-shadow);
transition: transform .2s ease;
}
.kpi-card:hover { transform: translateY(-2px); }

@media (max-width: 768px) {
.page { padding: 12px; }
.kpi-grid { grid-template-columns: 1fr; }
}

JsCode

自由 JavaScript, 页面加载后执行。darra 已就绪, 可直接调 darra.bind / darra.write:

// ----- 全局初始化 -----
darra.init({ updateRate: 100, reconnect: true })

// ----- 连接生命周期 -----
darra.on('ready', () => {
console.log('[HMI] 已连接 PLC Service')
startDataPump()
})

darra.on('offline', () => {
console.warn('[HMI] 连接断开, 禁用写入')
document.querySelectorAll('.writable').forEach(el => el.setAttribute('disabled', ''))
})

darra.on('error', err => {
showToast('错误: ' + err.error, 'red')
})

// ----- 业务逻辑 -----
function startDataPump() {
darra.bindDB('DB1', (values) => {
updateKpi('temp', values.Temperature)
updateKpi('press', values.Pressure)
updateKpi('level', values.Level)
})

darra.bind('M0.0', (v) => {
document.getElementById('motor-state').textContent = v ? '运行' : '停止'
})
}

// ----- 带防抖的写入 -----
let writeTimer
function writeSetpointDebounced(val) {
clearTimeout(writeTimer)
writeTimer = setTimeout(() => darra.write('DB2.Setpoint', val), 300)
}

// ----- UI 帮助函数 -----
function updateKpi(id, value) {
const el = document.getElementById('kpi-' + id)
if (el) el.textContent = Number(value).toFixed(2)
}

function showToast(msg, color) {
const div = document.createElement('div')
div.className = 'toast'
div.style.background = color
div.textContent = msg
document.body.appendChild(div)
setTimeout(() => div.remove(), 3000)
}

Alpine.js 声明式 (与原生 JS 混用)

Alpine 用于简单响应, 不必写 JS:

<div x-data="{ count: 0, running: false }">

<button @click="count++; darra.write('MW100', count)">
写入 (<span x-text="count"></span>)
</button>

<button @click="running = !running; darra.write('M0.0', running)"
:class="running ? 'btn-green' : 'btn-gray'">
<span x-text="running ? '运行中' : '已停止'"></span>
</button>

<!-- 监听 PLC 变量 -->
<div x-data="{ temp: 0 }"
x-init="darra.bind('DB1.Temperature', v => temp = v)">
温度: <span x-text="temp.toFixed(2)"></span>
<span x-show="temp > 80" style="color:red">⚠ 超温</span>
</div>

</div>
Alpine 指令说明
x-data="{...}"定义响应式状态对象
x-init="..."初始化时执行
x-text="..."文本内容
x-bind:attr="...":attr属性绑定
x-on:event="..."@event事件监听
x-show="..."显隐 (display: none)
x-if="..."条件渲染 (DOM 增删)
x-for="item in list"循环
x-model="..."双向绑定 input
x-cloak防初始闪烁

Chart.js 集成

<canvas id="chart-trend" width="600" height="300"></canvas>

<script src="/static/chart.umd.js"></script>
<script>
const ctx = document.getElementById('chart-trend').getContext('2d')
const trend = new Chart(ctx, {
type: 'line',
data: { labels: [], datasets: [{ label: 'T1', data: [], borderColor: '#4F7CFF' }] },
options: { animation: false, responsive: true }
})

darra.bind('DB1.T1', (v) => {
const now = new Date().toLocaleTimeString()
trend.data.labels.push(now)
trend.data.datasets[0].data.push(v)
if (trend.data.labels.length > 200) {
trend.data.labels.shift()
trend.data.datasets[0].data.shift()
}
trend.update('none')
})
</script>

DevExtreme 集成 (非 <darra-dx-*> 的自定义初始化)

<div id="dx-grid"></div>

<script>
let dataGrid
darra.on('ready', async () => {
// 读当前配方列表 (PLC 数据库的配方表, 见 communication/database-recipe)
const recipes = await callPython('query_recipes', {}) // HmiScriptScheduler 暴露的 Python
dataGrid = new DevExpress.ui.dxDataGrid(document.getElementById('dx-grid'), {
dataSource: recipes,
columns: [
{ dataField: 'code', caption: '配方编号' },
{ dataField: 'name', caption: '名称' },
{ dataField: 'target', caption: '目标值' },
{ dataField: 'state', caption: '状态',
cellTemplate: (cell, { data }) => {
cell.style.color = data.state === 'active' ? 'green' : '#888'
cell.textContent = data.state
}
}
],
paging: { pageSize: 20 },
filterRow: { visible: true },
// 只读展示; 要切换配方点某行后调 darra.write('DB_Recipe.Active', code)
editing: { mode: 'row', allowUpdating: false }
})

dataGrid.option('onRowClick', (e) => {
if (confirm(`切换到配方 ${e.data.code}?`)) {
darra.write('DB_Recipe.Active', e.data.code)
}
})
})
</script>

按需引入库

默认框架 (Alpine + darra-plc + darra-controls + darra-hmi.css) 已自动注入。额外库必须在 HtmlCode <head> 里显式引入:

<!-- 仅需 Chart.js -->
<script src="/static/chart.umd.js"></script>

<!-- 仅需 DevExtreme (含 CSS) -->
<link rel="stylesheet" href="/static/devextreme/dx.common.css" />
<link rel="stylesheet" href="/static/devextreme/dx.light.css" />
<script src="/static/devextreme/dx.all.js"></script>
<script src="/static/darra-devextreme.js"></script> <!-- darra-dx-* 封装 -->

<!-- 自定义第三方 (比如 flatpickr 日期选择) -->
<link rel="stylesheet" href="/static/vendor/flatpickr.min.css" />
<script src="/static/vendor/flatpickr.min.js"></script>

未放到 /static/vendor/ 的第三方库请先部署到 Darra_PLC_Service/HMI/Static/vendor/, 避免外网 CDN 依赖。

与拖拽模式互转

Code 模式保存后, 再切回 FreeForm 时:

  1. IDE 尝试解析 HtmlCode 为 GrapesJS 模型
  2. 能识别的 <darra-*> 标签还原为 Block
  3. 无法解析的结构 (比如自定义 JS 插入的 DOM) 保留为原始 HTML 节点
  4. Elements[] 被清零 (因为无法反推坐标), 下次再保存时重新生成

建议: 一旦进入 Code 模式做复杂逻辑, 就不要回 FreeForm, 两种模式不要频繁切换

最佳实践

  • 骨架用拖拽, 逻辑用 Code: 先拖完基础布局, 再切 Code 补动画/数据处理
  • JsCode 模块化: 用 IIFE 包裹避免全局污染
    (function() { 'use strict'
    // 页面逻辑
    })()
  • 异常捕获: 所有 async 函数外包 try/catch, 用 console.error + darra._reportError 回传
  • 禁止轮询 PLC: 严禁 setInterval(() => fetch('/api/plc/read'), 100), 订阅 darra.bind 才是正道
  • 性能: Chart.js / DevExtreme 大数据量时开 animation: false
  • 资源释放: 页面切换时, Alpine x-data 自动销毁; 手动注册的 setInterval 要在 beforeunload 里 clear

排错

问题原因解决
darra is not defineddarra-plc.js 未加载检查 <script src="/static/darra-plc.js"> 路径
Alpine is not definedAlpine 未加载 或 在 Alpine init 前使用document.addEventListener('alpine:init', ...)
DevExtreme 样式错乱CSS 未引入dx.common.css + 主题 css
console.error 显示 writeGroup failed白名单未通过HMIProjectDef.WriteWhitelist

相关文档