| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280 |
-
- <!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>
|