- 缂栬緫鍣細椤圭洰鏂板缓/鎵撳紑/淇濆瓨/鍙﹀瓨涓猴紙璁颁綇鏈€鍚庢枃浠跺す锛夈€侀〉闈㈠鍒?鍓嶆彃/绉诲姩/澶嶅埗/鎴浘/椤甸潰閲嶆帓銆丅ase64 棰勮鍥俱€? 椤归〉闈㈠睘鎬?- 鍙戝竷鍣細棰勮鏈〉/鍗曞睆鍙戝竷(杞挱)/澶氬睆鍙戝竷(alsoMain)/鍋滄杞挱/鍏抽棴鎵€鏈?妫€娴嬪睆骞曪紝蹇嵎閿綋绯?1-9闀挎寜闃堝€?鈫戔啌/Esc/F5/閿佸畾閿? - 閰嶇疆鍣細椤圭洰淇℃伅/閬ユ帶璁剧疆/蹇嵎鏂瑰紡x3/鍙戝竷璁剧疆/璁惧娴嬭瘯/鍏充簬 6 鑿滃崟 - 鏁版嵁瀹夊叏锛欽SON 宸ョ▼ AES-256-GCM 瀵嗙爜鍔犲瘑 - 鐗堟湰鍙峰崟涓€鏉ユ簮(package.json)锛岀豢鑹茬増 zip 鎵撳寘(electron-builder) - 闄勫甫锛氶渶姹傛枃妗?v2.0銆佹寚浠ゆ竻鍗曘€乸rototype 澶у睆鍘熷瀷
1229 lines
64 KiB
HTML
1229 lines
64 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>智慧农业大数据可视化控制中心 — 前端原型(Office Ribbon)</title>
|
||
<style>
|
||
/* ============== 主题变量 ============== */
|
||
:root {
|
||
--blue-1: #f0f5fa; /* 最浅 */
|
||
--blue-2: #e1ecf8;
|
||
--blue-3: #cfdfee;
|
||
--blue-4: #b9d2eb;
|
||
--blue-5: #8eb1d7;
|
||
--blue-6: #4a7fc1; /* Office 蓝 */
|
||
--blue-7: #2a5d8a;
|
||
--blue-8: #1c3d62;
|
||
--line: #b3c5dd;
|
||
--line-lt:#d8e2ee;
|
||
--bg: #ffffff;
|
||
--panel: #f0f3f8;
|
||
--panel-2:#e6eef8;
|
||
--text: #1c3d62;
|
||
--text-dim:#5a7395;
|
||
--orange: #ed7d31;
|
||
--orange-bg: #fff5e8;
|
||
--green: #5fa15f;
|
||
--red: #d04a4a;
|
||
--gray: #8a9aa8;
|
||
}
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
html, body { height: 100%; }
|
||
body {
|
||
font-family: "Microsoft YaHei", "Segoe UI", system-ui, sans-serif;
|
||
background: #d8e2ee;
|
||
color: var(--text);
|
||
display: flex; align-items: center; justify-content: center; padding: 16px;
|
||
}
|
||
|
||
/* ============== WinForm 窗口外壳 ============== */
|
||
.app {
|
||
width: 1500px; height: 900px; max-width: 100vw; max-height: 100vh;
|
||
background: var(--bg); border: 1px solid var(--blue-5);
|
||
box-shadow: 0 12px 40px rgba(0,0,0,.25);
|
||
display: flex; flex-direction: column; overflow: hidden;
|
||
}
|
||
|
||
/* ============== 标题栏 ============== */
|
||
.titlebar {
|
||
height: 30px; display: flex; align-items: center; justify-content: space-between;
|
||
background: linear-gradient(180deg, #e9f0f8, #c1d4ec);
|
||
border-bottom: 1px solid var(--line);
|
||
padding: 0 10px; font-size: 12.5px; color: var(--text);
|
||
}
|
||
.titlebar .left { display: flex; align-items: center; gap: 8px; }
|
||
.titlebar .ico { width: 18px; height: 18px; border-radius: 3px; background: linear-gradient(135deg, #4a7fc1, #2a5d8a); display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px; }
|
||
.titlebar .winbtns { display: flex; gap: 2px; }
|
||
.titlebar .winbtns .b { width: 28px; height: 22px; display: flex; align-items: center; justify-content: center; cursor: pointer; color: var(--text); font-size: 12px; }
|
||
.titlebar .winbtns .b:hover { background: rgba(255,255,255,.5); }
|
||
.titlebar .winbtns .b.close:hover { background: #e81123; color: #fff; }
|
||
|
||
/* ============== 菜单栏 ============== */
|
||
.menubar { display: flex; height: 26px; background: linear-gradient(180deg, #d8e6f5, #b9d2eb); border-bottom: 1px solid var(--line); padding: 0 6px; gap: 1px; }
|
||
.menubar .m { padding: 0 14px; display: flex; align-items: center; cursor: pointer; color: var(--text); font-size: 13px; }
|
||
.menubar .m:hover { background: rgba(255,255,255,.5); }
|
||
.menubar .m.active { background: #fff; border: 1px solid var(--line); border-bottom-color: #fff; }
|
||
|
||
/* ============== Ribbon ============== */
|
||
.ribbon-tabs {
|
||
display: flex; align-items: flex-end; gap: 2px;
|
||
height: 28px; padding: 0 8px;
|
||
background: linear-gradient(180deg, #cfdfee, #a9c4e0);
|
||
border-bottom: 1px solid var(--line);
|
||
}
|
||
.ribbon-tab {
|
||
padding: 0 16px; height: 22px; line-height: 22px;
|
||
cursor: pointer; color: var(--text); font-size: 13px;
|
||
border-radius: 3px 3px 0 0;
|
||
border: 1px solid transparent; border-bottom: none;
|
||
}
|
||
.ribbon-tab:hover { background: rgba(255,255,255,.4); }
|
||
.ribbon-tab.active {
|
||
background: linear-gradient(180deg, #f6f9fc, #e1ecf8);
|
||
border-color: var(--line);
|
||
font-weight: 600; position: relative; top: 1px;
|
||
border-bottom: 1px solid #e1ecf8;
|
||
}
|
||
|
||
.ribbon-content {
|
||
background: linear-gradient(180deg, #f0f3f8, #d8e2ee);
|
||
border-bottom: 1px solid var(--line);
|
||
padding: 4px 8px 6px;
|
||
min-height: 96px;
|
||
}
|
||
.ribbon-panel { display: none; align-items: stretch; gap: 6px; }
|
||
.ribbon-panel.active { display: flex; }
|
||
|
||
.group {
|
||
display: flex; flex-direction: column;
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
background: rgba(255,255,255,.4);
|
||
min-height: 92px;
|
||
}
|
||
.group-header {
|
||
display: flex; align-items: center; gap: 4px;
|
||
padding: 2px 8px;
|
||
background: linear-gradient(180deg, #e6eef8, #d3e0ef);
|
||
border-bottom: 1px solid var(--line);
|
||
font-size: 12px; color: var(--text);
|
||
border-radius: 3px 3px 0 0;
|
||
}
|
||
.group-header .gh-ic { color: var(--blue-6); font-size: 12px; }
|
||
.group-content { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 2px; padding: 4px 6px; flex: 1; }
|
||
|
||
.rb {
|
||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||
padding: 4px 6px; min-width: 48px; cursor: pointer; user-select: none;
|
||
border: 1px solid transparent; border-radius: 3px;
|
||
background: transparent;
|
||
}
|
||
.rb:hover { background: linear-gradient(180deg, #fdfdfe, #e1ecf8); border-color: #c0d4ea; }
|
||
.rb:active { background: #c4dbf3; }
|
||
.rb .ic { font-size: 22px; line-height: 1; }
|
||
.rb .lb { font-size: 12px; margin-top: 2px; color: var(--text); }
|
||
.rb.large { min-width: 56px; padding: 4px 8px; }
|
||
.rb.large .ic { font-size: 26px; }
|
||
.rb.small { flex-direction: row; min-width: auto; padding: 6px 10px; gap: 6px; }
|
||
.rb.small .ic { font-size: 16px; }
|
||
.rb.small .lb { font-size: 12px; margin-top: 0; }
|
||
.rb.danger:hover { background: #fde6e6; border-color: #e8a8a8; }
|
||
.rb.danger .ic, .rb.danger .lb { color: var(--red); }
|
||
.rb.primary .ic { color: var(--blue-6); }
|
||
.rb:disabled { opacity: .45; cursor: not-allowed; }
|
||
|
||
.group-sep { width: 1px; background: var(--line); margin: 4px 2px; }
|
||
|
||
/* ============== 主区 ============== */
|
||
.main { flex: 1; display: flex; overflow: hidden; background: #fff; }
|
||
|
||
/* 左侧 */
|
||
.pane-left { width: 220px; flex: 0 0 220px; background: var(--panel); border-right: 1px solid var(--line); display: flex; flex-direction: column; }
|
||
.pane-head { display: flex; background: linear-gradient(180deg, #e6eef8, #d3e0ef); border-bottom: 1px solid var(--line); }
|
||
.pane-head .tab { flex: 1; text-align: center; padding: 7px 4px; cursor: pointer; font-size: 12.5px; color: var(--text); border-right: 1px solid var(--line); }
|
||
.pane-head .tab:last-child { border-right: none; }
|
||
.pane-head .tab.active { background: #fff; font-weight: 600; color: var(--blue-6); border-bottom: 2px solid var(--blue-6); margin-bottom: -1px; }
|
||
.pane-body { flex: 1; overflow: auto; }
|
||
|
||
.pagelist { padding: 8px; display: flex; flex-direction: column; gap: 8px; }
|
||
.pcard {
|
||
background: #fff; border: 1px solid var(--line-lt); border-radius: 3px;
|
||
cursor: pointer; overflow: hidden; position: relative;
|
||
}
|
||
.pcard:hover { border-color: var(--blue-5); }
|
||
.pcard.active { border: 2px solid var(--orange); background: var(--orange-bg); }
|
||
.pcard .thumb { width: 100%; aspect-ratio: 16 / 10; background: linear-gradient(160deg, #0a2530, #062030); position: relative; overflow: hidden; }
|
||
.pcard .thumb .bar { position: absolute; bottom: 6px; width: 8px; border-radius: 2px; }
|
||
.pcard .thumb .line { position: absolute; height: 2px; }
|
||
.pcard .thumb .dot { position: absolute; width: 5px; height: 5px; border-radius: 50%; }
|
||
.pcard .no {
|
||
position: absolute; right: 4px; bottom: 4px;
|
||
background: rgba(0,0,0,.55); color: #fff;
|
||
padding: 1px 6px; font-size: 11px; border-radius: 2px; font-family: Consolas, monospace;
|
||
}
|
||
.pcard.off { opacity: .55; }
|
||
.pcard .title { font-size: 12px; padding: 4px 6px; color: var(--text); border-top: 1px solid var(--line-lt); }
|
||
|
||
/* 代码视图(左侧) */
|
||
.code-left { padding: 8px; font-family: Consolas, monospace; font-size: 12px; line-height: 1.5; color: var(--text); white-space: pre-wrap; word-break: break-all; }
|
||
|
||
/* 中间画布 */
|
||
.pane-center { flex: 1; background: #000; display: flex; flex-direction: column; }
|
||
.canvas-head { height: 32px; display: flex; align-items: center; gap: 10px; padding: 0 10px; background: var(--panel-2); border-bottom: 1px solid var(--line); font-size: 12.5px; }
|
||
.canvas-head .pill { background: #fff; border: 1px solid var(--line); border-radius: 3px; padding: 2px 8px; font-size: 12px; }
|
||
.canvas-head .url { color: var(--text-dim); font-family: Consolas, monospace; font-size: 11.5px; background: #fff; border: 1px solid var(--line); border-radius: 3px; padding: 2px 8px; }
|
||
.canvas-head .zoom-ctrl { margin-left: auto; display: flex; align-items: center; gap: 4px; }
|
||
.zoom-ctrl .z { background: #fff; border: 1px solid var(--line); padding: 2px 8px; font-size: 12px; cursor: pointer; }
|
||
.zoom-ctrl .z:hover { background: var(--blue-2); }
|
||
.zoom-ctrl .v { background: #fff; border: 1px solid var(--line); padding: 2px 10px; font-size: 12px; color: var(--blue-6); font-family: Consolas, monospace; min-width: 50px; text-align: center; }
|
||
.canvas-body { flex: 1; display: flex; align-items: center; justify-content: center; padding: 16px; overflow: hidden; position: relative; }
|
||
.canvas-frame { width: 100%; height: 100%; border: 1px solid #2a3a4a; transform-origin: center center; }
|
||
.canvas-frame iframe { width: 100%; height: 100%; border: 0; }
|
||
|
||
/* 代码视图(中央) */
|
||
.center-code { flex: 1; background: #1e1e1e; color: #d4d4d4; font-family: Consolas, monospace; font-size: 12.5px; padding: 12px 16px; overflow: auto; line-height: 1.6; }
|
||
.center-code .k { color: #569cd6; }
|
||
.center-code .s { color: #ce9178; }
|
||
.center-code .n { color: #b5cea8; }
|
||
.center-code .b { color: #569cd6; font-weight: 600; }
|
||
|
||
/* 右侧 */
|
||
.pane-right { width: 280px; flex: 0 0 280px; background: var(--panel); border-left: 1px solid var(--line); display: flex; flex-direction: column; }
|
||
.props { padding: 8px; display: flex; flex-direction: column; gap: 8px; }
|
||
.pgrp {
|
||
background: #fff; border: 1px solid var(--line-lt); border-radius: 3px;
|
||
padding: 6px 8px;
|
||
}
|
||
.pgrp-title { font-size: 12px; color: var(--blue-6); font-weight: 600; margin-bottom: 6px; letter-spacing: .5px; }
|
||
.pfield { display: flex; flex-direction: column; gap: 3px; margin-bottom: 6px; }
|
||
.pfield:last-child { margin-bottom: 0; }
|
||
.pfield label { font-size: 11.5px; color: var(--text-dim); display: flex; align-items: center; gap: 4px; }
|
||
.pfield label .req { color: var(--orange); }
|
||
.pfield .hint { font-size: 10.5px; color: var(--gray); }
|
||
.pfield .row { display: flex; align-items: center; gap: 6px; }
|
||
.pfield .input, .pfield .select, .pfield textarea {
|
||
width: 100%; padding: 4px 6px; background: #fff; color: var(--text);
|
||
border: 1px solid var(--line-lt); border-radius: 3px; font-size: 12.5px; outline: none;
|
||
font-family: inherit;
|
||
}
|
||
.pfield .input:focus, .pfield .select:focus, .pfield textarea:focus { border-color: var(--blue-6); }
|
||
.pfield textarea { resize: vertical; min-height: 48px; line-height: 1.5; }
|
||
.pfield .input:disabled { background: var(--blue-1); color: var(--text-dim); }
|
||
|
||
.switch { position: relative; width: 36px; height: 18px; display: inline-block; flex: 0 0 auto; }
|
||
.switch input { display: none; }
|
||
.switch .slider { position: absolute; inset: 0; background: #c0c0c0; border-radius: 18px; cursor: pointer; transition: .2s; }
|
||
.switch .slider::before { content: ""; position: absolute; width: 14px; height: 14px; border-radius: 50%; background: #fff; top: 2px; left: 2px; transition: .2s; box-shadow: 0 1px 3px rgba(0,0,0,.3); }
|
||
.switch input:checked + .slider { background: var(--blue-6); }
|
||
.switch input:checked + .slider::before { transform: translateX(18px); }
|
||
|
||
/* 属性树 */
|
||
.proptree { padding: 6px 8px; font-size: 12.5px; }
|
||
.ptrow { display: flex; align-items: center; gap: 4px; padding: 2px 0; line-height: 1.5; }
|
||
.ptrow:hover { background: var(--blue-1); }
|
||
.ptrow .arr { width: 12px; color: var(--blue-6); cursor: pointer; user-select: none; text-align: center; }
|
||
.ptrow .name { color: var(--blue-6); flex: 0 0 auto; min-width: 100px; }
|
||
.ptrow .val { color: var(--text); flex: 1; word-break: break-all; }
|
||
.ptrow .val.true { color: var(--green); }
|
||
.ptrow .val.false { color: var(--red); }
|
||
.ptrow .val.string { color: #a05050; }
|
||
|
||
/* 底部 GroupBox(PageDescribe) */
|
||
.gbox { margin: 8px; border: 1px solid var(--line-lt); border-radius: 3px; background: #fff; }
|
||
.gbox-title { padding: 3px 10px; font-size: 12px; color: var(--text); background: linear-gradient(180deg, #f0f3f8, #d8e2ee); border-bottom: 1px solid var(--line-lt); }
|
||
.gbox-body { padding: 6px 8px; min-height: 60px; font-size: 12.5px; color: var(--text); line-height: 1.5; }
|
||
|
||
/* ============== 发布 Tab ============== */
|
||
.publish-main { flex: 1; display: flex; overflow: hidden; }
|
||
.pub-left { width: 320px; flex: 0 0 320px; background: var(--panel); border-right: 1px solid var(--line); overflow: auto; padding: 10px; display: flex; flex-direction: column; gap: 10px; }
|
||
.pub-right { flex: 1; display: flex; flex-direction: column; background: #000; }
|
||
.pub-preview-head { height: 36px; display: flex; align-items: center; gap: 8px; padding: 0 12px; background: linear-gradient(180deg, #1a1f2a, #0d121a); border-bottom: 1px solid #2a3a4a; }
|
||
.pub-preview-head .ttl { color: #fff; font-size: 13px; font-weight: 600; }
|
||
.pub-preview-head .badge { background: var(--blue-6); color: #fff; font-size: 11px; padding: 2px 8px; border-radius: 3px; }
|
||
.pub-preview-head .ctrl { margin-left: auto; display: flex; align-items: center; gap: 6px; }
|
||
.pub-preview-head .ctrl .btn-mini { background: #1f2a3a; color: #cbd5e1; border: 1px solid #2a3a4a; padding: 3px 10px; font-size: 12px; cursor: pointer; border-radius: 3px; }
|
||
.pub-preview-head .ctrl .btn-mini:hover { background: #2a3a4a; color: #fff; }
|
||
.pub-preview-head .ctrl .btn-mini.primary { background: var(--blue-6); border-color: var(--blue-6); color: #fff; }
|
||
.pub-preview-head .ctrl select { background: #1f2a3a; color: #cbd5e1; border: 1px solid #2a3a4a; padding: 3px 8px; font-size: 12px; border-radius: 3px; }
|
||
.pub-preview-body { flex: 1; display: flex; align-items: center; justify-content: center; padding: 20px; position: relative; }
|
||
.pub-preview-frame { width: 100%; height: 100%; border: 2px solid #2a3a4a; background: #08131f; box-shadow: 0 10px 50px rgba(0,0,0,.5); }
|
||
.pub-preview-frame iframe { width: 100%; height: 100%; border: 0; }
|
||
|
||
.pub-card { background: #fff; border: 1px solid var(--line-lt); border-radius: 4px; }
|
||
.pub-card .ch { padding: 6px 10px; font-size: 13px; font-weight: 600; color: var(--blue-7); background: linear-gradient(180deg, #f0f3f8, #d8e2ee); border-bottom: 1px solid var(--line-lt); display: flex; align-items: center; gap: 6px; }
|
||
.pub-card .cb { padding: 10px; }
|
||
.pub-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; flex-wrap: wrap; }
|
||
.pub-row:last-child { margin-bottom: 0; }
|
||
.pub-row .lbl { font-size: 12px; color: var(--text-dim); min-width: 60px; }
|
||
.pub-row .grow { flex: 1; min-width: 120px; }
|
||
.pub-row .input, .pub-row .select {
|
||
width: 100%; padding: 4px 6px; background: #fff; color: var(--text);
|
||
border: 1px solid var(--line-lt); border-radius: 3px; font-size: 12.5px; outline: none; font-family: inherit;
|
||
}
|
||
.pub-row .input:focus, .pub-row .select:focus { border-color: var(--blue-6); }
|
||
.pub-row .btn { padding: 4px 10px; font-size: 12px; background: var(--blue-2); border: 1px solid var(--line); border-radius: 3px; cursor: pointer; color: var(--text); }
|
||
.pub-row .btn:hover { background: var(--blue-3); border-color: var(--blue-5); }
|
||
.pub-row .btn.primary { background: var(--blue-6); border-color: var(--blue-6); color: #fff; }
|
||
.pub-row .btn.primary:hover { background: var(--blue-7); }
|
||
.pub-row .btn.danger { background: #fde6e6; border-color: #e8a8a8; color: var(--red); }
|
||
.pub-row .btn.danger:hover { background: #facfcf; }
|
||
.pub-hint { font-size: 11.5px; color: var(--text-dim); margin-top: 4px; }
|
||
|
||
.screen-list { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
||
.scard {
|
||
background: #fff; border: 1px solid var(--line-lt); border-radius: 3px;
|
||
padding: 6px 8px; cursor: pointer; position: relative;
|
||
}
|
||
.scard:hover { border-color: var(--blue-5); }
|
||
.scard.active { border-color: var(--blue-6); background: #f0f6fc; }
|
||
.scard .sno { font-size: 12px; font-weight: 600; color: var(--blue-7); }
|
||
.scard .sres { font-size: 10.5px; color: var(--text-dim); margin-top: 2px; }
|
||
.scard .splay { font-size: 11px; color: var(--green); margin-top: 3px; }
|
||
.scard .splay.off { color: var(--gray); }
|
||
.scard .splay.err { color: var(--red); }
|
||
|
||
/* ============== 配置 Tab ============== */
|
||
.config-main { flex: 1; padding: 14px; overflow: auto; background: var(--bg); display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; align-content: start; }
|
||
.cg { background: #fff; border: 1px solid var(--line-lt); border-radius: 4px; }
|
||
.cg .cg-title { padding: 8px 12px; font-size: 14px; font-weight: 600; color: var(--blue-7); background: linear-gradient(180deg, #f0f3f8, #d8e2ee); border-bottom: 1px solid var(--line-lt); display: flex; align-items: center; gap: 6px; }
|
||
.cg .cg-body { padding: 8px 12px; }
|
||
.cg-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 0; border-bottom: 1px dashed var(--line-lt); }
|
||
.cg-item:last-child { border-bottom: 0; }
|
||
.cg-item .ci-l { display: flex; align-items: center; gap: 10px; }
|
||
.cg-item .ci-ic { width: 32px; height: 32px; border-radius: 4px; background: var(--blue-1); border: 1px solid var(--line-lt); display: flex; align-items: center; justify-content: center; font-size: 16px; color: var(--blue-6); }
|
||
.cg-item .ci-name { font-size: 13px; }
|
||
.cg-item .ci-desc { font-size: 11.5px; color: var(--text-dim); margin-top: 2px; }
|
||
.cg-item .ci-btn { padding: 4px 12px; font-size: 12px; background: var(--blue-2); border: 1px solid var(--line); border-radius: 3px; cursor: pointer; color: var(--text); }
|
||
.cg-item .ci-btn:hover { background: var(--blue-3); border-color: var(--blue-5); }
|
||
|
||
/* ============== 状态栏 ============== */
|
||
.statusbar { height: 24px; display: flex; align-items: center; padding: 0 10px; background: linear-gradient(180deg, #e9f0f8, #c1d4ec); border-top: 1px solid var(--line); font-size: 12px; color: var(--text); gap: 14px; }
|
||
.statusbar .seg { display: flex; align-items: center; gap: 4px; }
|
||
.statusbar .sep { width: 1px; height: 14px; background: var(--line); }
|
||
.statusbar .right { margin-left: auto; display: flex; align-items: center; gap: 14px; }
|
||
|
||
/* ============== Toast ============== */
|
||
.toast-box { position: fixed; top: 16px; right: 16px; z-index: 999; display: flex; flex-direction: column; gap: 8px; }
|
||
.toast {
|
||
background: #fff; border: 1px solid var(--line); color: var(--text);
|
||
padding: 10px 14px; border-radius: 4px; font-size: 12.5px; min-width: 220px;
|
||
box-shadow: 0 6px 20px rgba(0,0,0,.15); display: flex; align-items: center; gap: 8px;
|
||
animation: slideIn .25s ease;
|
||
}
|
||
.toast .t-ic { font-size: 16px; }
|
||
.toast.info .t-ic { color: var(--blue-6); }
|
||
.toast.ok .t-ic { color: var(--green); }
|
||
.toast.warn .t-ic { color: var(--orange); }
|
||
@keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: none; opacity: 1; } }
|
||
.toast.out { animation: slideOut .25s ease forwards; }
|
||
@keyframes slideOut { to { transform: translateX(20px); opacity: 0; } }
|
||
|
||
/* 预览浮层 */
|
||
.preview-mask { position: fixed; inset: 0; background: rgba(0,0,0,.75); z-index: 500; display: none; align-items: center; justify-content: center; }
|
||
.preview-mask.active { display: flex; }
|
||
.preview-frame { width: 80vw; height: 86vh; background: #08131f; border: 1px solid var(--blue-5); border-radius: 6px; overflow: hidden; box-shadow: 0 30px 100px rgba(0,0,0,.7); display: flex; flex-direction: column; }
|
||
.preview-frame .pf-head { height: 38px; background: linear-gradient(180deg, #1a1f2a, #0d121a); display: flex; align-items: center; justify-content: space-between; padding: 0 14px; color: #fff; }
|
||
.preview-frame iframe { flex: 1; width: 100%; border: 0; }
|
||
|
||
.empty { color: var(--text-dim); font-size: 12.5px; text-align: center; padding: 24px 0; }
|
||
|
||
/* 滚动条 */
|
||
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||
::-webkit-scrollbar-track { background: var(--blue-1); }
|
||
::-webkit-scrollbar-thumb { background: var(--blue-4); border-radius: 4px; }
|
||
::-webkit-scrollbar-thumb:hover { background: var(--blue-5); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="app">
|
||
<!-- ===== 标题栏 ===== -->
|
||
<div class="titlebar">
|
||
<div class="left">
|
||
<span class="ico">🌾</span>
|
||
<span id="winTitle">智慧农业大数据可视化控制中心 — 大屏可视化编辑发布工具</span>
|
||
</div>
|
||
<div class="winbtns">
|
||
<div class="b" title="最小化" onclick="toast('最小化(原型)','info')">─</div>
|
||
<div class="b" title="最大化" onclick="toast('最大化(原型)','info')">▢</div>
|
||
<div class="b close" title="关闭" onclick="toast('关闭窗口(原型)','warn')">✕</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 菜单栏 ===== -->
|
||
<div class="menubar">
|
||
<div class="m">项目</div>
|
||
<div class="m">发布</div>
|
||
<div class="m">管理</div>
|
||
<div class="m" style="margin-left:auto;color:var(--text-dim);">⚙ 设置</div>
|
||
<div class="m">帮助</div>
|
||
</div>
|
||
|
||
<!-- ===== Ribbon ===== -->
|
||
<div class="ribbon-tabs">
|
||
<div class="ribbon-tab active" data-rtab="edit">编辑</div>
|
||
<div class="ribbon-tab" data-rtab="publish">发布</div>
|
||
<div class="ribbon-tab" data-rtab="config">配置</div>
|
||
</div>
|
||
<div class="ribbon-content">
|
||
|
||
<!-- 编辑 Ribbon -->
|
||
<div class="ribbon-panel active" data-rtab="edit">
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">▤</span>项目操作</div>
|
||
<div class="group-content">
|
||
<button class="rb large" onclick="toast('新建项目(原型)','info')"><span class="ic">📄</span><span class="lb">新建</span></button>
|
||
<button class="rb large" onclick="toast('打开 .json 项目文件(原型)','info')"><span class="ic">📂</span><span class="lb">打开</span></button>
|
||
<button class="rb large primary" onclick="saveProject()"><span class="ic">💾</span><span class="lb">保存</span></button>
|
||
<button class="rb large" onclick="toast('另存为(原型)','info')"><span class="ic">📋</span><span class="lb">另存为</span></button>
|
||
<div class="group-sep"></div>
|
||
<button class="rb large" onclick="toast('导入项目(原型)','info')"><span class="ic">⬇</span><span class="lb">导入</span></button>
|
||
<button class="rb large" onclick="exportProject()"><span class="ic">⬆</span><span class="lb">导出</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">▦</span>页面操作</div>
|
||
<div class="group-content">
|
||
<button class="rb large primary" onclick="addPage()"><span class="ic">+</span><span class="lb">新建页面</span></button>
|
||
<button class="rb large" onclick="dupPage()"><span class="ic">📑</span><span class="lb">复制</span></button>
|
||
<button class="rb large" onclick="movePage(-1)"><span class="ic">▲</span><span class="lb">上移</span></button>
|
||
<button class="rb large" onclick="movePage(1)"><span class="ic">▼</span><span class="lb">下移</span></button>
|
||
<button class="rb large danger" onclick="delPage()"><span class="ic">🗑</span><span class="lb">删除</span></button>
|
||
<div class="group-sep"></div>
|
||
<button class="rb large" onclick="resetProject()"><span class="ic">↻</span><span class="lb">重置</span></button>
|
||
<button class="rb large" onclick="initBlank()"><span class="ic">◫</span><span class="lb">初始化空白页</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">◐</span>视图</div>
|
||
<div class="group-content">
|
||
<button class="rb large active" id="rbViewPage" onclick="switchView('page')"><span class="ic">🖼</span><span class="lb">页面视图</span></button>
|
||
<button class="rb large" id="rbViewCode" onclick="switchView('code')"><span class="ic">{ }</span><span class="lb">代码视图</span></button>
|
||
<div class="group-sep"></div>
|
||
<button class="rb large" onclick="zoomCanvas(-10)"><span class="ic">🔍-</span><span class="lb">缩小</span></button>
|
||
<button class="rb large" onclick="zoomCanvas(10)"><span class="ic">🔍+</span><span class="lb">放大</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 发布 Ribbon -->
|
||
<div class="ribbon-panel" data-rtab="publish">
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">🔎</span>单页预览</div>
|
||
<div class="group-content">
|
||
<button class="rb large" onclick="detectScreens()"><span class="ic">🖥</span><span class="lb">检测屏幕</span></button>
|
||
<button class="rb large" onclick="previewToScreen()"><span class="ic">📡</span><span class="lb">预览到屏幕</span></button>
|
||
<button class="rb large primary" onclick="openPreview()"><span class="ic">👁</span><span class="lb">预览本页</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">▣</span>单屏发布</div>
|
||
<div class="group-content">
|
||
<button class="rb large primary" onclick="publishSingle()"><span class="ic">📤</span><span class="lb">发布到屏幕</span></button>
|
||
<button class="rb large danger" onclick="closeScreen()"><span class="ic">✖</span><span class="lb">关闭页面</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">▦</span>多屏发布</div>
|
||
<div class="group-content">
|
||
<button class="rb large primary" onclick="publishAll()"><span class="ic">🗗</span><span class="lb">多屏发布</span></button>
|
||
<button class="rb large danger" onclick="closeAll()"><span class="ic">⛔</span><span class="lb">关闭所有页面</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 配置 Ribbon -->
|
||
<div class="ribbon-panel" data-rtab="config">
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">▤</span>项目集</div>
|
||
<div class="group-content">
|
||
<button class="rb large" onclick="toast('项目集管理(原型)','info')"><span class="ic">🗂</span><span class="lb">项目集管理</span></button>
|
||
<button class="rb large" onclick="document.getElementById('cfgOffline').click()"><span class="ic">📴</span><span class="lb">离线模式</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">🔧</span>设备调试</div>
|
||
<div class="group-content">
|
||
<button class="rb large" onclick="beep()"><span class="ic">🔊</span><span class="lb">声音测试</span></button>
|
||
<button class="rb large" onclick="keyTest()"><span class="ic">⌨</span><span class="lb">键盘测试</span></button>
|
||
<button class="rb large" onclick="toast('更多调试选项(原型)','info')"><span class="ic">🛠</span><span class="lb">更多调试</span></button>
|
||
</div>
|
||
</div>
|
||
<div class="group">
|
||
<div class="group-header"><span class="gh-ic">▦</span>更多工具</div>
|
||
<div class="group-content">
|
||
<button class="rb large" onclick="toast('已创建桌面快捷方式(原型)','ok')"><span class="ic">🖱</span><span class="lb">桌面快捷方式</span></button>
|
||
<button class="rb large" onclick="toast('打开帮助文档(原型)','info')"><span class="ic">📖</span><span class="lb">帮助文档</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 主区 ===== -->
|
||
<div class="main">
|
||
|
||
<!-- ============ 编辑 主区 ============ -->
|
||
<div id="main-edit" style="flex:1; display:flex; overflow:hidden;">
|
||
<!-- 左侧页面列表 -->
|
||
<div class="pane-left">
|
||
<div class="pane-head">
|
||
<div class="tab active" data-ltab="page" onclick="switchLeftTab('page')">页面视图</div>
|
||
<div class="tab" data-ltab="code" onclick="switchLeftTab('code')">代码视图</div>
|
||
</div>
|
||
<div class="pane-body" id="leftBody">
|
||
<div class="pagelist" id="pageList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 中央画布 -->
|
||
<div class="pane-center" id="centerPage">
|
||
<div class="canvas-head">
|
||
<span class="pill" id="cvScreen">屏幕 1</span>
|
||
<span class="pill" style="color:var(--blue-6);font-weight:600;" id="cvTitle">农业数据总览</span>
|
||
<span class="url" id="cvUrl">sample-screen.html</span>
|
||
<div class="zoom-ctrl">
|
||
<span style="color:var(--text-dim);font-size:12px;">缩放</span>
|
||
<div class="z" onclick="zoomCanvas(-10)">-</div>
|
||
<div class="v" id="zoomVal">100%</div>
|
||
<div class="z" onclick="zoomCanvas(10)">+</div>
|
||
</div>
|
||
</div>
|
||
<div class="canvas-body">
|
||
<div class="canvas-frame" id="canvasFrame" style="transform:scale(1);">
|
||
<iframe id="screenIframe" src="sample-screen.html?no=1&title=%E5%86%9C%E4%B8%9A%E6%95%B0%E6%8D%AE%E6%80%BB%E8%A7%88"></iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="center-code" id="centerCode" style="display:none;"><pre id="codeContent"></pre></div>
|
||
|
||
<!-- 右侧属性 -->
|
||
<div class="pane-right">
|
||
<div class="pane-head">
|
||
<div class="tab active" data-rtab="page" onclick="switchRightTab('page')">页面视图</div>
|
||
<div class="tab" data-rtab="code" onclick="switchRightTab('code')">代码视图</div>
|
||
</div>
|
||
<div class="pane-body" id="rightBody">
|
||
<div class="props" id="propsForm"></div>
|
||
<div class="proptree" id="propsTree" style="display:none;"></div>
|
||
<div class="gbox">
|
||
<div class="gbox-title">PageDescribe</div>
|
||
<div class="gbox-body" id="describeBody">—</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============ 发布 主区(支持预览) ============ -->
|
||
<div id="main-publish" style="flex:1; display:none;">
|
||
<div class="publish-main">
|
||
<!-- 左:控制面板 -->
|
||
<div class="pub-left">
|
||
<div class="pub-card">
|
||
<div class="ch">🖥 屏幕列表</div>
|
||
<div class="cb">
|
||
<div class="pub-row" style="margin-bottom:8px;">
|
||
<button class="btn primary" onclick="detectScreens()" style="flex:1;">🔍 检测屏幕</button>
|
||
</div>
|
||
<div class="screen-list" id="screenList">
|
||
<div class="empty" style="grid-column:1/-1;">尚未检测屏幕</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="pub-card">
|
||
<div class="ch">▣ 单屏发布</div>
|
||
<div class="cb">
|
||
<div class="pub-row">
|
||
<span class="lbl">屏幕</span>
|
||
<div class="grow"><select class="select" id="singleScreen"></select></div>
|
||
</div>
|
||
<div class="pub-row">
|
||
<button class="btn primary" onclick="publishSingle()" style="flex:1;">📤 发布到屏幕</button>
|
||
<button class="btn danger" onclick="closeScreen()">✖ 关闭</button>
|
||
</div>
|
||
<div class="pub-hint">将当前选中页面推送到指定屏幕</div>
|
||
</div>
|
||
</div>
|
||
<div class="pub-card">
|
||
<div class="ch">▦ 多屏发布</div>
|
||
<div class="cb">
|
||
<div class="pub-row">
|
||
<span class="lbl">主屏</span>
|
||
<div class="grow"><select class="select" id="mainScreen"></select></div>
|
||
</div>
|
||
<div class="pub-row">
|
||
<button class="btn primary" onclick="publishAll()" style="flex:1;">🗗 多屏发布</button>
|
||
<button class="btn danger" onclick="closeAll()">⛔ 关闭所有</button>
|
||
</div>
|
||
<div class="pub-hint">按页面 ScreenIndex 自动分发到对应屏幕</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- 右:大屏预览 -->
|
||
<div class="pub-right">
|
||
<div class="pub-preview-head">
|
||
<span class="ttl">大屏预览</span>
|
||
<span class="badge" id="pvBadge">屏幕 1</span>
|
||
<span style="color:#a0bcd6;font-size:12px;" id="pvCurPage">— 农业数据总览 —</span>
|
||
<div class="ctrl">
|
||
<span style="color:#a0bcd6;font-size:12px;">预览对象</span>
|
||
<select id="pvTarget" onchange="updatePreview()"></select>
|
||
<button class="btn-mini primary" onclick="openPreview()">⛶ 全屏预览</button>
|
||
<button class="btn-mini" onclick="refreshPreview()">↻ 刷新</button>
|
||
</div>
|
||
</div>
|
||
<div class="pub-preview-body">
|
||
<div class="pub-preview-frame">
|
||
<iframe id="pvIframe" src="sample-screen.html?no=1&title=%E5%86%9C%E4%B8%9A%E6%95%B0%E6%8D%AE%E6%80%BB%E8%A7%88"></iframe>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ============ 配置 主区 ============ -->
|
||
<div id="main-config" style="flex:1; display:none;">
|
||
<div class="config-main">
|
||
<div class="cg">
|
||
<div class="cg-title">🗂 项目集</div>
|
||
<div class="cg-body">
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">📂</div><div><div class="ci-name">项目集管理</div><div class="ci-desc">管理大屏工程集合</div></div></div>
|
||
<button class="ci-btn" onclick="toast('打开项目集管理(原型)','info')">管理</button>
|
||
</div>
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">📴</div><div><div class="ci-name">离线模式</div><div class="ci-desc">无网络环境下使用本地工程</div></div></div>
|
||
<label class="switch"><input type="checkbox" id="cfgOffline" onchange="offlineToggle(this)"><span class="slider"></span></label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="cg">
|
||
<div class="cg-title">🔧 设备调试组</div>
|
||
<div class="cg-body">
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">🔊</div><div><div class="ci-name">声音测试</div><div class="ci-desc">测试大屏音频输出</div></div></div>
|
||
<button class="ci-btn" onclick="beep()">测试</button>
|
||
</div>
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">⌨</div><div><div class="ci-name">键盘测试</div><div class="ci-desc">数字键 1-9 切换 ControllerIndex 对应页面</div></div></div>
|
||
<button class="ci-btn" onclick="keyTest()">测试</button>
|
||
</div>
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">🛠</div><div><div class="ci-name">更多调试选项</div><div class="ci-desc">高级诊断与日志</div></div></div>
|
||
<button class="ci-btn" onclick="toast('更多调试选项(原型)','info')">打开</button>
|
||
</div>
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">🎮</div><div><div class="ci-name">遥控器适配</div><div class="ci-desc">对接硬件遥控器操作大屏</div></div></div>
|
||
<label class="switch"><input type="checkbox" onchange="toast(this.checked?'已开启遥控器适配':'已关闭遥控器适配',this.checked?'ok':'info')"><span class="slider"></span></label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="cg">
|
||
<div class="cg-title">🧰 更多工具</div>
|
||
<div class="cg-body">
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">🖱</div><div><div class="ci-name">添加桌面快捷方式</div><div class="ci-desc">快速启动本工具</div></div></div>
|
||
<button class="ci-btn" onclick="toast('已创建桌面快捷方式(原型)','ok')">添加</button>
|
||
</div>
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">📖</div><div><div class="ci-name">帮助文档</div><div class="ci-desc">使用说明与常见问题</div></div></div>
|
||
<button class="ci-btn" onclick="toast('打开帮助文档(原型)','info')">查看</button>
|
||
</div>
|
||
<div class="cg-item">
|
||
<div class="ci-l"><div class="ci-ic">ℹ</div><div><div class="ci-name">关于</div><div class="ci-desc">版本 v0.1.0 原型</div></div></div>
|
||
<button class="ci-btn" onclick="toast('智慧农业大数据可视化控制中心 v0.1.0(原型)','info')">关于</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="cg" style="grid-column:1/-1;">
|
||
<div class="cg-title">⌨ 快捷键(模拟)</div>
|
||
<div class="cg-body">
|
||
<div style="display:flex;flex-wrap:wrap;gap:6px;">
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">1</span>
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">2</span>
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">3</span>
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">4</span>
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">5</span>
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">6</span>
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">7</span>
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">8</span>
|
||
<span class="kbd" style="background:var(--blue-1);border:1px solid var(--line);padding:4px 10px;border-radius:3px;font-size:12px;">9</span>
|
||
<span style="font-size:12px;color:var(--text-dim);margin-left:8px;">= 按 ControllerIndex 切换大屏页面</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ===== 状态栏 ===== -->
|
||
<div class="statusbar">
|
||
<div class="seg">📄 <span id="sbFile">untitled.json</span></div>
|
||
<div class="sep"></div>
|
||
<div class="seg">📑 <span id="sbPageCount">0</span> 页</div>
|
||
<div class="seg">🖥 <span id="sbScreenCount">0</span> 屏</div>
|
||
<div class="seg">🎯 当前:<span id="sbCur">—</span></div>
|
||
<div class="right">
|
||
<div class="seg" id="sbView">视图:页面视图</div>
|
||
<div class="seg" id="sbTime">--:--:--</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 预览浮层 -->
|
||
<div class="preview-mask" id="previewMask" onclick="if(event.target===this)closePreview()">
|
||
<div class="preview-frame">
|
||
<div class="pf-head">
|
||
<span id="pvHead" style="color:var(--blue-4);font-weight:600;">预览:农业数据总览</span>
|
||
<div style="display:flex;gap:8px;">
|
||
<button class="btn-mini" onclick="refreshPreview()" style="background:#1f2a3a;color:#cbd5e1;border:1px solid #2a3a4a;padding:3px 10px;border-radius:3px;cursor:pointer;">↻ 刷新</button>
|
||
<button class="btn-mini" onclick="closePreview()" style="background:#1f2a3a;color:#cbd5e1;border:1px solid #2a3a4a;padding:3px 10px;border-radius:3px;cursor:pointer;">关闭</button>
|
||
<button class="btn-mini" onclick="publishSingle();closePreview();" style="background:var(--blue-6);color:#fff;border:1px solid var(--blue-6);padding:3px 10px;border-radius:3px;cursor:pointer;">📤 发布到屏幕</button>
|
||
</div>
|
||
</div>
|
||
<iframe id="pvIframeBig" src="sample-screen.html?no=1&title=%E5%86%9C%E4%B8%9A%E6%95%B0%E6%8D%AE%E6%80%BB%E8%A7%88"></iframe>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Toast -->
|
||
<div class="toast-box" id="toastBox"></div>
|
||
|
||
<script>
|
||
/* ============== 数据模型 ============== */
|
||
let pages = [
|
||
{ id: 'P-001', title: '农业数据总览', desc: '全基地农业数据总览,核心指标、产量、设备运行状态。', screen: 1, ctrl: 1, display: true, url: 'sample-screen.html', params: 'no=1\ntitle=农业数据总览' },
|
||
{ id: 'P-002', title: '土壤墒情监测', desc: '土壤湿度与墒情分析,含各地块柱状图。', screen: 2, ctrl: 2, display: true, url: 'sample-screen.html', params: 'no=2\ntitle=土壤墒情监测' },
|
||
{ id: 'P-003', title: '温室环境监控', desc: '温室内环境参数(温度/湿度/光照/CO2)实时监控。', screen: 3, ctrl: 3, display: true, url: 'sample-screen.html', params: 'no=3\ntitle=温室环境监控' },
|
||
{ id: 'P-004', title: '设备运维看板', desc: '灌溉设备、植保无人机、水肥一体机运维状态。', screen: 1, ctrl: 4, display: true, url: 'sample-screen.html', params: 'no=4\ntitle=设备运维看板' },
|
||
{ id: 'P-005', title: '智慧园区', desc: '智慧园区总览(演示)。', screen: 2, ctrl: 5, display: true, url: 'sample-screen.html', params: 'no=5\ntitle=智慧园区' },
|
||
{ id: 'P-006', title: '排行榜', desc: '产量排行、效率排行等统计页面。', screen: 3, ctrl: 6, display: true, url: 'sample-screen.html', params: 'no=6\ntitle=排行榜' },
|
||
{ id: 'P-007', title: '新建页面', desc: '', screen: 1, ctrl: 0, display: false, url: 'about:blank', params: '' },
|
||
];
|
||
let curIdx = 0;
|
||
let screens = [];
|
||
let screensDetected = false;
|
||
let curView = 'page';
|
||
let curLeftTab = 'page';
|
||
let curRightTab = 'page';
|
||
let zoom = 100;
|
||
let viewMode = 'edit'; // edit / publish / config
|
||
|
||
const THUMB_COLORS = ['#4a7fc1', '#5fa15f', '#ed7d31', '#a05050', '#7a5fa1', '#2a8a8a', '#8a8a2a'];
|
||
|
||
/* ============== Ribbon Tab 切换 ============== */
|
||
document.querySelectorAll('.ribbon-tab').forEach(t => {
|
||
t.onclick = () => {
|
||
const name = t.dataset.rtab;
|
||
viewMode = name;
|
||
document.querySelectorAll('.ribbon-tab').forEach(x => x.classList.remove('active'));
|
||
t.classList.add('active');
|
||
document.querySelectorAll('.ribbon-panel').forEach(p => p.classList.remove('active'));
|
||
document.querySelector(`.ribbon-panel[data-rtab="${name}"]`).classList.add('active');
|
||
document.getElementById('main-edit').style.display = name === 'edit' ? 'flex' : 'none';
|
||
document.getElementById('main-publish').style.display = name === 'publish' ? 'block' : 'none';
|
||
document.getElementById('main-config').style.display = name === 'config' ? 'block' : 'none';
|
||
document.getElementById('sbView').textContent = '视图:' + ({ edit: '编辑', publish: '发布', config: '配置' }[name]);
|
||
};
|
||
});
|
||
|
||
/* ============== 左侧 Tab ============== */
|
||
function switchLeftTab(name) {
|
||
curLeftTab = name;
|
||
document.querySelectorAll('.pane-left .pane-head .tab').forEach(x => x.classList.remove('active'));
|
||
document.querySelector(`.pane-left .pane-head .tab[data-ltab="${name}"]`).classList.add('active');
|
||
if (name === 'page') {
|
||
document.getElementById('pageList').style.display = 'flex';
|
||
renderPageList();
|
||
} else {
|
||
document.getElementById('pageList').style.display = 'none';
|
||
document.getElementById('leftBody').innerHTML = '<pre class="code-left">' + jsonHighlight(projectJson()) + '</pre>';
|
||
}
|
||
}
|
||
|
||
/* ============== 右侧 Tab ============== */
|
||
function switchRightTab(name) {
|
||
curRightTab = name;
|
||
document.querySelectorAll('.pane-right .pane-head .tab').forEach(x => x.classList.remove('active'));
|
||
document.querySelector(`.pane-right .pane-head .tab[data-rtab="${name}"]`).classList.add('active');
|
||
document.getElementById('propsForm').style.display = name === 'page' ? 'flex' : 'none';
|
||
document.getElementById('propsTree').style.display = name === 'code' ? 'block' : 'none';
|
||
if (name === 'code') renderPropsTree();
|
||
}
|
||
|
||
/* ============== 渲染:页面列表 ============== */
|
||
function renderPageList() {
|
||
const box = document.getElementById('pageList');
|
||
box.innerHTML = '';
|
||
pages.forEach((p, i) => {
|
||
const c = THUMB_COLORS[i % THUMB_COLORS.length];
|
||
let thumbHtml;
|
||
if (p.display && p.url && p.url !== 'about:blank') {
|
||
const bars = [];
|
||
for (let k = 0; k < 6; k++) {
|
||
const h = 12 + ((i * 11 + k * 9) % 50);
|
||
bars.push(`<span class="bar" style="left:${6 + k * 14}px;height:${h}%;background:${c};"></span>`);
|
||
}
|
||
thumbHtml = `<div class="thumb">
|
||
<span class="line" style="top:18%;left:6%;width:50%;background:rgba(255,255,255,.45);"></span>
|
||
<span class="line" style="top:34%;left:6%;width:30%;background:rgba(255,255,255,.3);"></span>
|
||
<span class="dot" style="top:24%;right:10%;background:#ffd479;"></span>
|
||
${bars.join('')}
|
||
</div>`;
|
||
} else {
|
||
thumbHtml = `<div class="thumb" style="display:flex;align-items:center;justify-content:center;color:#5a7395;font-size:11px;">about:blank</div>`;
|
||
}
|
||
const card = document.createElement('div');
|
||
card.className = 'pcard' + (i === curIdx ? ' active' : '') + (p.display ? '' : ' off');
|
||
card.innerHTML = `
|
||
${thumbHtml}
|
||
<span class="no">#${i + 1}</span>
|
||
<div class="title">${escapeHtml(p.title)}</div>`;
|
||
card.onclick = () => selectPage(i);
|
||
box.appendChild(card);
|
||
});
|
||
document.getElementById('sbPageCount').textContent = pages.length;
|
||
}
|
||
|
||
function selectPage(i) {
|
||
curIdx = i;
|
||
renderPageList();
|
||
renderProps();
|
||
renderCanvas();
|
||
document.getElementById('sbCur').textContent = pages[i].title;
|
||
if (curLeftTab === 'code') switchLeftTab('code'); else renderPageList();
|
||
if (curRightTab === 'code') renderPropsTree();
|
||
updatePreview();
|
||
}
|
||
|
||
/* ============== 渲染:属性表单 ============== */
|
||
function renderProps() {
|
||
const p = pages[curIdx];
|
||
const f = document.getElementById('propsForm');
|
||
const screenOpts = [1, 2, 3].map(n => `<option value="${n}" ${p.screen === n ? 'selected' : ''}>屏幕 ${n}</option>`).join('');
|
||
f.innerHTML = `
|
||
<div class="pgrp">
|
||
<div class="pgrp-title">▦ 基本信息</div>
|
||
<div class="pfield">
|
||
<label>PageID <span class="req">*</span> <span style="color:var(--text-dim);">页面编号</span></label>
|
||
<input class="input" value="${escapeHtml(p.id)}" disabled>
|
||
</div>
|
||
<div class="pfield">
|
||
<label>PageTitle 页面标题</label>
|
||
<input class="input" id="f_title" value="${escapeHtml(p.title)}" oninput="applyProp('title')">
|
||
</div>
|
||
</div>
|
||
<div class="pgrp">
|
||
<div class="pgrp-title">▣ 投放设置</div>
|
||
<div class="pfield">
|
||
<label>ScreenIndex <span class="req">*</span> <span style="color:var(--text-dim);">绑定屏幕</span></label>
|
||
<select class="select" id="f_screen" onchange="applyProp('screen')">${screenOpts}</select>
|
||
<div class="hint">决定该页面发布到哪一块屏幕</div>
|
||
</div>
|
||
<div class="pfield">
|
||
<label>ControllerIndex 控制器索引 <span style="color:var(--text-dim);">数字键切换</span></label>
|
||
<input class="input" id="f_ctrl" type="number" min="0" value="${p.ctrl}" oninput="applyProp('ctrl')">
|
||
<div class="hint">键盘 / 遥控器数字键切换到该页面</div>
|
||
</div>
|
||
<div class="pfield">
|
||
<label>IsDisplay 是否显示</label>
|
||
<div class="row">
|
||
<label class="switch"><input type="checkbox" id="f_display" ${p.display ? 'checked' : ''} onchange="applyProp('display')"><span class="slider"></span></label>
|
||
<span style="font-size:12.5px;" id="f_displayTxt">${p.display ? '显示' : '隐藏'}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="pgrp">
|
||
<div class="pgrp-title">🌐 网页地址</div>
|
||
<div class="pfield">
|
||
<label>PageUrl 页面网页地址</label>
|
||
<input class="input" id="f_url" value="${escapeHtml(p.url)}" oninput="applyProp('url')">
|
||
<div class="hint">如 about:blank 或 http(s) 大屏地址</div>
|
||
</div>
|
||
<div class="pfield">
|
||
<label>PageParams 页面参数 <span style="color:var(--text-dim);">key=value 每行</span></label>
|
||
<textarea id="f_params" oninput="applyProp('params')">${escapeHtml(p.params)}</textarea>
|
||
<div class="hint">访问时自动拼为 URL 查询参数</div>
|
||
</div>
|
||
</div>`;
|
||
document.getElementById('describeBody').textContent = p.desc || '(无描述)';
|
||
}
|
||
|
||
function applyProp(key) {
|
||
const p = pages[curIdx];
|
||
if (key === 'title') p.title = document.getElementById('f_title').value;
|
||
if (key === 'screen') p.screen = parseInt(document.getElementById('f_screen').value);
|
||
if (key === 'ctrl') p.ctrl = parseInt(document.getElementById('f_ctrl').value || 0);
|
||
if (key === 'display') { p.display = document.getElementById('f_display').checked; document.getElementById('f_displayTxt').textContent = p.display ? '显示' : '隐藏'; }
|
||
if (key === 'url') p.url = document.getElementById('f_url').value;
|
||
if (key === 'params') p.params = document.getElementById('f_params').value;
|
||
renderPageList();
|
||
renderCanvas();
|
||
document.getElementById('describeBody').textContent = p.desc || '(无描述)';
|
||
document.getElementById('sbCur').textContent = p.title;
|
||
}
|
||
|
||
/* ============== 渲染:属性树(代码视图) ============== */
|
||
function renderPropsTree() {
|
||
const p = pages[curIdx];
|
||
const html = `
|
||
<div class="ptrow"><span class="arr">▼</span><span class="name">PageID</span><span class="val string">${escapeHtml(p.id)}</span></div>
|
||
<div class="ptrow"><span class="arr">▼</span><span class="name">PageTitle</span><span class="val string">${escapeHtml(p.title)}</span></div>
|
||
<div class="ptrow"><span class="arr">▼</span><span class="name">PageDescribe</span><span class="val string">${escapeHtml(p.desc || '')}</span></div>
|
||
<div class="ptrow"><span class="arr">▼</span><span class="name">PageUrl</span><span class="val string">${escapeHtml(p.url)}</span></div>
|
||
<div class="ptrow"><span class="arr">▼</span><span class="name">PageParams</span><span class="val string">${escapeHtml(p.params)}</span></div>
|
||
<div class="ptrow"><span class="arr">▼</span><span class="name">ScreenIndex</span><span class="val">${p.screen}</span></div>
|
||
<div class="ptrow"><span class="arr">▼</span><span class="name">ControllerIndex</span><span class="val">${p.ctrl}</span></div>
|
||
<div class="ptrow"><span class="arr">▼</span><span class="name">IsDisplay</span><span class="val ${p.display ? 'true' : 'false'}">${p.display ? 'True' : 'False'}</span></div>
|
||
<div class="ptrow"><span class="arr">▶</span><span class="name">Thumbnail</span><span class="val" style="color:var(--text-dim);">(image)</span></div>`;
|
||
document.getElementById('propsTree').innerHTML = html;
|
||
}
|
||
|
||
/* ============== 渲染:画布 ============== */
|
||
function renderCanvas() {
|
||
const p = pages[curIdx];
|
||
document.getElementById('cvTitle').textContent = p.title;
|
||
document.getElementById('cvScreen').textContent = '屏幕 ' + p.screen;
|
||
const params = new URLSearchParams();
|
||
params.set('no', p.id.replace(/\D/g, ''));
|
||
params.set('title', p.title);
|
||
const src = p.url + '?' + params.toString();
|
||
document.getElementById('cvUrl').textContent = src;
|
||
document.getElementById('screenIframe').src = src;
|
||
document.getElementById('codeContent').textContent = JSON.stringify(projectJson(), null, 2);
|
||
}
|
||
|
||
/* ============== 页面操作 ============== */
|
||
function addPage() {
|
||
const n = pages.length + 1;
|
||
pages.push({ id: 'P-' + String(n).padStart(3, '0'), title: '新页面 ' + n, desc: '', screen: 1, ctrl: n, display: true, url: 'sample-screen.html', params: 'no=' + n + '\ntitle=新页面 ' + n });
|
||
selectPage(pages.length - 1);
|
||
toast('已新建页面 P-' + String(n).padStart(3, '0'), 'ok');
|
||
}
|
||
function dupPage() {
|
||
const src = pages[curIdx];
|
||
const copy = JSON.parse(JSON.stringify(src));
|
||
const ids = pages.map(p => p.id);
|
||
let n = pages.length + 1;
|
||
while (ids.includes('P-' + String(n).padStart(3, '0'))) n++;
|
||
copy.id = 'P-' + String(n).padStart(3, '0');
|
||
copy.title = src.title + '(副本)';
|
||
pages.splice(curIdx + 1, 0, copy);
|
||
selectPage(curIdx + 1);
|
||
toast('已复制页面 ' + copy.id, 'ok');
|
||
}
|
||
function movePage(dir) {
|
||
const ni = curIdx + dir;
|
||
if (ni < 0 || ni >= pages.length) { toast('已在边界,无法移动', 'warn'); return; }
|
||
[pages[curIdx], pages[ni]] = [pages[ni], pages[curIdx]];
|
||
selectPage(ni);
|
||
}
|
||
function delPage() {
|
||
if (pages.length <= 1) { toast('至少保留一个页面', 'warn'); return; }
|
||
const rm = pages[curIdx];
|
||
pages.splice(curIdx, 1);
|
||
curIdx = Math.min(curIdx, pages.length - 1);
|
||
selectPage(curIdx);
|
||
toast('已删除页面 ' + rm.id, 'ok');
|
||
}
|
||
function resetProject() {
|
||
pages = [
|
||
{ id: 'P-001', title: '农业数据总览', desc: '全基地农业数据总览', screen: 1, ctrl: 1, display: true, url: 'sample-screen.html', params: 'no=1\ntitle=农业数据总览' },
|
||
{ id: 'P-002', title: '土壤墒情监测', desc: '土壤湿度与墒情分析', screen: 2, ctrl: 2, display: true, url: 'sample-screen.html', params: 'no=2\ntitle=土壤墒情监测' },
|
||
{ id: 'P-003', title: '温室环境监控', desc: '温室内环境参数实时监控', screen: 3, ctrl: 3, display: true, url: 'sample-screen.html', params: 'no=3\ntitle=温室环境监控' },
|
||
{ id: 'P-004', title: '设备运维看板', desc: '灌溉设备运维状态', screen: 1, ctrl: 4, display: true, url: 'sample-screen.html', params: 'no=4\ntitle=设备运维看板' },
|
||
];
|
||
curIdx = 0;
|
||
selectPage(0);
|
||
toast('项目已重置', 'ok');
|
||
}
|
||
function initBlank() {
|
||
const n = pages.length + 1;
|
||
pages.push({ id: 'P-' + String(n).padStart(3, '0'), title: '空白页 ' + n, desc: '初始化为空白页面', screen: 1, ctrl: 0, display: false, url: 'about:blank', params: '' });
|
||
selectPage(pages.length - 1);
|
||
toast('已初始化空白页(PageUrl = about:blank)', 'ok');
|
||
}
|
||
|
||
/* ============== 项目操作 ============== */
|
||
function saveProject() {
|
||
const data = JSON.stringify(projectJson(), null, 2);
|
||
const blob = new Blob([data], { type: 'application/json' });
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = '智慧农业大数据可视化.json';
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
document.getElementById('sbFile').textContent = '智慧农业大数据可视化.json';
|
||
toast('项目已保存为 .json 工程文件', 'ok');
|
||
}
|
||
function exportProject() { saveProject(); }
|
||
function projectJson() {
|
||
return {
|
||
appName: '智慧农业大数据可视化控制中心',
|
||
version: '0.1.0',
|
||
pages: pages.map(p => ({
|
||
PageID: p.id, PageTitle: p.title, PageDescribe: p.desc,
|
||
ScreenIndex: p.screen, ControllerIndex: p.ctrl, IsDisplay: p.display,
|
||
PageUrl: p.url, PageParams: p.params,
|
||
})),
|
||
};
|
||
}
|
||
|
||
/* ============== 视图切换 ============== */
|
||
function switchView(mode) {
|
||
curView = mode;
|
||
document.getElementById('rbViewPage').classList.toggle('active', mode === 'page');
|
||
document.getElementById('rbViewCode').classList.toggle('active', mode === 'code');
|
||
document.getElementById('centerPage').style.display = mode === 'page' ? 'flex' : 'none';
|
||
document.getElementById('centerCode').style.display = mode === 'code' ? 'block' : 'none';
|
||
if (mode === 'code') document.getElementById('codeContent').textContent = JSON.stringify(projectJson(), null, 2);
|
||
document.getElementById('sbView').textContent = '视图:' + (mode === 'page' ? '页面视图' : '代码视图');
|
||
}
|
||
|
||
/* ============== 画布缩放 ============== */
|
||
function zoomCanvas(d) {
|
||
zoom = Math.min(160, Math.max(50, zoom + d));
|
||
document.getElementById('zoomVal').textContent = zoom + '%';
|
||
document.getElementById('canvasFrame').style.transform = 'scale(' + zoom / 100 + ')';
|
||
}
|
||
|
||
/* ============== 发布:屏幕 ============== */
|
||
const SCREEN_PROFILES = [
|
||
{ no: 1, name: '屏幕 1', res: '1920×1080', pos: '主显示器' },
|
||
{ no: 2, name: '屏幕 2', res: '1366×768', pos: '扩展显示器' },
|
||
{ no: 3, name: '屏幕 3', res: '1920×1080', pos: '扩展显示器' },
|
||
];
|
||
function detectScreens() {
|
||
screens = SCREEN_PROFILES.map(s => ({ ...s, playing: null }));
|
||
screensDetected = true;
|
||
renderScreens();
|
||
updateScreenSelects();
|
||
refreshPvTarget();
|
||
document.getElementById('sbScreenCount').textContent = screens.length;
|
||
toast('已检测到 ' + screens.length + ' 块可用显示屏幕', 'ok');
|
||
}
|
||
function updateScreenSelects() {
|
||
const opts = SCREEN_PROFILES.map(s => `<option value="${s.no}">${s.name} · ${s.res}</option>`).join('');
|
||
['singleScreen', 'mainScreen'].forEach(id => document.getElementById(id).innerHTML = opts);
|
||
}
|
||
function refreshPvTarget() {
|
||
const opts = [];
|
||
opts.push('<optgroup label="按屏幕预览">');
|
||
SCREEN_PROFILES.forEach(s => opts.push(`<option value="s:${s.no}">${s.name} · ${s.res}</option>`));
|
||
opts.push('</optgroup>');
|
||
opts.push('<optgroup label="按页面预览">');
|
||
pages.forEach((p, i) => opts.push(`<option value="p:${i}">${p.title}</option>`));
|
||
opts.push('</optgroup>');
|
||
document.getElementById('pvTarget').innerHTML = opts.join('');
|
||
updatePreview();
|
||
}
|
||
function renderScreens() {
|
||
const box = document.getElementById('screenList');
|
||
if (!screensDetected) {
|
||
box.innerHTML = '<div class="empty" style="grid-column:1/-1;">尚未检测屏幕</div>';
|
||
return;
|
||
}
|
||
box.innerHTML = '';
|
||
screens.forEach(s => {
|
||
const card = document.createElement('div');
|
||
card.className = 'scard';
|
||
const playText = s.playing
|
||
? `<div class="splay">▶ ${escapeHtml(s.playing.title)}</div>`
|
||
: `<div class="splay off">○ 空闲</div>`;
|
||
card.innerHTML = `
|
||
<div class="sno">${s.name}</div>
|
||
<div class="sres">${s.res} · ${s.pos}</div>
|
||
${playText}`;
|
||
card.onclick = () => {
|
||
const tgt = document.getElementById('pvTarget');
|
||
tgt.value = 's:' + s.no;
|
||
updatePreview();
|
||
toast(`已切换预览:${s.name}`, 'info');
|
||
};
|
||
box.appendChild(card);
|
||
});
|
||
}
|
||
|
||
/* ============== 发布:动作 ============== */
|
||
function getCurPage() { return pages[curIdx]; }
|
||
|
||
function pageIframeSrc(p) {
|
||
const params = new URLSearchParams();
|
||
params.set('no', p.id.replace(/\D/g, ''));
|
||
params.set('title', p.title);
|
||
return p.url + '?' + params.toString();
|
||
}
|
||
|
||
function updatePreview() {
|
||
const val = document.getElementById('pvTarget').value;
|
||
const iframe = document.getElementById('pvIframe');
|
||
let label, src;
|
||
if (val.startsWith('s:')) {
|
||
const no = parseInt(val.slice(2));
|
||
const s = screens.find(x => x.no === no);
|
||
document.getElementById('pvBadge').textContent = s ? s.name : '屏幕 ' + no;
|
||
if (s && s.playing) {
|
||
src = pageIframeSrc({ id: 'P-' + String(no).padStart(3, '0'), title: s.playing.title });
|
||
label = s.playing.title;
|
||
} else {
|
||
src = 'about:blank';
|
||
label = '(空闲)';
|
||
}
|
||
} else {
|
||
const idx = parseInt(val.slice(2));
|
||
const p = pages[idx];
|
||
document.getElementById('pvBadge').textContent = '页面预览';
|
||
src = pageIframeSrc(p);
|
||
label = p.title;
|
||
}
|
||
iframe.src = src;
|
||
document.getElementById('pvCurPage').textContent = '— ' + label + ' —';
|
||
}
|
||
|
||
function refreshPreview() {
|
||
const f = document.getElementById('pvIframe');
|
||
f.src = f.src;
|
||
toast('预览已刷新', 'info');
|
||
}
|
||
|
||
function previewToScreen() {
|
||
if (!screensDetected) detectScreens();
|
||
const tgt = document.getElementById('pvTarget');
|
||
let no = 1;
|
||
if (tgt.value.startsWith('s:')) no = parseInt(tgt.value.slice(2));
|
||
const p = getCurPage();
|
||
const s = screens.find(x => x.no === no);
|
||
if (s) {
|
||
s.playing = { title: p.title, url: p.url };
|
||
renderScreens();
|
||
updatePreview();
|
||
toast(`已将「${p.title}」预览到 ${s.name}`, 'ok');
|
||
}
|
||
}
|
||
function openPreview() {
|
||
const p = getCurPage();
|
||
document.getElementById('pvHead').textContent = '预览:' + p.title;
|
||
document.getElementById('pvIframeBig').src = pageIframeSrc(p);
|
||
document.getElementById('previewMask').classList.add('active');
|
||
}
|
||
function closePreview() {
|
||
document.getElementById('previewMask').classList.remove('active');
|
||
}
|
||
function publishSingle() {
|
||
if (!screensDetected) detectScreens();
|
||
const no = parseInt(document.getElementById('singleScreen').value);
|
||
const p = getCurPage();
|
||
const s = screens.find(x => x.no === no);
|
||
if (s) {
|
||
s.playing = { title: p.title, url: p.url };
|
||
renderScreens();
|
||
updatePreview();
|
||
toast(`「${p.title}」已发布到 ${s.name}`, 'ok');
|
||
}
|
||
}
|
||
function closeScreen() {
|
||
if (!screensDetected) { toast('请先检测屏幕', 'warn'); return; }
|
||
const no = parseInt(document.getElementById('singleScreen').value);
|
||
const s = screens.find(x => x.no === no);
|
||
if (s) { s.playing = null; renderScreens(); updatePreview(); toast(`${s.name} 已关闭播放页面`, 'info'); }
|
||
}
|
||
function publishAll() {
|
||
if (!screensDetected) detectScreens();
|
||
let cnt = 0;
|
||
screens.forEach(s => { s.playing = null; });
|
||
pages.filter(p => p.display).forEach(p => {
|
||
const s = screens.find(x => x.no === p.screen);
|
||
if (s) { s.playing = { title: p.title, url: p.url }; cnt++; }
|
||
});
|
||
renderScreens();
|
||
updatePreview();
|
||
toast(`多屏发布完成:${cnt} 个页面分发到 ${screens.length} 块屏幕`, 'ok');
|
||
}
|
||
function closeAll() {
|
||
screens.forEach(s => { s.playing = null; });
|
||
renderScreens();
|
||
updatePreview();
|
||
toast('已关闭所有屏幕的播放页面', 'info');
|
||
}
|
||
|
||
/* ============== 配置:工具 ============== */
|
||
let audioCtx = null;
|
||
function beep() {
|
||
try {
|
||
audioCtx = audioCtx || new (window.AudioContext || window.webkitAudioContext)();
|
||
const o = audioCtx.createOscillator();
|
||
const g = audioCtx.createGain();
|
||
o.connect(g); g.connect(audioCtx.destination);
|
||
o.frequency.value = 880; o.type = 'sine';
|
||
g.gain.setValueAtTime(0.3, audioCtx.currentTime);
|
||
g.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.4);
|
||
o.start(); o.stop(audioCtx.currentTime + 0.45);
|
||
toast('声音测试:已播放提示音(880Hz)', 'ok');
|
||
} catch (e) {
|
||
toast('当前浏览器无法播放声音(WinForm 中调用系统 Beep)', 'warn');
|
||
}
|
||
}
|
||
function keyTest() {
|
||
toast('键盘测试:请按数字键 1-9 切换页面(原型)', 'info');
|
||
if (window.__keyHandler) window.removeEventListener('keydown', window.__keyHandler);
|
||
window.__keyHandler = function (e) {
|
||
const n = parseInt(e.key);
|
||
if (!isNaN(n) && n >= 1 && n <= 9) {
|
||
const idx = pages.findIndex(p => p.ctrl === n);
|
||
if (idx >= 0) {
|
||
// 切到编辑 Tab 并选中页面
|
||
document.querySelector('.ribbon-tab[data-rtab="edit"]').click();
|
||
selectPage(idx);
|
||
toast(`快捷键 ${n} → 已切换到「${pages[idx].title}」`, 'ok');
|
||
window.removeEventListener('keydown', window.__keyHandler);
|
||
}
|
||
}
|
||
};
|
||
window.addEventListener('keydown', window.__keyHandler);
|
||
}
|
||
function offlineToggle(el) {
|
||
toast(el.checked ? '已开启离线模式:仅使用本地工程' : '已关闭离线模式', el.checked ? 'ok' : 'info');
|
||
}
|
||
|
||
/* ============== Toast ============== */
|
||
function toast(msg, type = 'info') {
|
||
const box = document.getElementById('toastBox');
|
||
const el = document.createElement('div');
|
||
el.className = 'toast ' + type;
|
||
const icons = { info: '💡', ok: '✅', warn: '⚠️' };
|
||
el.innerHTML = `<span class="t-ic">${icons[type] || '💡'}</span><span>${escapeHtml(msg)}</span>`;
|
||
box.appendChild(el);
|
||
setTimeout(() => { el.classList.add('out'); setTimeout(() => el.remove(), 260); }, 2600);
|
||
}
|
||
|
||
/* ============== 工具 ============== */
|
||
function escapeHtml(s) {
|
||
return String(s == null ? '' : s)
|
||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||
.replace(/"/g, '"').replace(/'/g, ''');
|
||
}
|
||
function jsonHighlight(obj) {
|
||
return escapeHtml(JSON.stringify(obj, null, 2));
|
||
}
|
||
|
||
/* ============== 状态栏时钟 ============== */
|
||
function tickClock() {
|
||
const d = new Date();
|
||
const p = x => x < 10 ? '0' + x : x;
|
||
document.getElementById('sbTime').textContent = d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds());
|
||
}
|
||
setInterval(tickClock, 1000);
|
||
tickClock();
|
||
|
||
/* ============== 初始化 ============== */
|
||
function init() {
|
||
renderPageList();
|
||
renderProps();
|
||
renderCanvas();
|
||
refreshPvTarget();
|
||
document.getElementById('sbCur').textContent = pages[curIdx].title;
|
||
document.getElementById('sbFile').textContent = 'untitled.json';
|
||
}
|
||
init();
|
||
</script>
|
||
</body>
|
||
</html>
|