mirror of
http://10.0.2.1:3031/sauer/bfa-dryer-design.git
synced 2026-06-30 12:56:42 +10:00
V2: GrapesJS-based HMI design editor
Complete rewrite of the editor frontend using GrapesJS: - Full drag/drop visual editor with style manager - Custom HMI blocks: Temperature, Motor, Output, Burner, Automation, Gauge - Layout blocks: Page Container, Tab Bar, Top Bar, Label, Divider, Spacer - Style panel: font family/size/weight/color, background, border, layout, effects - Traits panel: active/inactive colors, animation, linked control, setpoints - Device manager: Tab5 (1280x720), Schneider HMIDT651 (1280x800), Desktop - Theme switcher: Dark Industrial, Light Industrial, High Contrast, Classic SCADA - Layers panel with z-index reorder and show/hide - Undo/redo, preview mode, canvas zoom - V1 layout preserved (saved as current.json), V2 uses current_v2.json - Login/users/comments backend unchanged Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6374749397
commit
1831759563
23
app.py
23
app.py
@ -14,16 +14,29 @@ os.makedirs(PHOTO_DIR, exist_ok=True)
|
||||
CURRENT_LAYOUT = os.path.join(LAYOUT_DIR, "current.json")
|
||||
|
||||
def get_layout():
|
||||
# V2: GrapesJS project JSON stored in current_v2.json
|
||||
v2_path = os.path.join(LAYOUT_DIR, "current_v2.json")
|
||||
if os.path.exists(v2_path):
|
||||
with open(v2_path) as f:
|
||||
return json.load(f)
|
||||
# No V2 layout yet — return empty so GrapesJS starts fresh
|
||||
return {}
|
||||
|
||||
def get_v1_layout():
|
||||
"""Legacy V1 layout for reference"""
|
||||
if os.path.exists(CURRENT_LAYOUT):
|
||||
with open(CURRENT_LAYOUT) as f:
|
||||
return json.load(f)
|
||||
default = os.path.join(LAYOUT_DIR, "default.json")
|
||||
with open(default) as f:
|
||||
layout = json.load(f)
|
||||
save_layout(layout)
|
||||
return layout
|
||||
return {}
|
||||
|
||||
def save_layout(layout):
|
||||
# V2: save GrapesJS project data
|
||||
v2_path = os.path.join(LAYOUT_DIR, "current_v2.json")
|
||||
with open(v2_path, "w") as f:
|
||||
json.dump(layout, f, indent=2)
|
||||
|
||||
def save_v1_layout(layout):
|
||||
"""Legacy V1 save"""
|
||||
with open(CURRENT_LAYOUT, "w") as f:
|
||||
json.dump(layout, f, indent=2)
|
||||
|
||||
|
||||
277
static/hmi-blocks.js
Normal file
277
static/hmi-blocks.js
Normal file
@ -0,0 +1,277 @@
|
||||
/* BFA Banana Dryer — Custom HMI Blocks for GrapesJS */
|
||||
|
||||
function registerHMIBlocks(editor) {
|
||||
const bm = editor.BlockManager;
|
||||
const dm = editor.DomComponents;
|
||||
|
||||
// ── Shared styles ──
|
||||
const cardBase = {
|
||||
'display': 'flex', 'flex-direction': 'column', 'align-items': 'center', 'justify-content': 'center',
|
||||
'gap': '6px', 'padding': '16px', 'border-radius': '10px', 'cursor': 'pointer',
|
||||
'min-height': '120px', 'width': '48%', 'font-family': 'Segoe UI, system-ui, sans-serif',
|
||||
'border': '1px solid #1e2a45', 'transition': 'background 0.15s', 'box-sizing': 'border-box'
|
||||
};
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// TEMPERATURE CARD
|
||||
// ══════════════════════════════════════════
|
||||
dm.addType('temp-card', {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'div',
|
||||
draggable: true,
|
||||
droppable: false,
|
||||
attributes: {'data-type': 'temp'},
|
||||
traits: [
|
||||
{type: 'text', name: 'label', label: 'Name', changeProp: true},
|
||||
{type: 'color', name: 'temp_color', label: 'Temp Color', changeProp: true},
|
||||
{type: 'number', name: 'setpoint', label: 'Setpoint (°C)', changeProp: true, min: 0, max: 200},
|
||||
{type: 'text', name: 'linked_id', label: 'Linked Control ID', changeProp: true},
|
||||
],
|
||||
label: 'Heat Input',
|
||||
temp_color: '#ff4444',
|
||||
setpoint: 130,
|
||||
linked_id: '',
|
||||
components: [
|
||||
{tagName: 'div', attributes: {'data-role': 'name'}, content: 'Heat Input',
|
||||
style: {'font-size': '20px', 'color': '#7a8baa'}},
|
||||
{tagName: 'div', attributes: {'data-role': 'value'}, content: '-- °C',
|
||||
style: {'font-size': '36px', 'font-weight': '700', 'color': '#ff4444'}},
|
||||
{tagName: 'div', attributes: {'data-role': 'bar'},
|
||||
style: {'width': '80%', 'height': '6px', 'background': '#1a2240', 'border-radius': '3px', 'overflow': 'hidden'},
|
||||
components: [{tagName: 'div', style: {'width': '50%', 'height': '100%', 'background': '#ff4444', 'border-radius': '3px'}}]},
|
||||
{tagName: 'div', attributes: {'data-role': 'sp'}, content: 'SP: 130 °C',
|
||||
style: {'font-size': '14px', 'color': '#4a5670'}},
|
||||
],
|
||||
style: {...cardBase, 'background': '#131a2b'},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bm.add('temp-card', {
|
||||
label: '🌡 Temperature',
|
||||
category: 'HMI Controls',
|
||||
content: {type: 'temp-card'},
|
||||
attributes: {class: 'gjs-fonts gjs-f-b1'}
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// MOTOR CARD
|
||||
// ══════════════════════════════════════════
|
||||
dm.addType('motor-card', {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'div',
|
||||
draggable: true,
|
||||
droppable: false,
|
||||
attributes: {'data-type': 'motor'},
|
||||
traits: [
|
||||
{type: 'text', name: 'label', label: 'Name', changeProp: true},
|
||||
{type: 'number', name: 'speed_sp', label: 'Speed SP (%)', changeProp: true, min: 0, max: 100, step: 5},
|
||||
{type: 'color', name: 'active_color', label: 'Active Color', changeProp: true},
|
||||
{type: 'color', name: 'inactive_color', label: 'Inactive Color', changeProp: true},
|
||||
{type: 'select', name: 'animation', label: 'Animation', changeProp: true,
|
||||
options: [{value:'none'},{value:'fan'},{value:'agitator'},{value:'conveyor'},{value:'pulse'},{value:'vibrate'},{value:'glow'},{value:'blink'},{value:'flow'}]},
|
||||
{type: 'text', name: 'linked_id', label: 'Linked Control ID', changeProp: true},
|
||||
],
|
||||
label: 'Motor',
|
||||
speed_sp: 50,
|
||||
active_color: '#00c853',
|
||||
inactive_color: '#ff1744',
|
||||
animation: 'none',
|
||||
linked_id: '',
|
||||
components: [
|
||||
{tagName: 'div', style: {'display': 'flex', 'gap': '10px', 'align-items': 'center'},
|
||||
components: [
|
||||
{tagName: 'span', attributes: {'data-role': 'name'}, content: 'Motor',
|
||||
style: {'font-size': '24px', 'font-weight': '600', 'color': '#e8ecf4'}},
|
||||
{tagName: 'span', attributes: {'data-role': 'speed'}, content: '50%',
|
||||
style: {'font-size': '24px', 'color': '#7a8baa'}},
|
||||
]},
|
||||
{tagName: 'div', attributes: {'data-role': 'state'}, content: 'OFF',
|
||||
style: {'font-size': '36px', 'font-weight': '700', 'color': '#e8ecf4'}},
|
||||
],
|
||||
style: {...cardBase, 'background': '#ff1744'},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bm.add('motor-card', {
|
||||
label: '⚙ Motor / Speed',
|
||||
category: 'HMI Controls',
|
||||
content: {type: 'motor-card'},
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// OUTPUT CARD
|
||||
// ══════════════════════════════════════════
|
||||
dm.addType('output-card', {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'div',
|
||||
draggable: true,
|
||||
droppable: false,
|
||||
attributes: {'data-type': 'output'},
|
||||
traits: [
|
||||
{type: 'text', name: 'label', label: 'Name', changeProp: true},
|
||||
{type: 'color', name: 'active_color', label: 'Active Color', changeProp: true},
|
||||
{type: 'color', name: 'inactive_color', label: 'Inactive Color', changeProp: true},
|
||||
{type: 'text', name: 'linked_id', label: 'Linked Control ID', changeProp: true},
|
||||
],
|
||||
label: 'Output',
|
||||
active_color: '#00c853',
|
||||
inactive_color: '#ff1744',
|
||||
linked_id: '',
|
||||
components: [
|
||||
{tagName: 'div', attributes: {'data-role': 'name'}, content: 'Output',
|
||||
style: {'font-size': '20px', 'text-align': 'center', 'color': '#e8ecf4'}},
|
||||
{tagName: 'div', attributes: {'data-role': 'state'}, content: 'OFF',
|
||||
style: {'font-size': '28px', 'font-weight': '700', 'color': '#e8ecf4'}},
|
||||
],
|
||||
style: {...cardBase, 'background': '#ff1744'},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bm.add('output-card', {
|
||||
label: '⚡ Output / Switch',
|
||||
category: 'HMI Controls',
|
||||
content: {type: 'output-card'},
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// BURNER CARD
|
||||
// ══════════════════════════════════════════
|
||||
dm.addType('burner-card', {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'div',
|
||||
draggable: true,
|
||||
droppable: false,
|
||||
attributes: {'data-type': 'burner'},
|
||||
traits: [
|
||||
{type: 'text', name: 'label', label: 'Name', changeProp: true},
|
||||
{type: 'color', name: 'active_color', label: 'Active Color', changeProp: true},
|
||||
{type: 'color', name: 'inactive_color', label: 'Inactive Color', changeProp: true},
|
||||
],
|
||||
label: 'Burner',
|
||||
active_color: '#ffab00',
|
||||
inactive_color: '#ff1744',
|
||||
components: [
|
||||
{tagName: 'div', attributes: {'data-role': 'name'}, content: 'Burner',
|
||||
style: {'font-size': '24px', 'color': '#e8ecf4'}},
|
||||
{tagName: 'div', attributes: {'data-role': 'state'}, content: 'OFF',
|
||||
style: {'font-size': '32px', 'font-weight': '700', 'color': '#e8ecf4'}},
|
||||
],
|
||||
style: {...cardBase, 'background': '#ff1744'},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bm.add('burner-card', {
|
||||
label: '🔥 Burner',
|
||||
category: 'HMI Controls',
|
||||
content: {type: 'burner-card'},
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// AUTOMATION CARD
|
||||
// ══════════════════════════════════════════
|
||||
dm.addType('automation-card', {
|
||||
model: {
|
||||
defaults: {
|
||||
tagName: 'div',
|
||||
draggable: true,
|
||||
droppable: false,
|
||||
attributes: {'data-type': 'automation'},
|
||||
traits: [
|
||||
{type: 'text', name: 'label', label: 'Name', changeProp: true},
|
||||
{type: 'color', name: 'active_color', label: 'Active Color', changeProp: true},
|
||||
{type: 'color', name: 'inactive_color', label: 'Inactive Color', changeProp: true},
|
||||
],
|
||||
label: 'Automation',
|
||||
active_color: '#00c853',
|
||||
inactive_color: '#ff1744',
|
||||
components: [
|
||||
{tagName: 'div', attributes: {'data-role': 'name'}, content: 'Automation',
|
||||
style: {'font-size': '24px', 'font-weight': '700', 'color': '#e8ecf4'}},
|
||||
{tagName: 'div', attributes: {'data-role': 'rules'}, content: 'Click to configure',
|
||||
style: {'font-size': '12px', 'color': '#7a8baa'}},
|
||||
],
|
||||
style: {...cardBase, 'background': '#ff1744', 'width': '100%'},
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
bm.add('automation-card', {
|
||||
label: '🤖 Automation',
|
||||
category: 'HMI Controls',
|
||||
content: {type: 'automation-card'},
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// GAUGE (arc)
|
||||
// ══════════════════════════════════════════
|
||||
bm.add('gauge', {
|
||||
label: '📊 Gauge',
|
||||
category: 'HMI Controls',
|
||||
content: `<div style="width:150px;height:150px;display:flex;align-items:center;justify-content:center;position:relative" data-type="gauge">
|
||||
<svg viewBox="0 0 100 100" width="120" height="120">
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="#1a2240" stroke-width="6"/>
|
||||
<circle cx="50" cy="50" r="42" fill="none" stroke="#2d7ff9" stroke-width="6"
|
||||
stroke-dasharray="264" stroke-dashoffset="132" transform="rotate(-90 50 50)" stroke-linecap="round"/>
|
||||
<text x="50" y="50" text-anchor="middle" dominant-baseline="central" fill="#e8ecf4" font-size="18" font-weight="700">50%</text>
|
||||
</svg>
|
||||
</div>`,
|
||||
});
|
||||
|
||||
// ══════════════════════════════════════════
|
||||
// LAYOUT HELPERS
|
||||
// ══════════════════════════════════════════
|
||||
bm.add('page-container', {
|
||||
label: '📄 Page Container',
|
||||
category: 'Layout',
|
||||
content: `<div style="display:flex;flex-wrap:wrap;gap:8px;padding:8px;width:100%;height:100%;align-content:flex-start;background:#0a0e17" data-type="page">
|
||||
</div>`,
|
||||
});
|
||||
|
||||
bm.add('tab-bar', {
|
||||
label: '📑 Tab Bar',
|
||||
category: 'Layout',
|
||||
content: `<div style="display:flex;height:50px;background:#0d1220;border-bottom:1px solid #1e2a45;width:100%">
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:600;color:#2d7ff9;border-bottom:3px solid #2d7ff9;cursor:pointer">PAGE 1</div>
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:600;color:#7a8baa;cursor:pointer">PAGE 2</div>
|
||||
<div style="flex:1;display:flex;align-items:center;justify-content:center;font-size:16px;font-weight:600;color:#7a8baa;cursor:pointer">PAGE 3</div>
|
||||
</div>`,
|
||||
});
|
||||
|
||||
bm.add('top-bar', {
|
||||
label: '📌 Top Bar',
|
||||
category: 'Layout',
|
||||
content: `<div style="display:flex;align-items:center;justify-content:space-between;height:48px;padding:0 20px;background:#111827;border-bottom:1px solid #1e2a45;width:100%">
|
||||
<div style="display:flex;align-items:center;gap:16px">
|
||||
<span style="font-size:20px;font-weight:700;color:#e8ecf4">BFA BANANA DRYER</span>
|
||||
<span style="font-size:14px;color:#7a8baa">SAE Engineering</span>
|
||||
</div>
|
||||
<span style="font-size:16px;font-weight:600;color:#ff1744">RS485 OFFLINE</span>
|
||||
</div>`,
|
||||
});
|
||||
|
||||
bm.add('text-label', {
|
||||
label: '📝 Text Label',
|
||||
category: 'Layout',
|
||||
content: `<div style="font-size:18px;color:#e8ecf4;padding:8px;text-align:center">Label Text</div>`,
|
||||
});
|
||||
|
||||
bm.add('divider', {
|
||||
label: '➖ Divider',
|
||||
category: 'Layout',
|
||||
content: `<div style="width:100%;height:1px;background:#1e2a45;margin:8px 0"></div>`,
|
||||
});
|
||||
|
||||
bm.add('spacer', {
|
||||
label: '⬜ Spacer',
|
||||
category: 'Layout',
|
||||
content: `<div style="width:100%;height:20px"></div>`,
|
||||
});
|
||||
}
|
||||
8
static/hmi-canvas.css
Normal file
8
static/hmi-canvas.css
Normal file
@ -0,0 +1,8 @@
|
||||
/* Canvas styles — injected into GrapesJS iframe */
|
||||
:root {
|
||||
--bg: #0a0e17; --card: #131a2b; --border: #1e2a45;
|
||||
--text: #e8ecf4; --text2: #7a8baa; --dim: #4a5670;
|
||||
--blue: #2d7ff9; --green: #00c853; --amber: #ffab00; --red: #ff1744;
|
||||
}
|
||||
body { margin: 0; background: var(--bg); font-family: 'Segoe UI', system-ui, sans-serif; color: var(--text); }
|
||||
* { box-sizing: border-box; }
|
||||
41
static/hmi-themes.js
Normal file
41
static/hmi-themes.js
Normal file
@ -0,0 +1,41 @@
|
||||
/* BFA Banana Dryer — HMI Theme Presets */
|
||||
|
||||
const HMI_THEMES = {
|
||||
'dark-industrial': {
|
||||
'--bg': '#0a0e17', '--card': '#131a2b', '--border': '#1e2a45',
|
||||
'--text': '#e8ecf4', '--text2': '#7a8baa', '--dim': '#4a5670',
|
||||
'--blue': '#2d7ff9', '--green': '#00c853', '--amber': '#ffab00', '--red': '#ff1744',
|
||||
},
|
||||
'light-industrial': {
|
||||
'--bg': '#f0f2f5', '--card': '#ffffff', '--border': '#d0d5dd',
|
||||
'--text': '#1a1a2e', '--text2': '#667085', '--dim': '#98a2b3',
|
||||
'--blue': '#2563eb', '--green': '#16a34a', '--amber': '#d97706', '--red': '#dc2626',
|
||||
},
|
||||
'high-contrast': {
|
||||
'--bg': '#000000', '--card': '#1a1a1a', '--border': '#444444',
|
||||
'--text': '#ffffff', '--text2': '#cccccc', '--dim': '#888888',
|
||||
'--blue': '#00aaff', '--green': '#00ff55', '--amber': '#ffcc00', '--red': '#ff3333',
|
||||
},
|
||||
'classic-scada': {
|
||||
'--bg': '#c0c0c0', '--card': '#d4d4d4', '--border': '#808080',
|
||||
'--text': '#000000', '--text2': '#333333', '--dim': '#666666',
|
||||
'--blue': '#0000cc', '--green': '#008800', '--amber': '#cc8800', '--red': '#cc0000',
|
||||
}
|
||||
};
|
||||
|
||||
function applyHMITheme(editor, themeName) {
|
||||
const theme = HMI_THEMES[themeName];
|
||||
if (!theme) return;
|
||||
|
||||
// Apply to canvas iframe
|
||||
const frame = editor.Canvas.getFrameEl();
|
||||
if (frame && frame.contentDocument) {
|
||||
const doc = frame.contentDocument.documentElement;
|
||||
Object.entries(theme).forEach(([k, v]) => doc.style.setProperty(k, v));
|
||||
doc.style.background = theme['--bg'];
|
||||
}
|
||||
|
||||
// Apply to editor UI
|
||||
const root = document.documentElement;
|
||||
Object.entries(theme).forEach(([k, v]) => root.style.setProperty(k, v));
|
||||
}
|
||||
1
static/vendor/grapes.min.css
vendored
Normal file
1
static/vendor/grapes.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
3
static/vendor/grapes.min.js
vendored
Normal file
3
static/vendor/grapes.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user