fix: auto-refresh preview after element edits, improved rendering
Preview now reloads display data whenever elements change. Card style renders with proper label/value/unit layout. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
19497fe335
commit
4964a2e8ab
@ -9,37 +9,35 @@ interface LayoutElement {
|
|||||||
width: number | null;
|
width: number | null;
|
||||||
height: number | null;
|
height: number | null;
|
||||||
font_size: number;
|
font_size: number;
|
||||||
|
font_weight: string;
|
||||||
|
text_align: string;
|
||||||
style: string;
|
style: string;
|
||||||
static_text: string | null;
|
static_text: string | null;
|
||||||
datapoint_id: number | null;
|
datapoint_id: number | null;
|
||||||
|
icon: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface DisplayJson {
|
interface DisplayApiElement {
|
||||||
display_id: number;
|
type: string;
|
||||||
layout: {
|
x: number;
|
||||||
id: number;
|
y: number;
|
||||||
width: number;
|
width?: number;
|
||||||
height: number;
|
height?: number;
|
||||||
};
|
font_size?: number;
|
||||||
elements: Array<{
|
font_weight?: string;
|
||||||
type: string;
|
text_align?: string;
|
||||||
x: number;
|
style?: string;
|
||||||
y: number;
|
icon?: string;
|
||||||
width?: number;
|
text?: string;
|
||||||
height?: number;
|
label?: string;
|
||||||
font_size: number;
|
value?: string | null;
|
||||||
style: string;
|
unit?: string | null;
|
||||||
static_text?: string;
|
|
||||||
label?: string;
|
|
||||||
value?: string;
|
|
||||||
unit?: string;
|
|
||||||
}>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Display {
|
interface DisplayApiResponse {
|
||||||
id: number;
|
display: { id: number; name: string; width: number; height: number; refresh_sec: number };
|
||||||
name: string;
|
layout: { type: string };
|
||||||
layout_id: number | null;
|
elements: DisplayApiElement[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EpaperPreviewProps {
|
interface EpaperPreviewProps {
|
||||||
@ -49,119 +47,146 @@ interface EpaperPreviewProps {
|
|||||||
layoutHeight?: number;
|
layoutHeight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCALE = 0.5;
|
const SCALE = 0.6;
|
||||||
|
|
||||||
export default function EpaperPreview({ layoutId, elements, layoutWidth = 800, layoutHeight = 480 }: EpaperPreviewProps) {
|
export default function EpaperPreview({ layoutId, elements, layoutWidth = 800, layoutHeight = 480 }: EpaperPreviewProps) {
|
||||||
const [displayJson, setDisplayJson] = useState<DisplayJson | null>(null);
|
const [liveElements, setLiveElements] = useState<DisplayApiElement[] | null>(null);
|
||||||
|
const [displayId, setDisplayId] = useState<number | null>(null);
|
||||||
|
|
||||||
|
// Find a display that uses this layout
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Find a display using this layout and fetch its JSON
|
api.get<any[]>('/api/displays')
|
||||||
api.get<Display[]>('/api/displays')
|
|
||||||
.then((displays) => {
|
.then((displays) => {
|
||||||
const d = displays.find((disp) => disp.layout_id === layoutId);
|
const d = displays.find((disp: any) => disp.layout_id === layoutId);
|
||||||
if (!d) return;
|
setDisplayId(d ? d.id : null);
|
||||||
return api.get<DisplayJson>(`/api/display/${d.id}`).then(setDisplayJson).catch(() => {});
|
|
||||||
})
|
})
|
||||||
.catch(() => {});
|
.catch(() => setDisplayId(null));
|
||||||
}, [layoutId]);
|
}, [layoutId]);
|
||||||
|
|
||||||
|
// Reload display data whenever elements change OR displayId is found
|
||||||
|
useEffect(() => {
|
||||||
|
if (!displayId) { setLiveElements(null); return; }
|
||||||
|
api.get<DisplayApiResponse>(`/api/display/${displayId}`)
|
||||||
|
.then((data) => setLiveElements(data.elements))
|
||||||
|
.catch(() => setLiveElements(null));
|
||||||
|
}, [displayId, elements]); // elements is a new array ref after each selectLayout()
|
||||||
|
|
||||||
const w = layoutWidth * SCALE;
|
const w = layoutWidth * SCALE;
|
||||||
const h = layoutHeight * SCALE;
|
const h = layoutHeight * SCALE;
|
||||||
|
|
||||||
const renderEl = (el: typeof elements[0], _idx?: number) => {
|
const renderElements = liveElements || [];
|
||||||
const left = el.x * SCALE;
|
const isLive = liveElements !== null;
|
||||||
const top = el.y * SCALE;
|
|
||||||
const fs = Math.max(8, el.font_size * SCALE);
|
|
||||||
|
|
||||||
if (el.type === 'line') {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={el.id}
|
|
||||||
style={{ position: 'absolute', left, top, width: (el.width ?? 100) * SCALE, height: 1, backgroundColor: '#000' }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (el.type === 'rect') {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={el.id}
|
|
||||||
style={{
|
|
||||||
position: 'absolute', left, top,
|
|
||||||
width: (el.width ?? 80) * SCALE,
|
|
||||||
height: (el.height ?? 40) * SCALE,
|
|
||||||
border: '1px solid #000',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// label or value elements
|
|
||||||
const text = el.static_text ?? (el.type === 'label' ? `[DP ${el.datapoint_id ?? '?'}]` : `[Wert]`);
|
|
||||||
const isCard = el.style === 'card';
|
|
||||||
|
|
||||||
|
// If no display linked and no elements, show hint
|
||||||
|
if (!isLive && elements.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className="mt-4 p-4 bg-gray-50 rounded border text-sm text-gray-400 text-center">
|
||||||
key={el.id}
|
Fuege Elemente hinzu um die Vorschau zu sehen.
|
||||||
style={{
|
|
||||||
position: 'absolute', left, top,
|
|
||||||
fontSize: fs,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
color: '#000',
|
|
||||||
background: isCard ? '#e8e8e8' : 'transparent',
|
|
||||||
border: isCard ? '1px solid #aaa' : 'none',
|
|
||||||
padding: isCard ? '2px 4px' : 0,
|
|
||||||
whiteSpace: 'nowrap',
|
|
||||||
lineHeight: 1.2,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{text}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
// Use live display JSON if available, else render from elements prop
|
// Build render list: use live data if available, otherwise use raw elements
|
||||||
const liveEls = displayJson?.elements;
|
const items: DisplayApiElement[] = isLive
|
||||||
|
? renderElements
|
||||||
|
: elements.map((el) => ({
|
||||||
|
type: el.type,
|
||||||
|
x: el.x,
|
||||||
|
y: el.y,
|
||||||
|
width: el.width ?? undefined,
|
||||||
|
height: el.height ?? undefined,
|
||||||
|
font_size: el.font_size,
|
||||||
|
font_weight: el.font_weight,
|
||||||
|
text_align: el.text_align,
|
||||||
|
style: el.style,
|
||||||
|
icon: el.icon ?? undefined,
|
||||||
|
text: el.type === 'label' ? (el.static_text || '') : undefined,
|
||||||
|
label: el.type === 'value' ? `DP #${el.datapoint_id || '?'}` : undefined,
|
||||||
|
value: el.type === 'value' ? '—' : undefined,
|
||||||
|
unit: '',
|
||||||
|
}));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="text-xs text-gray-500 mb-2">
|
<p className="text-xs text-gray-500 mb-2">
|
||||||
E-Paper Vorschau ({layoutWidth}x{layoutHeight}px){liveEls ? ' — Live-Daten' : ' — Design-Modus'}
|
Vorschau ({layoutWidth}x{layoutHeight})
|
||||||
|
{isLive ? ' — Live-Daten' : ' — Layout-Struktur (kein Display zugewiesen)'}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div style={{
|
||||||
style={{
|
position: 'relative', width: w, height: h,
|
||||||
position: 'relative',
|
backgroundColor: '#f0f0e8', border: '3px solid #222',
|
||||||
width: w,
|
borderRadius: 4, overflow: 'hidden',
|
||||||
height: h,
|
boxShadow: '2px 2px 8px rgba(0,0,0,0.25)',
|
||||||
backgroundColor: '#f5f0e8',
|
fontFamily: 'Arial, sans-serif',
|
||||||
border: '2px solid #1a1a1a',
|
}}>
|
||||||
borderRadius: 4,
|
{items.map((el, idx) => {
|
||||||
overflow: 'hidden',
|
const left = el.x * SCALE;
|
||||||
boxShadow: '2px 2px 8px rgba(0,0,0,0.2)',
|
const top = el.y * SCALE;
|
||||||
}}
|
const fontSize = Math.max(7, (el.font_size || 24) * SCALE);
|
||||||
>
|
const isBold = el.font_weight === 'bold';
|
||||||
{liveEls
|
|
||||||
? liveEls.map((el, idx) => {
|
|
||||||
const left = el.x * SCALE;
|
|
||||||
const top = el.y * SCALE;
|
|
||||||
const fs = Math.max(8, el.font_size * SCALE);
|
|
||||||
|
|
||||||
if (el.type === 'line') {
|
if (el.type === 'line') {
|
||||||
return <div key={idx} style={{ position: 'absolute', left, top, width: (el.width ?? 100) * SCALE, height: 1, backgroundColor: '#000' }} />;
|
return <div key={idx} style={{
|
||||||
}
|
position: 'absolute', left, top,
|
||||||
if (el.type === 'rect') {
|
width: (el.width || 800) * SCALE,
|
||||||
return <div key={idx} style={{ position: 'absolute', left, top, width: (el.width ?? 80) * SCALE, height: (el.height ?? 40) * SCALE, border: '1px solid #000' }} />;
|
height: Math.max(1, (el.height || 2) * SCALE),
|
||||||
}
|
backgroundColor: '#000',
|
||||||
|
}} />;
|
||||||
|
}
|
||||||
|
|
||||||
const text = el.static_text ?? (el.type === 'value' ? `${el.value ?? '?'} ${el.unit ?? ''}` : (el.label ?? ''));
|
if (el.type === 'rect') {
|
||||||
const isCard = el.style === 'card';
|
return <div key={idx} style={{
|
||||||
|
position: 'absolute', left, top,
|
||||||
|
width: (el.width || 100) * SCALE,
|
||||||
|
height: (el.height || 50) * SCALE,
|
||||||
|
border: '1px solid #000',
|
||||||
|
}} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
if (el.type === 'label') {
|
||||||
<div key={idx} style={{ position: 'absolute', left, top, fontSize: fs, fontFamily: 'monospace', color: '#000', background: isCard ? '#e8e8e8' : 'transparent', border: isCard ? '1px solid #aaa' : 'none', padding: isCard ? '2px 4px' : 0, whiteSpace: 'nowrap', lineHeight: 1.2 }}>
|
const align = el.text_align || 'left';
|
||||||
{text}
|
return <div key={idx} style={{
|
||||||
|
position: 'absolute', left, top,
|
||||||
|
fontSize, fontWeight: isBold ? 'bold' : 'normal',
|
||||||
|
color: '#000', whiteSpace: 'nowrap',
|
||||||
|
...(align === 'center' ? { width: w, left: 0, textAlign: 'center' } : {}),
|
||||||
|
...(align === 'right' ? { right: 0, textAlign: 'right' } : {}),
|
||||||
|
}}>
|
||||||
|
{el.text || ''}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (el.type === 'value') {
|
||||||
|
const isCard = el.style === 'card';
|
||||||
|
const elW = (el.width || 370) * SCALE;
|
||||||
|
const elH = (el.height || 90) * SCALE;
|
||||||
|
|
||||||
|
if (isCard) {
|
||||||
|
return <div key={idx} style={{
|
||||||
|
position: 'absolute', left, top,
|
||||||
|
width: elW, height: elH,
|
||||||
|
border: '1px solid #aaa', borderRadius: 4 * SCALE,
|
||||||
|
background: '#e8e8e0', padding: 4 * SCALE,
|
||||||
|
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
|
||||||
|
}}>
|
||||||
|
<div style={{ fontSize: fontSize * 0.45, color: '#555', textTransform: 'uppercase', letterSpacing: 0.5 }}>
|
||||||
|
{el.icon ? `[${el.icon}] ` : ''}{el.label || ''}
|
||||||
</div>
|
</div>
|
||||||
);
|
<div style={{ fontSize, fontWeight: 'bold', textAlign: 'right' }}>
|
||||||
})
|
{el.value ?? '—'} <span style={{ fontSize: fontSize * 0.5, color: '#555' }}>{el.unit || ''}</span>
|
||||||
: elements.map((el) => renderEl(el, el.id))}
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div key={idx} style={{
|
||||||
|
position: 'absolute', left, top, fontSize, color: '#000', whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
{el.label}: <strong>{el.value ?? '—'}</strong> {el.unit || ''}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user