代码式制作
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 时:
- IDE 尝试解析 HtmlCode 为 GrapesJS 模型
- 能识别的
<darra-*>标签还原为 Block - 无法解析的结构 (比如自定义 JS 插入的 DOM) 保留为原始 HTML 节点
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 defined | darra-plc.js 未加载 | 检查 <script src="/static/darra-plc.js"> 路径 |
Alpine is not defined | Alpine 未加载 或 在 Alpine init 前使用 | 用 document.addEventListener('alpine:init', ...) |
| DevExtreme 样式错乱 | CSS 未引入 | 加 dx.common.css + 主题 css |
console.error 显示 writeGroup failed | 白名单未通过 | 看 HMIProjectDef.WriteWhitelist |