Files

1229 lines
64 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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; }
/* 底部 GroupBoxPageDescribe */
.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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
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>