mirror of
http://10.0.2.1:3031/sauer/bfa-dryer-design.git
synced 2026-06-30 11:26:42 +10:00
334 lines
16 KiB
HTML
334 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>BFA Banana Dryer — HMI Design Tool</title>
|
||
<link rel="stylesheet" href="static/vendor/grapes.min.css">
|
||
<style>
|
||
:root{--bg:#0a0e17;--card:#131a2b;--border:#1e2a45;--text:#e8ecf4;--text2:#7a8baa;--dim:#4a5670;--blue:#2d7ff9;--green:#00c853;--amber:#ffab00;--red:#ff1744}
|
||
|
||
body{margin:0;font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(--text);height:100vh;overflow:hidden}
|
||
|
||
/* GrapesJS dark theme overrides */
|
||
.gjs-one-bg{background:var(--bg) !important}
|
||
.gjs-two-color{color:var(--text) !important}
|
||
.gjs-three-bg{background:var(--card) !important}
|
||
.gjs-four-color,.gjs-four-color-h:hover{color:var(--blue) !important}
|
||
.gjs-pn-panel{background:var(--card) !important;border-color:var(--border) !important}
|
||
.gjs-pn-views-container,.gjs-pn-views{background:var(--card) !important}
|
||
.gjs-block{background:var(--bg) !important;border:1px solid var(--border) !important;color:var(--text2) !important;border-radius:8px !important;min-height:60px !important}
|
||
.gjs-block:hover{border-color:var(--blue) !important;color:var(--text) !important}
|
||
.gjs-block__media{color:var(--text2) !important}
|
||
.gjs-blocks-cs{background:var(--card) !important}
|
||
.gjs-category-title,.gjs-layer-title,.gjs-sm-sector-title{background:var(--bg) !important;color:var(--text2) !important;border-color:var(--border) !important}
|
||
.gjs-field{background:var(--bg) !important;color:var(--text) !important;border-color:var(--border) !important}
|
||
.gjs-field input,.gjs-field select,.gjs-field textarea{color:var(--text) !important}
|
||
.gjs-sm-property,.gjs-trt-trait{color:var(--text2) !important}
|
||
.gjs-clm-tags .gjs-sm-composite{background:var(--bg) !important}
|
||
.gjs-layer{background:var(--card) !important;color:var(--text2) !important}
|
||
.gjs-layer.gjs-selected{background:var(--border) !important}
|
||
.gjs-input-holder,.gjs-sm-input-holder{background:var(--bg) !important}
|
||
.gjs-pn-btn{color:var(--text2) !important}
|
||
.gjs-pn-btn.gjs-pn-active{color:var(--blue) !important}
|
||
.gjs-cv-canvas{background:#080c14 !important}
|
||
.gjs-frame-wrapper{background:var(--bg) !important}
|
||
.gjs-toolbar{background:var(--card) !important}
|
||
.gjs-toolbar-item{color:var(--text) !important}
|
||
.gjs-resizer-h{border-color:var(--blue) !important}
|
||
.gjs-highlighter{outline-color:var(--blue) !important}
|
||
.gjs-badge{background:var(--blue) !important}
|
||
.gjs-ghost{background:rgba(45,127,249,0.15) !important;border:2px dashed var(--blue) !important}
|
||
|
||
/* Top bar */
|
||
.top-bar{display:flex;align-items:center;justify-content:space-between;height:48px;padding:0 16px;background:#111827;border-bottom:1px solid var(--border);flex-shrink:0;z-index:10}
|
||
.top-bar .title{font-size:18px;font-weight:700}
|
||
.top-bar .sub{font-size:13px;color:var(--text2);margin-left:16px}
|
||
.top-bar-right{display:flex;align-items:center;gap:12px}
|
||
.top-bar select,.top-bar button{padding:5px 12px;border:1px solid var(--border);border-radius:5px;background:var(--card);color:var(--text);font-size:12px;cursor:pointer}
|
||
.top-bar button:hover{background:var(--border)}
|
||
.top-bar .save-btn{background:var(--green);border-color:var(--green);color:#000;font-weight:600}
|
||
|
||
/* Editor container */
|
||
#gjs{height:calc(100vh - 48px);width:100%}
|
||
|
||
/* Right panel width */
|
||
.gjs-pn-views-container{width:280px !important}
|
||
|
||
/* Main layout — editor + optional sim panel side by side */
|
||
.main-wrap{display:flex;height:calc(100vh - 48px)}
|
||
.editor-wrap{flex:1;overflow:hidden}
|
||
.editor-wrap #gjs{height:100% !important}
|
||
|
||
/* Simulation toggle panel */
|
||
.sim-panel{width:0;overflow:hidden;background:#080c14;border-left:2px solid var(--border);transition:width 0.2s;flex-shrink:0}
|
||
.sim-panel.open{width:45%}
|
||
.sim-panel-header{padding:8px 12px;background:var(--card);border-bottom:1px solid var(--border);display:flex;justify-content:space-between;align-items:center;font-size:13px;font-weight:600;color:var(--text2)}
|
||
.sim-panel-header button{padding:4px 10px;border:1px solid var(--border);border-radius:4px;background:var(--card);color:var(--text2);font-size:11px;cursor:pointer}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- TOP BAR -->
|
||
<div class="top-bar">
|
||
<div style="display:flex;align-items:center">
|
||
<span class="title">BFA Banana Dryer</span>
|
||
<span class="sub">HMI Design Tool v2</span>
|
||
</div>
|
||
<div class="top-bar-right">
|
||
<span style="color:var(--blue);font-weight:600">{{ user }}</span>
|
||
<a href="logout" style="color:var(--dim);text-decoration:none;font-size:12px">Logout</a>
|
||
<span style="color:var(--dim)">|</span>
|
||
<select id="theme-select" onchange="switchTheme(this.value)">
|
||
<option value="dark-industrial">Dark Industrial</option>
|
||
<option value="light-industrial">Light Industrial</option>
|
||
<option value="high-contrast">High Contrast</option>
|
||
<option value="classic-scada">Classic SCADA</option>
|
||
</select>
|
||
<button onclick="toggleSim()">Simulation</button>
|
||
<button class="save-btn" onclick="saveProject()">Save</button>
|
||
<button onclick="exportProject()">Export</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- MAIN LAYOUT -->
|
||
<div class="main-wrap">
|
||
<div class="editor-wrap">
|
||
<div id="gjs"></div>
|
||
</div>
|
||
<div class="sim-panel" id="sim-panel">
|
||
<div class="sim-panel-header">
|
||
MACHINE SIMULATION
|
||
<button onclick="toggleSim()">Close</button>
|
||
</div>
|
||
<div id="sim-container" style="padding:16px;display:flex;align-items:center;justify-content:center;min-height:400px"></div>
|
||
</div>
|
||
</div><!-- /main-wrap -->
|
||
|
||
<script src="static/vendor/grapes.min.js"></script>
|
||
<script src="static/hmi-blocks.js"></script>
|
||
<script src="static/hmi-themes.js"></script>
|
||
<script>
|
||
// ══════════════════════════════════════════════════════
|
||
// GrapesJS Init
|
||
// ══════════════════════════════════════════════════════
|
||
const editor = grapesjs.init({
|
||
container: '#gjs',
|
||
fromElement: false,
|
||
height: '100%',
|
||
width: 'auto',
|
||
storageManager: {
|
||
type: 'remote',
|
||
stepsBeforeSave: 3,
|
||
options: {
|
||
remote: {
|
||
urlLoad: 'api/layout',
|
||
urlStore: 'api/layout',
|
||
fetchOptions: opts => ({...opts, method: opts.method || 'POST', headers: {'Content-Type':'application/json'}}),
|
||
onStore: data => data,
|
||
onLoad: result => result,
|
||
}
|
||
}
|
||
},
|
||
deviceManager: {
|
||
devices: [
|
||
{id: 'tab5', name: 'Tab5 (1280×720)', width: '1280px', height: '720px'},
|
||
{id: 'schneider', name: 'Schneider HMIDT651 (1280×800)', width: '1280px', height: '800px'},
|
||
{id: 'desktop', name: 'Desktop (Full)', width: ''},
|
||
]
|
||
},
|
||
styleManager: {
|
||
sectors: [
|
||
{
|
||
name: 'Typography',
|
||
open: true,
|
||
properties: [
|
||
{property: 'font-family', type: 'select', defaults: 'Segoe UI',
|
||
list: [{value:'Segoe UI',name:'Segoe UI'},{value:'Arial',name:'Arial'},{value:'Helvetica',name:'Helvetica'},{value:'Roboto',name:'Roboto'},{value:'monospace',name:'Monospace'},{value:'Courier New',name:'Courier New'}]},
|
||
{property: 'font-size', type: 'number', units: ['px','em','rem'], defaults: '18px', min: 8, max: 72},
|
||
{property: 'font-weight', type: 'select', defaults: '400',
|
||
list: [{value:'400',name:'Normal'},{value:'600',name:'Semi Bold'},{value:'700',name:'Bold'},{value:'900',name:'Black'}]},
|
||
{property: 'color', type: 'color'},
|
||
{property: 'text-align', type: 'select', defaults: 'center',
|
||
list: [{value:'left',name:'Left'},{value:'center',name:'Center'},{value:'right',name:'Right'}]},
|
||
{property: 'line-height', type: 'number', units: ['px',''], defaults: '1.4'},
|
||
]
|
||
},
|
||
{
|
||
name: 'Background & Colors',
|
||
open: true,
|
||
properties: [
|
||
{property: 'background-color', type: 'color'},
|
||
{property: 'border-color', type: 'color'},
|
||
{property: 'border-width', type: 'number', units: ['px'], defaults: '1px', min: 0, max: 10},
|
||
{property: 'border-radius', type: 'number', units: ['px'], defaults: '10px', min: 0, max: 50},
|
||
{property: 'border-style', type: 'select', defaults: 'solid',
|
||
list: [{value:'none'},{value:'solid'},{value:'dashed'},{value:'dotted'}]},
|
||
{property: 'opacity', type: 'number', defaults: 1, min: 0, max: 1, step: 0.1},
|
||
]
|
||
},
|
||
{
|
||
name: 'Layout',
|
||
open: false,
|
||
properties: [
|
||
{property: 'width', type: 'number', units: ['px','%','vw']},
|
||
{property: 'height', type: 'number', units: ['px','%','vh']},
|
||
{property: 'min-height', type: 'number', units: ['px']},
|
||
{property: 'padding', type: 'composite',
|
||
properties: [{property:'padding-top',type:'number',units:['px']},{property:'padding-right',type:'number',units:['px']},{property:'padding-bottom',type:'number',units:['px']},{property:'padding-left',type:'number',units:['px']}]},
|
||
{property: 'margin', type: 'composite',
|
||
properties: [{property:'margin-top',type:'number',units:['px']},{property:'margin-right',type:'number',units:['px']},{property:'margin-bottom',type:'number',units:['px']},{property:'margin-left',type:'number',units:['px']}]},
|
||
{property: 'display', type: 'select', defaults: 'flex',
|
||
list: [{value:'block'},{value:'flex'},{value:'grid'},{value:'inline-block'},{value:'none'}]},
|
||
{property: 'flex-direction', type: 'select',
|
||
list: [{value:'row'},{value:'column'},{value:'row-reverse'},{value:'column-reverse'}]},
|
||
{property: 'justify-content', type: 'select',
|
||
list: [{value:'flex-start'},{value:'center'},{value:'flex-end'},{value:'space-between'},{value:'space-around'}]},
|
||
{property: 'align-items', type: 'select',
|
||
list: [{value:'flex-start'},{value:'center'},{value:'flex-end'},{value:'stretch'}]},
|
||
{property: 'gap', type: 'number', units: ['px'], defaults: '8px'},
|
||
{property: 'z-index', type: 'number', defaults: 'auto', min: -10, max: 100},
|
||
{property: 'overflow', type: 'select', list: [{value:'visible'},{value:'hidden'},{value:'auto'},{value:'scroll'}]},
|
||
]
|
||
},
|
||
{
|
||
name: 'Effects',
|
||
open: false,
|
||
properties: [
|
||
{property: 'box-shadow', type: 'text'},
|
||
{property: 'transition', type: 'text'},
|
||
{property: 'cursor', type: 'select', list: [{value:'default'},{value:'pointer'},{value:'grab'},{value:'not-allowed'}]},
|
||
]
|
||
}
|
||
]
|
||
},
|
||
blockManager: {blocks: []},
|
||
canvas: {
|
||
styles: ['static/hmi-canvas.css'],
|
||
}
|
||
});
|
||
|
||
// Use GrapesJS default panels — they include blocks, styles, layers, traits
|
||
// Just add device switching buttons
|
||
editor.Panels.addButton('options', {
|
||
id: 'device-tab5', command: e => e.setDevice('tab5'), label: 'Tab5', attributes: {title: 'Tab5 1280x720'}
|
||
});
|
||
editor.Panels.addButton('options', {
|
||
id: 'device-schneider', command: e => e.setDevice('schneider'), label: 'HMIDT', attributes: {title: 'Schneider 1280x800'}
|
||
});
|
||
editor.Panels.addButton('options', {
|
||
id: 'device-full', command: e => e.setDevice('desktop'), label: 'Full', attributes: {title: 'Full width'}
|
||
});
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// Register HMI blocks
|
||
// ══════════════════════════════════════════════════════
|
||
registerHMIBlocks(editor);
|
||
|
||
// Open blocks panel by default
|
||
editor.on('load', () => {
|
||
const pn = editor.Panels;
|
||
const openBl = pn.getButton('views', 'open-blocks');
|
||
if (openBl) openBl.set('active', true);
|
||
});
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// Save / Load / Export
|
||
// ══════════════════════════════════════════════════════
|
||
async function saveProject() {
|
||
try {
|
||
const data = editor.getProjectData();
|
||
await fetch('api/layout', {
|
||
method: 'POST',
|
||
headers: {'Content-Type': 'application/json'},
|
||
body: JSON.stringify(data)
|
||
});
|
||
alert('Saved!');
|
||
} catch(e) { alert('Save failed: ' + e.message); }
|
||
}
|
||
|
||
function exportProject() {
|
||
const data = editor.getProjectData();
|
||
const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = 'bfa-hmi-design.json';
|
||
a.click();
|
||
}
|
||
|
||
// Load on init
|
||
(async function() {
|
||
try {
|
||
const resp = await fetch('api/layout');
|
||
const data = await resp.json();
|
||
if (data.assets || data.styles || (data.pages && data.pages.length && data.pages[0].frames)) {
|
||
// V2 GrapesJS project — load it
|
||
editor.loadProjectData(data);
|
||
} else {
|
||
// No V2 yet — try loading converted V1 HTML
|
||
try {
|
||
const htmlResp = await fetch('api/v1html');
|
||
const html = await htmlResp.text();
|
||
if (html && html.length > 10) {
|
||
editor.setComponents(html);
|
||
console.log('Loaded V1 converted layout');
|
||
}
|
||
} catch(e2) { console.log('No V1 HTML either, starting blank'); }
|
||
}
|
||
} catch(e) { console.log('No saved layout, starting fresh'); }
|
||
})();
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// Theme switching
|
||
// ══════════════════════════════════════════════════════
|
||
function switchTheme(theme) {
|
||
applyHMITheme(editor, theme);
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════
|
||
// Simulation panel toggle
|
||
// ══════════════════════════════════════════════════════
|
||
function toggleSim() {
|
||
document.getElementById('sim-panel').classList.toggle('open');
|
||
// Give GrapesJS time to see the resize, then refresh canvas
|
||
setTimeout(() => editor.refresh(), 300);
|
||
}
|
||
|
||
// Inject page-switching script into the canvas after components load
|
||
editor.on('load', () => {
|
||
// Add script to make tab clicks switch pages inside the canvas
|
||
const script = `
|
||
document.addEventListener('click', function(e) {
|
||
var tab = e.target.closest('[style*="cursor:pointer"][style*="font-weight:600"]');
|
||
if (!tab || !tab.parentElement || tab.parentElement.children.length < 2) return;
|
||
// Check if it's in the tab bar
|
||
var bar = tab.parentElement;
|
||
if (!bar.style.cssText.includes('height:50px') && !bar.style.cssText.includes('height: 50px')) return;
|
||
// Get tab index
|
||
var tabs = Array.from(bar.children);
|
||
var idx = tabs.indexOf(tab);
|
||
if (idx < 0) return;
|
||
// Reset all tab styles
|
||
tabs.forEach(function(t) { t.style.color = '#7a8baa'; t.style.borderBottom = 'none'; });
|
||
tab.style.color = '#2d7ff9';
|
||
tab.style.borderBottom = '3px solid #2d7ff9';
|
||
// Show/hide pages
|
||
var pages = document.querySelectorAll('[data-page], [style*="flex-wrap:wrap"][style*="padding:8px"]');
|
||
var pageContainers = Array.from(pages).filter(function(p) { return p.style.cssText.includes('wrap') && p.style.cssText.includes('padding'); });
|
||
pageContainers.forEach(function(p, i) {
|
||
p.style.display = (i === idx) ? 'flex' : 'none';
|
||
});
|
||
});
|
||
`;
|
||
// Inject into canvas
|
||
const frame = editor.Canvas.getFrameEl();
|
||
if (frame && frame.contentDocument) {
|
||
const s = frame.contentDocument.createElement('script');
|
||
s.textContent = script;
|
||
frame.contentDocument.body.appendChild(s);
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|