|
|
@@ -0,0 +1,1280 @@
|
|
|
+
|
|
|
+<!DOCTYPE html>
|
|
|
+<html lang="zh-CN">
|
|
|
+<head>
|
|
|
+ <meta charset="UTF-8">
|
|
|
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
+ <title>油气回收在线监控系统</title>
|
|
|
+ <script src="https://cdn.tailwindcss.com"></script>
|
|
|
+ <script src="https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js"></script>
|
|
|
+ <link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&f[]=jetbrains-mono@400,700&display=swap" rel="stylesheet">
|
|
|
+ <script>
|
|
|
+ tailwind.config = {
|
|
|
+ theme: {
|
|
|
+ extend: {
|
|
|
+ fontFamily: {
|
|
|
+ sans: ['Satoshi', 'sans-serif'],
|
|
|
+ mono: ['JetBrains Mono', 'monospace'],
|
|
|
+ },
|
|
|
+ colors: {
|
|
|
+ system: {
|
|
|
+ bg: '#f8fafc', // Light theme background
|
|
|
+ card: '#ffffff', // Light theme card
|
|
|
+ border: '#e2e8f0', // Light theme border
|
|
|
+ normal: '#10b981', // Emerald 500
|
|
|
+ warning: '#f59e0b', // Amber 500
|
|
|
+ alarm: '#ef4444', // Red 500
|
|
|
+ text: '#0f172a', // Dark text for light bg
|
|
|
+ muted: '#64748b' // Muted text
|
|
|
+ }
|
|
|
+ },
|
|
|
+ animation: {
|
|
|
+ 'flow-up': 'flow-up 1s linear infinite',
|
|
|
+ 'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
|
|
+ 'spin-slow': 'spin 3s linear infinite',
|
|
|
+ },
|
|
|
+ keyframes: {
|
|
|
+ 'flow-up': {
|
|
|
+ '0%': { transform: 'translateY(10px)', opacity: '0' },
|
|
|
+ '50%': { opacity: '1' },
|
|
|
+ '100%': { transform: 'translateY(-10px)', opacity: '0' }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+ <style>
|
|
|
+ /* Custom Scrollbar for light dashboard feel */
|
|
|
+ ::-webkit-scrollbar {
|
|
|
+ width: 6px;
|
|
|
+ height: 6px;
|
|
|
+ }
|
|
|
+
|
|
|
+ ::-webkit-scrollbar-track {
|
|
|
+ background: #f1f5f9;
|
|
|
+ }
|
|
|
+
|
|
|
+ ::-webkit-scrollbar-thumb {
|
|
|
+ background: #cbd5e1;
|
|
|
+ border-radius: 3px;
|
|
|
+ }
|
|
|
+
|
|
|
+ ::-webkit-scrollbar-thumb:hover {
|
|
|
+ background: #94a3b8;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Glowing effect for status indicators - adjusted for light mode */
|
|
|
+ .glow-normal {
|
|
|
+ box-shadow: 0 0 12px rgba(16, 185, 129, 0.3);
|
|
|
+ }
|
|
|
+
|
|
|
+ .glow-warning {
|
|
|
+ box-shadow: 0 0 12px rgba(245, 158, 11, 0.4);
|
|
|
+ }
|
|
|
+
|
|
|
+ .glow-alarm {
|
|
|
+ box-shadow: 0 0 16px rgba(239, 68, 68, 0.4);
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+
|
|
|
+ <meta name="preview-version" content="6b5723dc-a31d-45f5-8dff-da5335066aea" />
|
|
|
+ <meta name="preview-timestamp" content="2026-03-04T05:55:35.649Z" />
|
|
|
+
|
|
|
+ <style>
|
|
|
+ html::-webkit-scrollbar, body::-webkit-scrollbar, *::-webkit-scrollbar {
|
|
|
+ display: none !important;
|
|
|
+ width: 0 !important;
|
|
|
+ height: 0 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ html, body, * {
|
|
|
+ -ms-overflow-style: none !important;
|
|
|
+ scrollbar-width: none !important;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+ <style>
|
|
|
+ body:not(.sd-ready) {
|
|
|
+ opacity: 0 !important;
|
|
|
+ }
|
|
|
+
|
|
|
+ body.sd-ready {
|
|
|
+ opacity: 1 !important;
|
|
|
+ transition: opacity 0.1s ease-in;
|
|
|
+ }
|
|
|
+
|
|
|
+ sd-component {
|
|
|
+ display: block;
|
|
|
+ }
|
|
|
+ </style>
|
|
|
+
|
|
|
+ <script type="module">
|
|
|
+ (function() {
|
|
|
+ document.addEventListener('wheel', function(e) {
|
|
|
+ if (e.ctrlKey) e.preventDefault();
|
|
|
+ }, { passive: false });
|
|
|
+ })();
|
|
|
+ </script>
|
|
|
+ <script type="module">
|
|
|
+ // SuperDesign Runtime
|
|
|
+ console.log('[SuperDesign] Preview loaded for versionId:', "6b5723dc-a31d-45f5-8dff-da5335066aea");
|
|
|
+
|
|
|
+ window.__SUPERDESIGN_PREVIEW__ = {
|
|
|
+ versionId: "6b5723dc-a31d-45f5-8dff-da5335066aea",
|
|
|
+ timestamp: "2026-03-04T05:55:35.649Z",
|
|
|
+ _modernScreenshotModule: null,
|
|
|
+
|
|
|
+ sendMessage: function(type, data) {
|
|
|
+ if (window.parent) {
|
|
|
+ window.parent.postMessage({
|
|
|
+ source: 'superdesign-preview',
|
|
|
+ type: type,
|
|
|
+ data: data,
|
|
|
+ versionId: this.versionId
|
|
|
+ }, '*');
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ ready: function() {
|
|
|
+ this.sendMessage('ready', { timestamp: Date.now() });
|
|
|
+ },
|
|
|
+
|
|
|
+
|
|
|
+ _captureScreenshotInternal: async function() {
|
|
|
+ if (!this._modernScreenshotModule) {
|
|
|
+ console.log('[SuperDesign] Loading modern-screenshot library...');
|
|
|
+ this._modernScreenshotModule = await import('https://esm.sh/modern-screenshot@4.6.6');
|
|
|
+ }
|
|
|
+ var modernScreenshot = this._modernScreenshotModule;
|
|
|
+ if (!modernScreenshot || !modernScreenshot.domToCanvas) {
|
|
|
+ throw new Error('modern-screenshot library not loaded');
|
|
|
+ }
|
|
|
+
|
|
|
+ var scrollX = window.scrollX || window.pageXOffset;
|
|
|
+ var scrollY = window.scrollY || window.pageYOffset;
|
|
|
+ var viewportWidth = window.innerWidth;
|
|
|
+ var viewportHeight = window.innerHeight;
|
|
|
+ var documentWidth = Math.max(
|
|
|
+ document.documentElement.scrollWidth,
|
|
|
+ document.documentElement.offsetWidth,
|
|
|
+ document.documentElement.clientWidth,
|
|
|
+ document.body.scrollWidth,
|
|
|
+ document.body.offsetWidth
|
|
|
+ );
|
|
|
+ var documentHeight = Math.max(
|
|
|
+ document.documentElement.scrollHeight,
|
|
|
+ document.documentElement.offsetHeight,
|
|
|
+ document.documentElement.clientHeight,
|
|
|
+ document.body.scrollHeight,
|
|
|
+ document.body.offsetHeight
|
|
|
+ );
|
|
|
+
|
|
|
+ var isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
|
|
|
+ var pixelRatio = isMobile ? 1 : (window.devicePixelRatio || 1);
|
|
|
+
|
|
|
+ var dataUrl;
|
|
|
+
|
|
|
+ if (isMobile) {
|
|
|
+ var wrapper = document.createElement('div');
|
|
|
+ wrapper.style.cssText = 'position:fixed;top:0;left:0;width:' + viewportWidth + 'px;height:' + viewportHeight + 'px;overflow:hidden;z-index:999999;pointer-events:none;';
|
|
|
+ var clone = document.body.cloneNode(true);
|
|
|
+ clone.style.cssText = 'position:absolute;top:' + (-scrollY) + 'px;left:' + (-scrollX) + 'px;margin:0;width:' + documentWidth + 'px;';
|
|
|
+ wrapper.appendChild(clone);
|
|
|
+ document.body.appendChild(wrapper);
|
|
|
+ try {
|
|
|
+ var viewportCanvas = await modernScreenshot.domToCanvas(wrapper, {
|
|
|
+ scale: 1, backgroundColor: '#ffffff',
|
|
|
+ width: viewportWidth, height: viewportHeight,
|
|
|
+ });
|
|
|
+ dataUrl = viewportCanvas.toDataURL('image/png', 0.5);
|
|
|
+ } finally {
|
|
|
+ document.body.removeChild(wrapper);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ var fullCanvas = await modernScreenshot.domToCanvas(document.documentElement, {
|
|
|
+ scale: pixelRatio, backgroundColor: '#ffffff',
|
|
|
+ width: documentWidth, height: documentHeight,
|
|
|
+ });
|
|
|
+ var viewportCanvas = document.createElement('canvas');
|
|
|
+ viewportCanvas.width = viewportWidth * pixelRatio;
|
|
|
+ viewportCanvas.height = viewportHeight * pixelRatio;
|
|
|
+ var ctx = viewportCanvas.getContext('2d');
|
|
|
+ if (!ctx) throw new Error('Failed to get canvas context');
|
|
|
+ ctx.drawImage(
|
|
|
+ fullCanvas,
|
|
|
+ scrollX * pixelRatio, scrollY * pixelRatio,
|
|
|
+ viewportWidth * pixelRatio, viewportHeight * pixelRatio,
|
|
|
+ 0, 0, viewportWidth * pixelRatio, viewportHeight * pixelRatio
|
|
|
+ );
|
|
|
+ dataUrl = viewportCanvas.toDataURL('image/png', 1.0);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!dataUrl || !dataUrl.startsWith('data:image/png;base64,') || dataUrl.length < 100) {
|
|
|
+ throw new Error('Invalid screenshot data');
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ dataUrl: dataUrl,
|
|
|
+ viewportWidth: viewportWidth,
|
|
|
+ viewportHeight: viewportHeight,
|
|
|
+ pixelRatio: pixelRatio
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ autoCapture: async function() {
|
|
|
+ if (window === window.parent) return;
|
|
|
+ try {
|
|
|
+ var result = await this._captureScreenshotInternal();
|
|
|
+ this.sendMessage('auto-screenshot', {
|
|
|
+ dataUrl: result.dataUrl,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ viewportWidth: result.viewportWidth,
|
|
|
+ viewportHeight: result.viewportHeight,
|
|
|
+ pixelRatio: result.pixelRatio,
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[SuperDesign] Auto-capture failed:', error);
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ captureScreenshot: async function(requestId, autoCaptureId) {
|
|
|
+ try {
|
|
|
+ this._currentRequestId = requestId;
|
|
|
+ this._currentAutoCaptureId = autoCaptureId;
|
|
|
+ var result = await this._captureScreenshotInternal();
|
|
|
+ this.sendMessage('screenshot-captured', {
|
|
|
+ dataUrl: result.dataUrl,
|
|
|
+ timestamp: Date.now(),
|
|
|
+ viewportWidth: result.viewportWidth,
|
|
|
+ viewportHeight: result.viewportHeight,
|
|
|
+ pixelRatio: result.pixelRatio,
|
|
|
+ requestId: this._currentRequestId,
|
|
|
+ autoCaptureId: this._currentAutoCaptureId
|
|
|
+ });
|
|
|
+ return result.dataUrl;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[SuperDesign] Screenshot capture failed:', error);
|
|
|
+ this.sendMessage('screenshot-error', {
|
|
|
+ error: error.message || String(error),
|
|
|
+ timestamp: Date.now(),
|
|
|
+ requestId: this._currentRequestId,
|
|
|
+ autoCaptureId: this._currentAutoCaptureId
|
|
|
+ });
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ // Font loading helpers
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.loadedFonts = new Set();
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.loadGoogleFont = function(fontFamily) {
|
|
|
+ if (!fontFamily || this.loadedFonts.has(fontFamily)) return;
|
|
|
+ if (fontFamily.startsWith('Custom-')) return;
|
|
|
+
|
|
|
+ var systemFonts = ['system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI',
|
|
|
+ 'Roboto', 'Helvetica', 'Arial', 'sans-serif', 'Georgia', 'Times New Roman',
|
|
|
+ 'Times', 'serif', 'Menlo', 'Monaco', 'Consolas', 'Courier New', 'monospace'];
|
|
|
+ if (systemFonts.some(function(sf) { return fontFamily.toLowerCase().includes(sf.toLowerCase()); })) return;
|
|
|
+
|
|
|
+ if (!document.querySelector('link[href*="fonts.googleapis.com"]')) {
|
|
|
+ var preconnect1 = document.createElement('link');
|
|
|
+ preconnect1.rel = 'preconnect';
|
|
|
+ preconnect1.href = 'https://fonts.googleapis.com';
|
|
|
+ document.head.appendChild(preconnect1);
|
|
|
+ var preconnect2 = document.createElement('link');
|
|
|
+ preconnect2.rel = 'preconnect';
|
|
|
+ preconnect2.href = 'https://fonts.gstatic.com';
|
|
|
+ preconnect2.crossOrigin = 'anonymous';
|
|
|
+ document.head.appendChild(preconnect2);
|
|
|
+ }
|
|
|
+
|
|
|
+ var linkId = 'google-font-' + fontFamily.replace(/\\s+/g, '-').toLowerCase();
|
|
|
+ if (!document.getElementById(linkId)) {
|
|
|
+ var link = document.createElement('link');
|
|
|
+ link.id = linkId;
|
|
|
+ link.rel = 'stylesheet';
|
|
|
+ link.href = 'https://fonts.googleapis.com/css2?family=' + encodeURIComponent(fontFamily) + ':wght@400;500;600;700&display=swap';
|
|
|
+ link.onerror = function() { console.error('[SuperDesign] Failed to load Google Font:', fontFamily); };
|
|
|
+ document.head.appendChild(link);
|
|
|
+ }
|
|
|
+ this.loadedFonts.add(fontFamily);
|
|
|
+ };
|
|
|
+
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.loadedCustomFonts = new Set();
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.loadCustomFont = function(fontInfo) {
|
|
|
+ if (!fontInfo || !fontInfo.family || !fontInfo.url) return;
|
|
|
+ if (this.loadedCustomFonts.has(fontInfo.family)) return;
|
|
|
+ var styleId = 'custom-font-' + fontInfo.family.replace(/[^a-zA-Z0-9]/g, '-');
|
|
|
+ if (document.getElementById(styleId)) return;
|
|
|
+ var style = document.createElement('style');
|
|
|
+ style.id = styleId;
|
|
|
+ style.textContent = '@font-face { ' +
|
|
|
+ 'font-family: "' + fontInfo.family + '"; ' +
|
|
|
+ 'src: url("' + fontInfo.url + '") format("' + (fontInfo.format || 'woff2') + '"); ' +
|
|
|
+ 'font-weight: 100 900; font-style: normal; font-display: swap; }';
|
|
|
+ document.head.appendChild(style);
|
|
|
+ this.loadedCustomFonts.add(fontInfo.family);
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ // Theme sync
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.applyTheme = function(payload, isDark) {
|
|
|
+ var root = document.documentElement;
|
|
|
+ var colorVars = isDark ? payload.dark : payload.light;
|
|
|
+ for (var name in colorVars) {
|
|
|
+ if (colorVars.hasOwnProperty(name)) {
|
|
|
+ root.style.setProperty(name, colorVars[name]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (payload.customFonts && Array.isArray(payload.customFonts)) {
|
|
|
+ payload.customFonts.forEach(function(fontInfo) {
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.loadCustomFont(fontInfo);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (payload.typography) {
|
|
|
+ var t = payload.typography;
|
|
|
+ if (t.fontSans && !t.fontSans.startsWith('Custom-')) window.__SUPERDESIGN_PREVIEW__.loadGoogleFont(t.fontSans);
|
|
|
+ if (t.fontSerif && !t.fontSerif.startsWith('Custom-')) window.__SUPERDESIGN_PREVIEW__.loadGoogleFont(t.fontSerif);
|
|
|
+ if (t.fontMono && !t.fontMono.startsWith('Custom-')) window.__SUPERDESIGN_PREVIEW__.loadGoogleFont(t.fontMono);
|
|
|
+
|
|
|
+ var formatFontValue = function(ff) {
|
|
|
+ if (!ff) return ff;
|
|
|
+ var first = ff.split(',')[0].trim().replace(/^["']|["']$/g, '');
|
|
|
+ return first.startsWith('var(') ? first : '"' + first + '"';
|
|
|
+ };
|
|
|
+ root.style.setProperty('--font-sans', formatFontValue(t.fontSans));
|
|
|
+ root.style.setProperty('--font-serif', formatFontValue(t.fontSerif));
|
|
|
+ root.style.setProperty('--font-mono', formatFontValue(t.fontMono));
|
|
|
+ root.style.setProperty('--font-size', t.fontSize);
|
|
|
+ root.style.setProperty('--line-height', t.lineHeight);
|
|
|
+ root.style.setProperty('--letter-spacing-body', t.letterSpacing);
|
|
|
+ root.style.setProperty('--letter-spacing-heading', t.headingLetterSpacing);
|
|
|
+ root.style.setProperty('--font-weight-regular', t.fontWeightRegular);
|
|
|
+ root.style.setProperty('--font-weight-bold', t.fontWeightBold);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (payload.effects) {
|
|
|
+ root.style.setProperty('--radius', payload.effects.radius);
|
|
|
+ root.style.setProperty('--shadow', payload.effects.shadow);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isDark) { root.classList.add('dark'); } else { root.classList.remove('dark'); }
|
|
|
+ };
|
|
|
+
|
|
|
+
|
|
|
+ // Message listener
|
|
|
+ window.addEventListener('message', function(event) {
|
|
|
+ if (event.data && event.data.type === 'request-screenshot') {
|
|
|
+ if (window.__SUPERDESIGN_PREVIEW__) {
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.captureScreenshot(event.data.requestId, event.data.autoCaptureId);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (event.data && event.data.type === 'superdesign-theme-update') {
|
|
|
+ if (window.__SUPERDESIGN_PREVIEW__ && event.data.payload) {
|
|
|
+ var isDark = event.data.isDark || false;
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.applyTheme(event.data.payload, isDark);
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.sendMessage('theme-applied', { timestamp: Date.now(), isDark: isDark });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (event.data && event.data.type === 'navigate-to-route') {
|
|
|
+ var route = event.data.route;
|
|
|
+ if (route) {
|
|
|
+ if (route === '/') {
|
|
|
+ if (window.location.hash) {
|
|
|
+ history.pushState(null, '', window.location.pathname + window.location.search);
|
|
|
+ window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ var newHash = '#' + route;
|
|
|
+ if (window.location.hash !== newHash) {
|
|
|
+ window.location.hash = route;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+
|
|
|
+ // Page lifecycle
|
|
|
+ if (document.readyState === 'loading') {
|
|
|
+ document.addEventListener('DOMContentLoaded', function() { window.__SUPERDESIGN_PREVIEW__.ready(); });
|
|
|
+ } else {
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.ready();
|
|
|
+ }
|
|
|
+
|
|
|
+ function onPageFullyLoaded() {
|
|
|
+ requestAnimationFrame(function() {
|
|
|
+ requestAnimationFrame(function() {
|
|
|
+ setTimeout(function() {
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.sendMessage('loaded', { timestamp: Date.now() });
|
|
|
+ }, 1500);
|
|
|
+ // Auto-capture disabled for this preview type
|
|
|
+ });
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ if (document.readyState === 'complete') {
|
|
|
+ onPageFullyLoaded();
|
|
|
+ } else {
|
|
|
+ window.addEventListener('load', onPageFullyLoaded);
|
|
|
+ }
|
|
|
+
|
|
|
+ </script>
|
|
|
+ <script src="https://cdn.jsdelivr.net/npm/petite-vue@0.4.1/dist/petite-vue.iife.js"></script>
|
|
|
+</head>
|
|
|
+<body>
|
|
|
+ <!-- Root Container -->
|
|
|
+ <div class="min-h-screen bg-system-bg text-system-text font-sans flex flex-col overflow-hidden selection:bg-system-normal/20">
|
|
|
+
|
|
|
+ <!-- 1. Header Section -->
|
|
|
+ <header class="h-16 bg-white/90 backdrop-blur-md border-b border-system-border flex items-center justify-between px-6 z-20 shrink-0 shadow-sm">
|
|
|
+ <div class="flex items-center gap-4">
|
|
|
+ <div class="w-10 h-10 rounded-lg bg-gradient-to-br from-system-normal to-emerald-600 flex items-center justify-center shadow-md shadow-system-normal/20">
|
|
|
+ <iconify-icon icon="mdi:leaf" class="text-2xl text-white"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ <h1 class="text-xl tracking-wide font-bold text-slate-800">
|
|
|
+ 油气回收在线监控系统
|
|
|
+ </h1>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Global System Status Summary -->
|
|
|
+ <div class="flex items-center gap-6 bg-slate-50 px-5 py-2 rounded-full border border-slate-200">
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span class="relative flex h-3 w-3">
|
|
|
+ <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-system-normal opacity-75"></span>
|
|
|
+ <span class="relative inline-flex rounded-full h-3 w-3 bg-system-normal"></span>
|
|
|
+ </span>
|
|
|
+ <span class="text-sm text-slate-600">正常: <span class="font-mono font-bold text-system-normal">88</span></span>
|
|
|
+ </div>
|
|
|
+ <div class="w-px h-4 bg-slate-300"></div>
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span class="relative flex h-3 w-3">
|
|
|
+ <span class="relative inline-flex rounded-full h-3 w-3 bg-system-warning"></span>
|
|
|
+ </span>
|
|
|
+ <span class="text-sm text-slate-600">预警: <span class="font-mono font-bold text-system-warning">2</span></span>
|
|
|
+ </div>
|
|
|
+ <div class="w-px h-4 bg-slate-300"></div>
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <span class="relative flex h-3 w-3">
|
|
|
+ <span class="animate-pulse absolute inline-flex h-full w-full rounded-full bg-system-alarm opacity-75"></span>
|
|
|
+ <span class="relative inline-flex rounded-full h-3 w-3 bg-system-alarm"></span>
|
|
|
+ </span>
|
|
|
+ <span class="text-sm text-slate-600">报警: <span class="font-mono font-bold text-system-alarm">2</span></span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Clock & User -->
|
|
|
+ <div class="flex items-center gap-6">
|
|
|
+ <div class="flex flex-col items-end">
|
|
|
+ <span class="font-mono text-lg font-bold text-slate-800 tracking-wider">14:32:45</span>
|
|
|
+ <span class="text-xs text-system-muted font-mono">2023-10-27 星期五</span>
|
|
|
+ </div>
|
|
|
+ <button class="w-10 h-10 rounded-full bg-slate-50 border border-slate-200 flex items-center justify-center hover:bg-slate-100 transition-colors">
|
|
|
+ <iconify-icon icon="mdi:account-cog" class="text-xl text-slate-600"></iconify-icon>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </header>
|
|
|
+
|
|
|
+ <!-- 2. Top Status Bar (Key Metrics & Remote Platforms) -->
|
|
|
+ <section class="px-6 py-4 grid grid-cols-12 gap-4 shrink-0 z-10">
|
|
|
+ <!-- Key Metrics (Left 8 columns) -->
|
|
|
+ <div class="col-span-8 grid grid-cols-4 gap-4">
|
|
|
+ <!-- Tank Pressure: Normal -->
|
|
|
+ <div class="bg-system-card rounded-xl border border-system-border shadow-sm p-4 flex items-center gap-4 relative overflow-hidden">
|
|
|
+ <div class="absolute top-0 right-0 w-16 h-16 bg-system-normal/10 rounded-bl-full"></div>
|
|
|
+ <div class="w-12 h-12 rounded-full bg-system-normal/10 flex items-center justify-center border border-system-normal/20 shrink-0">
|
|
|
+ <iconify-icon icon="mdi:gauge" class="text-2xl text-system-normal"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1">
|
|
|
+ <div class="text-xs text-system-muted mb-1">油罐压力</div>
|
|
|
+ <div class="flex items-baseline gap-1">
|
|
|
+ <span class="text-2xl font-mono font-bold text-slate-800">1.25</span>
|
|
|
+ <span class="text-xs text-system-muted font-mono">kPa</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex flex-col items-end">
|
|
|
+ <iconify-icon icon="mdi:check-circle" class="text-system-normal"></iconify-icon>
|
|
|
+ <span class="text-[10px] text-system-normal mt-1">正常</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Vapor Concentration: Warning -->
|
|
|
+ <div class="bg-system-card rounded-xl border border-system-warning/30 shadow-sm p-4 flex items-center gap-4 relative overflow-hidden">
|
|
|
+ <div class="absolute top-0 right-0 w-16 h-16 bg-system-warning/10 rounded-bl-full"></div>
|
|
|
+ <div class="w-12 h-12 rounded-full bg-system-warning/10 flex items-center justify-center border border-system-warning/30 shrink-0">
|
|
|
+ <iconify-icon icon="mdi:water-percent" class="text-2xl text-system-warning"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1">
|
|
|
+ <div class="text-xs text-system-muted mb-1">油气浓度</div>
|
|
|
+ <div class="flex items-baseline gap-1">
|
|
|
+ <span class="text-2xl font-mono font-bold text-system-warning">18.5</span>
|
|
|
+ <span class="text-xs text-system-muted font-mono">g/m³</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex flex-col items-end">
|
|
|
+ <iconify-icon icon="mdi:alert-outline" class="text-system-warning animate-pulse"></iconify-icon>
|
|
|
+ <span class="text-[10px] text-system-warning mt-1">偏高</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Pipeline Pressure: Normal -->
|
|
|
+ <div class="bg-system-card rounded-xl border border-system-border shadow-sm p-4 flex items-center gap-4 relative overflow-hidden">
|
|
|
+ <div class="absolute top-0 right-0 w-16 h-16 bg-system-normal/10 rounded-bl-full"></div>
|
|
|
+ <div class="w-12 h-12 rounded-full bg-system-normal/10 flex items-center justify-center border border-system-normal/20 shrink-0">
|
|
|
+ <iconify-icon icon="mdi:pipe" class="text-2xl text-system-normal"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1">
|
|
|
+ <div class="text-xs text-system-muted mb-1">管线压力</div>
|
|
|
+ <div class="flex items-baseline gap-1">
|
|
|
+ <span class="text-2xl font-mono font-bold text-slate-800">3.80</span>
|
|
|
+ <span class="text-xs text-system-muted font-mono">kPa</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex flex-col items-end">
|
|
|
+ <iconify-icon icon="mdi:check-circle" class="text-system-normal"></iconify-icon>
|
|
|
+ <span class="text-[10px] text-system-normal mt-1">正常</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Tank Temperature: Alarm -->
|
|
|
+ <div class="bg-system-card rounded-xl border border-system-alarm/40 shadow-sm p-4 flex items-center gap-4 relative overflow-hidden glow-alarm">
|
|
|
+ <div class="absolute top-0 right-0 w-16 h-16 bg-system-alarm/10 rounded-bl-full"></div>
|
|
|
+ <div class="w-12 h-12 rounded-full bg-system-alarm/10 flex items-center justify-center border border-system-alarm/30 shrink-0">
|
|
|
+ <iconify-icon icon="mdi:thermometer-alert" class="text-2xl text-system-alarm"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1">
|
|
|
+ <div class="text-xs text-system-muted mb-1">油罐温度</div>
|
|
|
+ <div class="flex items-baseline gap-1">
|
|
|
+ <span class="text-2xl font-mono font-bold text-system-alarm">42.5</span>
|
|
|
+ <span class="text-xs text-system-muted font-mono">°C</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex flex-col items-end">
|
|
|
+ <iconify-icon icon="mdi:close-octagon" class="text-system-alarm animate-bounce"></iconify-icon>
|
|
|
+ <span class="text-[10px] text-system-alarm mt-1 font-bold">超限</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Remote Platform Status (Right 4 columns) -->
|
|
|
+ <div class="col-span-4 bg-system-card rounded-xl border border-system-border shadow-sm p-4 flex flex-col justify-between">
|
|
|
+ <div class="text-sm font-medium text-slate-700 flex items-center gap-2 mb-2">
|
|
|
+ <iconify-icon icon="mdi:cloud-upload-outline" class="text-slate-500"></iconify-icon>
|
|
|
+ 数据远传状态
|
|
|
+ </div>
|
|
|
+ <div class="grid grid-cols-2 gap-2 h-full">
|
|
|
+ <div class="bg-slate-50 border border-slate-100 rounded px-3 py-2 flex items-center justify-between">
|
|
|
+ <span class="text-xs text-slate-600">市环保局</span>
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <iconify-icon icon="mdi:sync" class="text-system-normal text-xs animate-spin-slow"></iconify-icon>
|
|
|
+ <span class="text-[10px] text-system-normal">上传中</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bg-slate-50 border border-slate-100 rounded px-3 py-2 flex items-center justify-between">
|
|
|
+ <span class="text-xs text-slate-600">省质监局</span>
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <span class="w-2 h-2 rounded-full bg-system-normal"></span>
|
|
|
+ <span class="text-[10px] text-slate-500">已同步</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bg-slate-50 border border-slate-100 rounded px-3 py-2 flex items-center justify-between">
|
|
|
+ <span class="text-xs text-slate-600">集团总部</span>
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <iconify-icon icon="mdi:sync" class="text-system-normal text-xs animate-spin-slow"></iconify-icon>
|
|
|
+ <span class="text-[10px] text-system-normal">上传中</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="bg-orange-50/50 border border-system-warning/30 rounded px-3 py-2 flex items-center justify-between">
|
|
|
+ <span class="text-xs text-slate-600">应急管理</span>
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <span class="w-2 h-2 rounded-full bg-system-warning animate-pulse"></span>
|
|
|
+ <span class="text-[10px] text-system-warning">网络延迟</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </section>
|
|
|
+
|
|
|
+ <!-- 3. Main Nozzle Grid Section -->
|
|
|
+ <main class="flex-1 overflow-y-auto px-6 pb-24">
|
|
|
+ <!-- Dense Grid Layout -->
|
|
|
+ <div class="grid grid-cols-5 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 2xl:grid-cols-12 gap-1.5">
|
|
|
+
|
|
|
+ <!-- Card 01: Normal Fueling (Green Icon/Border, Green Tag) -->
|
|
|
+ <article class="group relative bg-system-card border border-system-normal shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">01#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200">92#</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-normal flex justify-center items-center bg-system-normal/10 glow-normal">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-normal"></iconify-icon>
|
|
|
+ <div class="absolute -right-2 top-1/2 -translate-y-1/2 flex flex-col gap-0.5">
|
|
|
+ <iconify-icon icon="mdi:chevron-up" class="text-[8px] text-system-normal animate-flow-up" style="animation-delay: 0s"></iconify-icon>
|
|
|
+ <iconify-icon icon="mdi:chevron-up" class="text-[8px] text-system-normal animate-flow-up" style="animation-delay: 0.3s"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-system-normal font-bold">1.05</span>
|
|
|
+ </div>
|
|
|
+ <span class="text-[9px] text-system-normal bg-system-normal/10 px-1 py-0.5 rounded border border-system-normal/20">加油中</span>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <!-- Card 02: Normal Idle (Green Icon/Border, Gray Tag) -->
|
|
|
+ <article class="group relative bg-system-card border border-system-normal/60 hover:border-system-normal shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 opacity-90">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">02#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200">92#</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-normal flex justify-center items-center bg-slate-50">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-normal"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-slate-500 font-bold">--</span>
|
|
|
+ </div>
|
|
|
+ <span class="text-[9px] text-slate-500 bg-slate-100 px-1 py-0.5 rounded border border-slate-200">空闲</span>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <!-- Card 03: Normal Idle -->
|
|
|
+ <article class="group relative bg-system-card border border-system-normal/60 hover:border-system-normal shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 opacity-90">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">03#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200">95#</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-normal flex justify-center items-center bg-slate-50">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-normal"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-slate-500 font-bold">--</span>
|
|
|
+ </div>
|
|
|
+ <span class="text-[9px] text-slate-500 bg-slate-100 px-1 py-0.5 rounded border border-slate-200">空闲</span>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <!-- Card 04: Offline (Gray Icon/Border, Gray Tag) -->
|
|
|
+ <article class="group relative bg-system-card border border-slate-300 hover:border-slate-400 shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 opacity-75">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-600">04#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-slate-100 text-slate-400 border border-slate-200">95#</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-slate-300 flex justify-center items-center bg-slate-50">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-slate-400"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-slate-400">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-slate-400 font-bold">--</span>
|
|
|
+ </div>
|
|
|
+ <span class="text-[9px] text-slate-500 bg-slate-100 px-1 py-0.5 rounded border border-slate-200">离线</span>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <!-- Card 05: WARNING (Yellow Color Only, No Text Tag) -->
|
|
|
+ <article class="group relative bg-system-card border-2 border-system-warning shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 bg-orange-50/30">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">05#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-orange-100 text-system-warning border border-orange-200">98#</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-warning flex justify-center items-center bg-system-warning/10 glow-warning">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-warning"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-system-warning font-bold">1.28</span>
|
|
|
+ </div>
|
|
|
+ <!-- Deliberately left empty, warning relies entirely on border/icon color -->
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <!-- Card 06: ALARM (Red Color Only, No Text Tag) -->
|
|
|
+ <article class="group relative bg-system-card border-2 border-system-alarm shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 bg-red-50/30">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">06#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-red-100 text-system-alarm border border-red-200">92#</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-alarm flex justify-center items-center bg-system-alarm/10 glow-alarm">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-alarm"></iconify-icon>
|
|
|
+ <div class="absolute -top-1 -right-1 w-3 h-3 bg-system-alarm rounded-full flex items-center justify-center animate-bounce shadow-md">
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-system-alarm font-bold">0.00</span>
|
|
|
+ </div>
|
|
|
+ <!-- Deliberately left empty, alarm relies entirely on border/icon color -->
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+
|
|
|
+ <!-- Generate remaining cards -->
|
|
|
+ <script>
|
|
|
+ const fuelTypes = ['92#', '95#', '98#', '0#'];
|
|
|
+ for(let i=7; i<=32; i++) {
|
|
|
+ let num = i < 10 ? '0'+i : i;
|
|
|
+ let fuel = fuelTypes[Math.floor(Math.random() * fuelTypes.length)];
|
|
|
+ let rand = Math.random();
|
|
|
+
|
|
|
+ if(i === 15) {
|
|
|
+ // Warning (Yellow, No Text Tag)
|
|
|
+ document.write(`
|
|
|
+ <article class="group relative bg-system-card border-2 border-system-warning shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 bg-orange-50/30">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">${num}#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-orange-100 text-system-warning border border-orange-200">${fuel}</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-warning flex justify-center items-center bg-system-warning/10 glow-warning">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-warning"></iconify-icon>
|
|
|
+ <div class="absolute -right-2 top-1/2 -translate-y-1/2 flex flex-col gap-0.5">
|
|
|
+ <iconify-icon icon="mdi:chevron-up" class="text-[8px] text-system-warning animate-flow-up" style="animation-delay: 0s"></iconify-icon>
|
|
|
+ <iconify-icon icon="mdi:chevron-up" class="text-[8px] text-system-warning animate-flow-up" style="animation-delay: 0.3s"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-system-warning font-bold">0.85</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ `);
|
|
|
+ } else if(i === 22) {
|
|
|
+ // Alarm (Red, No Text Tag)
|
|
|
+ document.write(`
|
|
|
+ <article class="group relative bg-system-card border-2 border-system-alarm shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 bg-red-50/30">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">${num}#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-red-100 text-system-alarm border border-red-200">${fuel}</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-alarm flex justify-center items-center bg-system-alarm/10 glow-alarm">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-alarm"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-system-alarm font-bold">2.50</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ `);
|
|
|
+ } else if (rand > 0.85) {
|
|
|
+ // Offline (Gray, Gray Tag)
|
|
|
+ document.write(`
|
|
|
+ <article class="group relative bg-system-card border border-slate-300 hover:border-slate-400 shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 opacity-75">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-600">${num}#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-slate-100 text-slate-400 border border-slate-200">${fuel}</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-slate-300 flex justify-center items-center bg-slate-50">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-slate-400"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-slate-400">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-slate-400 font-bold">--</span>
|
|
|
+ </div>
|
|
|
+ <span class="text-[9px] text-slate-500 bg-slate-100 px-1 py-0.5 rounded border border-slate-200">离线</span>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ `);
|
|
|
+ } else if (rand > 0.4) {
|
|
|
+ // Fueling (Green border/icon, Green Tag)
|
|
|
+ let al = (1.0 + Math.random() * 0.15).toFixed(2);
|
|
|
+ document.write(`
|
|
|
+ <article class="group relative bg-system-card border border-system-normal shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">${num}#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200">${fuel}</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-normal flex justify-center items-center bg-system-normal/10 glow-normal">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-normal"></iconify-icon>
|
|
|
+ <div class="absolute -right-2 top-1/2 -translate-y-1/2 flex flex-col gap-0.5">
|
|
|
+ <iconify-icon icon="mdi:chevron-up" class="text-[8px] text-system-normal animate-flow-up" style="animation-delay: 0s"></iconify-icon>
|
|
|
+ <iconify-icon icon="mdi:chevron-up" class="text-[8px] text-system-normal animate-flow-up" style="animation-delay: 0.3s"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-system-normal font-bold">${al}</span>
|
|
|
+ </div>
|
|
|
+ <span class="text-[9px] text-system-normal bg-system-normal/10 px-1 py-0.5 rounded border border-system-normal/20">加油中</span>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ `);
|
|
|
+ } else {
|
|
|
+ // Idle (Green border/icon, Gray Tag)
|
|
|
+ document.write(`
|
|
|
+ <article class="group relative bg-system-card border border-system-normal/60 hover:border-system-normal shadow-sm rounded-lg p-2 flex flex-col gap-1 transition-all duration-300 opacity-90">
|
|
|
+ <div class="flex justify-between items-center">
|
|
|
+ <span class="text-sm font-mono font-bold text-slate-800">${num}#</span>
|
|
|
+ <span class="text-[10px] px-1 py-0.5 rounded bg-slate-100 text-slate-600 border border-slate-200">${fuel}</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex-1 flex justify-center items-center py-1.5">
|
|
|
+ <div class="relative w-12 h-12 rounded-full border-2 border-system-normal flex justify-center items-center bg-slate-50">
|
|
|
+ <iconify-icon icon="mdi:fuel" class="text-lg text-system-normal"></iconify-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="flex justify-between items-end mt-1 h-5">
|
|
|
+ <div class="flex flex-col leading-tight">
|
|
|
+ <span class="text-[9px] text-system-muted">气液比</span>
|
|
|
+ <span class="text-xs font-mono text-slate-500 font-bold">--</span>
|
|
|
+ </div>
|
|
|
+ <span class="text-[9px] text-slate-500 bg-slate-100 px-1 py-0.5 rounded border border-slate-200">空闲</span>
|
|
|
+ </div>
|
|
|
+ </article>
|
|
|
+ `);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ </script>
|
|
|
+ </div>
|
|
|
+ </main>
|
|
|
+
|
|
|
+ <!-- 4. Pagination Control (Fixed Bottom) -->
|
|
|
+ <footer class="fixed bottom-0 w-full h-14 bg-white/95 backdrop-blur-md border-t border-system-border px-6 flex items-center justify-between z-40 shadow-[0_-2px_10px_rgba(0,0,0,0.02)]">
|
|
|
+ <!-- Legend -->
|
|
|
+ <div class="flex items-center gap-4 text-xs">
|
|
|
+ <span class="text-slate-500">状态图例:</span>
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <span class="w-2.5 h-2.5 rounded-full bg-system-normal glow-normal border border-system-normal/20"></span>
|
|
|
+ <span class="text-slate-600">正常(加油/空闲)</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <span class="w-2.5 h-2.5 rounded-full bg-slate-300 border border-slate-400"></span>
|
|
|
+ <span class="text-slate-600">离线</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <span class="w-2.5 h-2.5 rounded-full bg-system-warning glow-warning"></span>
|
|
|
+ <span class="text-slate-600">预警</span>
|
|
|
+ </div>
|
|
|
+ <div class="flex items-center gap-1.5">
|
|
|
+ <span class="w-2.5 h-2.5 rounded-full bg-system-alarm glow-alarm"></span>
|
|
|
+ <span class="text-slate-600">报警</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Pagination Center -->
|
|
|
+ <div class="flex items-center gap-6">
|
|
|
+ <span class="text-sm text-slate-500">第 <span class="text-slate-800 font-bold">1</span> 页 / 共 3 页 (96条)</span>
|
|
|
+
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <button class="px-3 py-1 rounded bg-white border border-slate-200 text-slate-400 transition-colors disabled:opacity-50 text-sm" disabled>
|
|
|
+ <iconify-icon icon="mdi:chevron-left" class="inline-block align-middle"></iconify-icon> 上一页
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- Page 1: Current, has Alarm (Red) -->
|
|
|
+ <button class="w-7 h-7 rounded flex items-center justify-center bg-system-alarm text-white font-bold shadow-sm transition-all">
|
|
|
+ 1
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- Page 2: Has Warning (Yellow) -->
|
|
|
+ <button class="w-7 h-7 rounded flex items-center justify-center bg-orange-50 text-system-warning border border-system-warning/30 hover:bg-orange-100 transition-all relative" title="该页存在预警项">
|
|
|
+ 2
|
|
|
+ <div class="absolute -top-1 -right-1 w-2 h-2 bg-system-warning rounded-full"></div>
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <!-- Page 3: All Normal (Green) -->
|
|
|
+ <button class="w-7 h-7 rounded flex items-center justify-center bg-emerald-50 text-emerald-700 border border-emerald-200 hover:bg-emerald-100 transition-all" title="该页全正常">
|
|
|
+ 3
|
|
|
+ </button>
|
|
|
+
|
|
|
+ <button class="px-3 py-1 rounded bg-white border border-slate-200 text-slate-600 hover:text-slate-800 hover:bg-slate-50 transition-colors text-sm">
|
|
|
+ 下一页 <iconify-icon icon="mdi:chevron-right" class="inline-block align-middle"></iconify-icon>
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- Quick Jump -->
|
|
|
+ <div class="flex items-center gap-2 text-sm">
|
|
|
+ <span class="text-slate-500">跳转至</span>
|
|
|
+ <select class="bg-white border border-slate-300 text-slate-700 text-sm rounded focus:ring-system-normal focus:border-system-normal block px-2 py-1 outline-none">
|
|
|
+ <option>1</option>
|
|
|
+ <option>2</option>
|
|
|
+ <option>3</option>
|
|
|
+ </select>
|
|
|
+ <span class="text-slate-500">页</span>
|
|
|
+ </div>
|
|
|
+ </footer>
|
|
|
+
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <script defer type="module">
|
|
|
+ window.addEventListener('error', function(event) {
|
|
|
+ var msg = event.message || '';
|
|
|
+ var name = (event.error && event.error.name) || msg.split(':')[0] || '';
|
|
|
+ if (name === 'HierarchyRequestError' || msg.includes("Failed to execute 'appendChild' on 'Node'")) return;
|
|
|
+ if (window.__SUPERDESIGN_PREVIEW__) {
|
|
|
+ var err = event.error;
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.sendMessage('error', {
|
|
|
+ message: event.message, filename: event.filename,
|
|
|
+ lineno: event.lineno, colno: event.colno,
|
|
|
+ name: (err && err.name) || name || 'Error',
|
|
|
+ stack: (err && err.stack) || null
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ window.addEventListener('unhandledrejection', function(event) {
|
|
|
+ var reason = event.reason;
|
|
|
+ var msg = (reason && reason.message) || (typeof reason === 'string' ? reason : '');
|
|
|
+ var name = (reason && reason.name) || 'UnhandledRejection';
|
|
|
+ if (name === 'HierarchyRequestError' || msg.includes("Failed to execute 'appendChild' on 'Node'")) return;
|
|
|
+ if (window.__SUPERDESIGN_PREVIEW__) {
|
|
|
+ window.__SUPERDESIGN_PREVIEW__.sendMessage('unhandled-rejection', {
|
|
|
+ reason: (reason && reason.message) || String(reason),
|
|
|
+ name: name,
|
|
|
+ stack: (reason && reason.stack) || null
|
|
|
+ });
|
|
|
+ }
|
|
|
+ });
|
|
|
+ </script>
|
|
|
+ <script defer type="module">import '/__visual-edit-bridge/iframe-runtime.mjs?v=e62d2259';</script>
|
|
|
+ <script defer type="module">
|
|
|
+ (function() {
|
|
|
+ 'use strict';
|
|
|
+
|
|
|
+ var MAX_RENDER_DEPTH = 5;
|
|
|
+
|
|
|
+ function showPage() {
|
|
|
+ document.body.classList.add('sd-ready');
|
|
|
+ }
|
|
|
+
|
|
|
+ var componentRegistry = new Map();
|
|
|
+ var instanceRegistry = new Map();
|
|
|
+ var API_BASE = "https://api.superdesign.dev";
|
|
|
+ var DRAFT_ID = "6b5723dc-a31d-45f5-8dff-da5335066aea";
|
|
|
+
|
|
|
+ function buildDefaultProps(definition) {
|
|
|
+ var defaultProps = {};
|
|
|
+ var props = definition.props;
|
|
|
+ if (!props || !Array.isArray(props)) return defaultProps;
|
|
|
+ for (var i = 0; i < props.length; i++) {
|
|
|
+ var prop = props[i];
|
|
|
+ if (prop.defaultValue === undefined) continue;
|
|
|
+ var path = prop.name.split('.');
|
|
|
+ var current = defaultProps;
|
|
|
+ for (var j = 0; j < path.length - 1; j++) {
|
|
|
+ var key = path[j];
|
|
|
+ if (!(key in current) || typeof current[key] !== 'object' || current[key] === null) {
|
|
|
+ current[key] = {};
|
|
|
+ }
|
|
|
+ current = current[key];
|
|
|
+ }
|
|
|
+ current[path[path.length - 1]] = prop.defaultValue;
|
|
|
+ }
|
|
|
+ return defaultProps;
|
|
|
+ }
|
|
|
+
|
|
|
+ function deepMerge(target, source) {
|
|
|
+ var result = Object.assign({}, target);
|
|
|
+ for (var key in source) {
|
|
|
+ if (!source.hasOwnProperty(key)) continue;
|
|
|
+ if (
|
|
|
+ source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
|
|
|
+ result[key] && typeof result[key] === 'object' && !Array.isArray(result[key])
|
|
|
+ ) {
|
|
|
+ result[key] = deepMerge(result[key], source[key]);
|
|
|
+ } else {
|
|
|
+ result[key] = source[key];
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ function registerComponent(id, definition) {
|
|
|
+ componentRegistry.set(id, definition);
|
|
|
+ console.log('[SDComponent] Registered component:', id);
|
|
|
+ }
|
|
|
+
|
|
|
+ function renderComponent(element, depth) {
|
|
|
+ depth = depth || 0;
|
|
|
+ var componentId = element.getAttribute('componentid') || element.getAttribute('ref');
|
|
|
+ var instanceId = element.getAttribute('instance');
|
|
|
+ var propsAttr = element.getAttribute('props');
|
|
|
+
|
|
|
+ if (!componentId) {
|
|
|
+ console.warn('[SDComponent] Missing componentId attribute on sd-component');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (depth >= MAX_RENDER_DEPTH) {
|
|
|
+ console.warn('[SDComponent] Max render depth reached for:', componentId);
|
|
|
+ element.setAttribute('data-error', 'max-depth');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var definition = componentRegistry.get(componentId);
|
|
|
+ if (!definition) {
|
|
|
+ console.warn('[SDComponent] Component not found:', componentId);
|
|
|
+ element.setAttribute('data-pending', 'true');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ var instanceProps = {};
|
|
|
+ if (propsAttr) {
|
|
|
+ try { instanceProps = JSON.parse(propsAttr); }
|
|
|
+ catch (e) { console.warn('[SDComponent] Invalid props JSON:', propsAttr); }
|
|
|
+ }
|
|
|
+
|
|
|
+ var defaultProps = buildDefaultProps(definition);
|
|
|
+ var finalProps = deepMerge(defaultProps, instanceProps);
|
|
|
+
|
|
|
+ element.innerHTML = definition.htmlContent;
|
|
|
+
|
|
|
+ if (window.PetiteVue) {
|
|
|
+ finalProps.$emit = function(eventName, payload) {
|
|
|
+ if (payload && typeof payload === 'object' && payload.href) {
|
|
|
+ window.location.href = payload.href;
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ var customEvent = new CustomEvent('sd-' + eventName, {
|
|
|
+ detail: payload, bubbles: true, composed: true,
|
|
|
+ });
|
|
|
+ element.dispatchEvent(customEvent);
|
|
|
+ };
|
|
|
+ window.PetiteVue.createApp(finalProps).mount(element);
|
|
|
+ } else {
|
|
|
+ console.warn('[SDComponent] Petite-Vue not loaded, template syntax will not be resolved');
|
|
|
+ }
|
|
|
+
|
|
|
+ instanceRegistry.set(instanceId, {
|
|
|
+ componentId: componentId,
|
|
|
+ element: element,
|
|
|
+ props: finalProps,
|
|
|
+ version: definition.version,
|
|
|
+ });
|
|
|
+
|
|
|
+ element.removeAttribute('data-pending');
|
|
|
+ element.setAttribute('data-rendered', 'true');
|
|
|
+ element.setAttribute('data-version', String(definition.version));
|
|
|
+ console.log('[SDComponent] Rendered:', componentId, instanceId);
|
|
|
+
|
|
|
+ // Render any nested sd-component children that were just injected
|
|
|
+ var nestedElements = element.querySelectorAll('sd-component[componentid], sd-component[ref]');
|
|
|
+ if (nestedElements.length > 0) {
|
|
|
+ var missingIds = [];
|
|
|
+ nestedElements.forEach(function(nested) {
|
|
|
+ var nestedId = nested.getAttribute('componentid') || nested.getAttribute('ref');
|
|
|
+ if (nestedId && !componentRegistry.has(nestedId)) {
|
|
|
+ missingIds.push(nestedId);
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (missingIds.length > 0) {
|
|
|
+ // Fetch missing nested components, then render
|
|
|
+ loadComponentsByIds(missingIds).then(function() {
|
|
|
+ nestedElements.forEach(function(nested) {
|
|
|
+ if (!nested.getAttribute('data-rendered')) {
|
|
|
+ renderComponent(nested, depth + 1);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // All nested definitions already loaded — render immediately
|
|
|
+ nestedElements.forEach(function(nested) {
|
|
|
+ if (!nested.getAttribute('data-rendered')) {
|
|
|
+ renderComponent(nested, depth + 1);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function updateComponentInstances(componentId) {
|
|
|
+ var elements = document.querySelectorAll('sd-component[componentid="' + componentId + '"], sd-component[ref="' + componentId + '"]');
|
|
|
+ elements.forEach(function(el) { renderComponent(el); });
|
|
|
+ }
|
|
|
+
|
|
|
+ function initializeComponents() {
|
|
|
+ var elements = document.querySelectorAll('sd-component[componentid], sd-component[ref]');
|
|
|
+ elements.forEach(function(el) {
|
|
|
+ if (!el.getAttribute('data-rendered')) { renderComponent(el); }
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadComponentsByIds(ids) {
|
|
|
+ if (!ids || ids.length === 0) return;
|
|
|
+ // Deduplicate and filter already-loaded
|
|
|
+ var uniqueIds = ids.filter(function(id, i, arr) {
|
|
|
+ return arr.indexOf(id) === i && !componentRegistry.has(id);
|
|
|
+ });
|
|
|
+ if (uniqueIds.length === 0) return;
|
|
|
+
|
|
|
+ try {
|
|
|
+ console.log('[SDComponent] Batch loading components:', uniqueIds);
|
|
|
+ var response = await fetch(API_BASE + '/v1/design-drafts/components/batch', {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({ componentIds: uniqueIds }),
|
|
|
+ });
|
|
|
+ if (!response.ok) throw new Error('Batch load failed: ' + response.status);
|
|
|
+ var components = await response.json();
|
|
|
+ console.log('[SDComponent] Batch loaded', components.length, 'components');
|
|
|
+ components.forEach(function(comp) { registerComponent(comp.id, comp); });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[SDComponent] Failed to batch load components:', error);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async function loadComponentsForDraft() {
|
|
|
+ if (!DRAFT_ID) {
|
|
|
+ console.warn('[SDComponent] No draft ID configured');
|
|
|
+ showPage();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ console.log('[SDComponent] Loading components for draft:', DRAFT_ID);
|
|
|
+ var response = await fetch(API_BASE + '/v1/design-drafts/' + DRAFT_ID + '/components', {
|
|
|
+ method: 'GET',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ });
|
|
|
+ if (!response.ok) throw new Error('Failed to load components: ' + response.status);
|
|
|
+ var components = await response.json();
|
|
|
+ console.log('[SDComponent] Loaded', components.length, 'components');
|
|
|
+ components.forEach(function(comp) { registerComponent(comp.id, comp); });
|
|
|
+ initializeComponents();
|
|
|
+ showPage();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[SDComponent] Failed to load components:', error);
|
|
|
+ showPage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ function findReferencedComponents() {
|
|
|
+ var elements = document.querySelectorAll('sd-component[componentid], sd-component[ref]');
|
|
|
+ var ids = new Set();
|
|
|
+ elements.forEach(function(el) {
|
|
|
+ var ref = el.getAttribute('componentid') || el.getAttribute('ref');
|
|
|
+ if (ref) ids.add(ref);
|
|
|
+ });
|
|
|
+ return Array.from(ids);
|
|
|
+ }
|
|
|
+
|
|
|
+ window.__SD_COMPONENTS__ = {
|
|
|
+ register: registerComponent,
|
|
|
+ render: renderComponent,
|
|
|
+ update: updateComponentInstances,
|
|
|
+ loadForDraft: loadComponentsForDraft,
|
|
|
+ loadByIds: loadComponentsByIds,
|
|
|
+ init: initializeComponents,
|
|
|
+ findRefs: findReferencedComponents,
|
|
|
+ getRegistry: function() { return componentRegistry; },
|
|
|
+ getInstances: function() { return instanceRegistry; },
|
|
|
+ getDraftId: function() { return DRAFT_ID; },
|
|
|
+ };
|
|
|
+
|
|
|
+ window.addEventListener('message', function(event) {
|
|
|
+ if (event.data && event.data.type === 'SD_COMPONENT_UPDATE') {
|
|
|
+ var cId = event.data.componentId;
|
|
|
+ var def = event.data.definition;
|
|
|
+ if (cId && def) { registerComponent(cId, def); updateComponentInstances(cId); }
|
|
|
+ }
|
|
|
+ if (event.data && event.data.type === 'SD_COMPONENTS_REGISTER') {
|
|
|
+ var comps = event.data.components;
|
|
|
+ if (Array.isArray(comps)) {
|
|
|
+ comps.forEach(function(c) { registerComponent(c.id, c); });
|
|
|
+ initializeComponents();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ function bootComponents() {
|
|
|
+ // Pre-register components from __SD_COMPONENT_PRELOAD__ (injected by backend for standalone previews)
|
|
|
+ if (window.__SD_COMPONENT_PRELOAD__ && Array.isArray(window.__SD_COMPONENT_PRELOAD__)) {
|
|
|
+ console.log('[SDComponent] Pre-registering', window.__SD_COMPONENT_PRELOAD__.length, 'preloaded components');
|
|
|
+ window.__SD_COMPONENT_PRELOAD__.forEach(function(comp) {
|
|
|
+ registerComponent(comp.id, comp);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ var refs = findReferencedComponents();
|
|
|
+ var unloaded = refs.filter(function(id) { return !componentRegistry.has(id); });
|
|
|
+
|
|
|
+ if (unloaded.length > 0) {
|
|
|
+ if (DRAFT_ID) {
|
|
|
+ loadComponentsForDraft();
|
|
|
+ } else {
|
|
|
+ // No draft ID — try batch loading the missing IDs directly
|
|
|
+ loadComponentsByIds(unloaded).then(function() {
|
|
|
+ initializeComponents();
|
|
|
+ showPage();
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } else if (refs.length > 0) {
|
|
|
+ // All components already registered (e.g. from preload) — render immediately
|
|
|
+ initializeComponents();
|
|
|
+ showPage();
|
|
|
+ } else {
|
|
|
+ showPage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (document.readyState === 'loading') {
|
|
|
+ document.addEventListener('DOMContentLoaded', bootComponents);
|
|
|
+ } else {
|
|
|
+ bootComponents();
|
|
|
+ }
|
|
|
+
|
|
|
+ var observer = new MutationObserver(function(mutations) {
|
|
|
+ var hasNew = false;
|
|
|
+ mutations.forEach(function(m) {
|
|
|
+ m.addedNodes.forEach(function(node) {
|
|
|
+ if (node.nodeType === 1) {
|
|
|
+ if (node.tagName === 'SD-COMPONENT') { hasNew = true; }
|
|
|
+ else if (node.querySelector) {
|
|
|
+ if (node.querySelectorAll('sd-component[componentid], sd-component[ref]').length > 0) hasNew = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ if (hasNew) {
|
|
|
+ var refs = findReferencedComponents();
|
|
|
+ var unloaded = refs.filter(function(id) { return !componentRegistry.has(id); });
|
|
|
+ if (unloaded.length > 0) {
|
|
|
+ loadComponentsByIds(unloaded).then(function() {
|
|
|
+ initializeComponents();
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ initializeComponents();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ observer.observe(document.body, { childList: true, subtree: true });
|
|
|
+
|
|
|
+ console.log('[SDComponent] Runtime initialized');
|
|
|
+ })();
|
|
|
+ </script>
|
|
|
+</body>
|
|
|
+</html>
|