guanghu-ice-heart/modules/guanghu-panel-kit/frontend/panel-lake-projection.example.html
冰朔 95c51c82d6 feat(panel-kit+skills): 注册湖面投影可复用UI模板 + 技能书包增补
冰朔判定"这个UI好看,注册成可复用模板":
- frontend/panel-lake-projection.example.html(夜湖冷星·胖头鱼现用)
- frontend/panel-lake-projection-warm.example.html(烛畔暖湖·守望现用)
- INDEX 文件清单更新:server.js v1.1 window_hours、模板说明、审美基线引用
- ZHUYUAN-SKILLS 增补 2.6 子系统面板建设 + 4.3 光湖UI设计(15→17项)
2026-08-07 19:46:36 +08:00

492 lines
36 KiB
HTML
Raw 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>胖头鱼 · 湖面投影 — SYS-GLW-PTS-0001</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=Noto+Serif+SC:wght@400;600;700&family=Noto+Sans+SC:wght@400;500;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
:root{
--ink-0:#06070f;
--ink-1:#0a0c1e;
--ink-2:#0f1229;
--nebula-a:rgba(64,46,138,.30);
--nebula-b:rgba(30,56,120,.22);
--star:#f2e9d3;
--star-strong:#fbf5e6;
--line:rgba(242,233,211,.12);
--line-strong:rgba(242,233,211,.30);
--hi:#f7f2e6;
--mid:rgba(236,233,222,.74);
--low:rgba(236,233,222,.48);
--glass:rgba(13,16,36,.55);
--ok:#a8d8a0;
--warn:#e8c98a;
--bad:#e08a82;
--f-display:'Cormorant Garamond','Noto Serif SC',serif;
--f-cn:'Noto Serif SC',serif;
--f-body:'Noto Sans SC',sans-serif;
--f-mono:'JetBrains Mono',monospace;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{height:100%}
body{
font-family:var(--f-body);color:var(--hi);overflow:hidden;-webkit-font-smoothing:antialiased;
background:
radial-gradient(1100px 640px at 12% 0%, var(--nebula-b), transparent 60%),
radial-gradient(1000px 700px at 88% 10%, var(--nebula-a), transparent 62%),
radial-gradient(1200px 500px at 50% 108%, rgba(40,32,90,.22), transparent 70%),
linear-gradient(178deg, var(--ink-1) 0%, var(--ink-0) 100%);
}
body::after{content:"";position:fixed;inset:0;pointer-events:none;opacity:.045;mix-blend-mode:overlay;
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");}
#dust{position:fixed;inset:0;pointer-events:none;z-index:0}
.mote{position:absolute;border-radius:50%;background:var(--star);animation:breathe var(--dur,8s) ease-in-out var(--delay,0s) infinite}
@keyframes breathe{0%,100%{opacity:.1}50%{opacity:.75}}
#constellation{position:fixed;inset:0;z-index:1;pointer-events:none;opacity:.5}
header{position:fixed;top:0;left:0;right:0;z-index:30;display:flex;justify-content:space-between;align-items:center;padding:24px 52px}
.brand{display:flex;align-items:center;gap:18px}
.brand .mark{width:11px;height:11px;border-radius:50%;background:var(--star);box-shadow:0 0 14px rgba(242,233,211,.9),0 0 34px rgba(242,233,211,.35);animation:breathe 5s ease-in-out infinite}
.brand h1{font-family:var(--f-cn);font-weight:600;font-size:19px;letter-spacing:.3em}
.brand small{display:block;font-family:var(--f-mono);font-weight:400;font-size:11px;letter-spacing:.22em;color:var(--low);margin-top:5px}
.head-right{display:flex;align-items:center;gap:20px;font-family:var(--f-mono);font-weight:400;font-size:13px;letter-spacing:.1em;color:var(--mid)}
.live{display:flex;align-items:center;gap:9px;color:var(--star);font-weight:500}
.live i{width:7px;height:7px;border-radius:50%;background:var(--star);box-shadow:0 0 8px rgba(242,233,211,.9);animation:blink 2.4s ease-in-out infinite}
@keyframes blink{0%,100%{opacity:.25}50%{opacity:1}}
.login-btn{font-family:var(--f-cn);font-weight:600;font-size:13px;letter-spacing:.24em;text-indent:.24em;color:var(--star);background:rgba(242,233,211,.06);border:1px solid var(--line-strong);border-radius:100px;padding:10px 26px;cursor:pointer;transition:all .3s ease}
.login-btn:hover{background:rgba(242,233,211,.14);box-shadow:0 0 22px rgba(242,233,211,.22);transform:translateY(-2px)}
.login-btn:active{transform:translateY(0) scale(.97)}
.user-chip{display:flex;align-items:center;gap:12px;font-family:var(--f-body);font-size:13px;color:var(--mid)}
.user-chip b{color:var(--star);font-weight:500}
.user-chip button{background:none;border:none;color:var(--low);cursor:pointer;font-family:var(--f-body);font-size:12.5px;letter-spacing:.1em;transition:color .3s}
.user-chip button:hover{color:var(--hi)}
#stage{position:fixed;inset:0;z-index:10;display:grid;grid-template-columns:minmax(430px,30%) 1fr;gap:34px;max-width:1780px;margin:0 auto;padding:96px 56px 70px 60px;overflow-y:auto}
.col-left{display:flex;flex-direction:column;gap:24px;justify-content:center}
.protagonist{display:flex;flex-direction:column;align-items:center;gap:24px;animation:rise 1.1s cubic-bezier(.22,1,.36,1) both}
.ring-wrap{position:relative;width:360px;height:360px}
.ring-wrap svg{width:100%;height:100%;overflow:visible}
.ring-bg{fill:none;stroke:rgba(242,233,211,.08);stroke-width:2}
.ring-arc{fill:none;stroke:url(#arcGrad);stroke-width:3;stroke-linecap:round;filter:drop-shadow(0 0 9px rgba(242,233,211,.35));animation:arcglow 5s ease-in-out infinite;transition:stroke-dashoffset .8s ease}
@keyframes arcglow{0%,100%{filter:drop-shadow(0 0 7px rgba(242,233,211,.28))}50%{filter:drop-shadow(0 0 15px rgba(242,233,211,.55))}}
.ring-tick{stroke:rgba(242,233,211,.2);stroke-width:1}
.orbit-dot{fill:var(--star);filter:drop-shadow(0 0 6px rgba(242,233,211,.9))}
.orbit-g{animation:spin 24s linear infinite;transform-origin:200px 200px}
@keyframes spin{to{transform:rotate(360deg)}}
.ring-center{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;text-align:center;padding:0 40px}
.ring-state{font-family:var(--f-cn);font-weight:600;font-size:15px;letter-spacing:.36em;color:var(--mid);text-indent:.36em}
.ring-count{font-family:var(--f-display);font-weight:600;font-size:62px;letter-spacing:.05em;color:var(--star-strong);text-shadow:0 0 32px rgba(242,233,211,.28);font-variant-numeric:tabular-nums;line-height:1}
.ring-unit{font-family:var(--f-mono);font-weight:400;font-size:11.5px;letter-spacing:.3em;color:var(--low)}
.ring-holder{margin-top:6px;display:flex;align-items:center;gap:10px;font-size:14px;letter-spacing:.05em;color:var(--mid);font-weight:500}
.ring-holder .pdot{width:7px;height:7px;border-radius:50%;background:var(--ok);box-shadow:0 0 9px rgba(168,216,160,.85);flex:none}
.vitals{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;width:100%;max-width:430px}
.vital{position:relative;overflow:hidden;border:1px solid var(--line);border-radius:14px;background:linear-gradient(168deg, rgba(24,28,58,.5), var(--glass) 42%);backdrop-filter:blur(14px);padding:16px 18px;transition:transform .35s cubic-bezier(.22,1,.36,1), border-color .35s, box-shadow .35s;animation:rise 1.1s cubic-bezier(.22,1,.36,1) .1s both}
.vital:hover{transform:translateY(-4px) scale(1.02);border-color:var(--line-strong);box-shadow:0 14px 34px rgba(0,0,0,.45),0 0 30px rgba(242,233,211,.07)}
.vital .v-n{font-family:var(--f-display);font-weight:600;font-size:26px;color:var(--star);line-height:1.1}
.vital .v-n small{font-size:14px}
.vital .v-t{font-family:var(--f-mono);font-weight:400;font-size:10.5px;letter-spacing:.2em;color:var(--low);margin-top:7px}
.vital .v-bar{margin-top:10px;height:3px;border-radius:2px;background:rgba(242,233,211,.1);overflow:hidden}
.vital .v-bar i{display:block;height:100%;border-radius:2px;background:linear-gradient(90deg,rgba(242,233,211,.35),var(--star));box-shadow:0 0 8px rgba(242,233,211,.5);transition:width .8s ease}
.svc-line{max-width:430px;width:100%;display:flex;align-items:center;gap:18px;font-family:var(--f-mono);font-size:11px;letter-spacing:.1em;color:var(--low);padding:0 6px}
.svc-line .sdot{width:6px;height:6px;border-radius:50%;display:inline-block;margin-right:7px;background:rgba(242,233,211,.2)}
.svc-line .sdot.on{background:var(--ok);box-shadow:0 0 8px rgba(168,216,160,.8)}
.svc-line .sdot.off{background:var(--bad);box-shadow:0 0 8px rgba(224,138,130,.7)}
.planes{display:grid;grid-template-columns:1fr 1fr;grid-template-areas:"merge merge" "access deploy" "persona feed";gap:22px;align-content:center}
.plane{position:relative;overflow:hidden;border:1px solid var(--line);border-radius:18px;
background:linear-gradient(168deg, rgba(24,28,58,.5), var(--glass) 42%);
backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);
padding:26px 30px;box-shadow:0 18px 50px rgba(0,0,0,.42);
transition:transform .4s cubic-bezier(.22,1,.36,1), border-color .4s ease, box-shadow .4s ease;
display:flex;flex-direction:column;min-height:150px}
.plane::after{content:"";position:absolute;inset:0;pointer-events:none;opacity:0;transition:opacity .4s ease;
background:radial-gradient(360px circle at var(--mx,50%) var(--my,50%), rgba(242,233,211,.09), transparent 62%)}
.plane::before{content:"";position:absolute;top:0;left:12%;right:12%;height:1px;
background:linear-gradient(90deg,transparent,rgba(242,233,211,.3),transparent)}
.plane:hover{transform:translateY(-6px) scale(1.008);border-color:var(--line-strong);
box-shadow:0 26px 64px rgba(0,0,0,.52),0 0 46px rgba(242,233,211,.09)}
.plane:hover::after{opacity:1}
.plane.p-merge{grid-area:merge;animation:rise 1.1s cubic-bezier(.22,1,.36,1) .12s both}
.plane.p-access{grid-area:access;animation:rise 1.1s cubic-bezier(.22,1,.36,1) .24s both}
.plane.p-deploy{grid-area:deploy;animation:rise 1.1s cubic-bezier(.22,1,.36,1) .36s both}
.plane.p-persona{grid-area:persona;animation:rise 1.1s cubic-bezier(.22,1,.36,1) .48s both}
.plane.p-feed{grid-area:feed;animation:rise 1.1s cubic-bezier(.22,1,.36,1) .6s both}
.plane-head{display:flex;align-items:baseline;justify-content:space-between;margin-bottom:16px;gap:10px}
.plane-head h2{font-family:var(--f-cn);font-weight:600;font-size:17.5px;letter-spacing:.22em;color:var(--hi);white-space:nowrap}
.plane-head .meta{font-family:var(--f-mono);font-weight:400;font-size:10.5px;letter-spacing:.12em;color:var(--low);text-align:right}
.badge{font-family:var(--f-display);font-weight:600;font-size:24px;color:var(--star);margin-left:10px}
.badge.zero{color:var(--low)}
.row{display:grid;grid-template-columns:1fr auto;align-items:center;gap:18px;padding:13px 14px;margin:0 -14px;border-radius:10px;transition:background .3s ease;position:relative}
.row+.row{border-top:1px solid rgba(242,233,211,.06)}
.row:hover{background:rgba(242,233,211,.05)}
.row:hover::before{content:"";position:absolute;left:0;top:18%;bottom:18%;width:2.5px;border-radius:2px;background:var(--star);box-shadow:0 0 10px rgba(242,233,211,.7)}
.row .title{font-size:14.5px;letter-spacing:.02em;color:var(--hi);font-weight:500}
.row .who{display:block;font-family:var(--f-mono);font-weight:400;font-size:11.5px;letter-spacing:.05em;color:var(--low);margin-top:6px}
.row .diff{font-family:var(--f-mono);font-weight:400;font-size:12.5px;color:var(--mid);white-space:nowrap}
.row .acts{display:flex;gap:10px}
.btn{font-family:var(--f-cn);font-size:13px;letter-spacing:.18em;font-weight:600;color:var(--star);background:transparent;border:1px solid var(--line-strong);border-radius:100px;padding:8px 22px;cursor:pointer;transition:all .3s ease;text-indent:.18em;white-space:nowrap}
.btn:hover{background:rgba(242,233,211,.12);box-shadow:0 0 20px rgba(242,233,211,.2)}
.btn:active{transform:scale(.95)}
.btn.ghost{border-color:var(--line);color:var(--mid)}
.btn.ghost:hover{color:var(--star)}
.empty{font-size:13.5px;color:var(--low);letter-spacing:.08em;padding:14px 0;line-height:2}
.locked-note{font-size:13.5px;color:var(--low);letter-spacing:.08em;padding:14px 0;line-height:2}
.win-item{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;font-size:13.5px}
.win-item+.win-item{border-top:1px solid rgba(242,233,211,.06)}
.win-item .w-name{display:flex;align-items:center;gap:10px;font-family:var(--f-cn);letter-spacing:.1em;color:var(--hi);font-weight:600}
.win-item .w-name .pdot{width:6px;height:6px;border-radius:50%;background:var(--ok);box-shadow:0 0 8px rgba(168,216,160,.8);flex:none}
.win-item .w-left{font-family:var(--f-mono);font-size:12.5px;color:var(--star);letter-spacing:.05em}
.sub-cap{font-family:var(--f-mono);font-size:10.5px;letter-spacing:.2em;color:var(--low);margin:16px 0 2px}
.steps{display:flex;flex-direction:column;gap:8px;margin-top:6px}
.step{display:flex;align-items:center;gap:11px;font-size:13px;color:var(--mid);letter-spacing:.05em}
.step i{width:7px;height:7px;border-radius:50%;background:rgba(242,233,211,.22);flex:none}
.step.PASS i,.step.done i{background:var(--ok);box-shadow:0 0 8px rgba(168,216,160,.7)}
.step.PASS{color:var(--hi)}
.step.FAIL i{background:var(--bad);box-shadow:0 0 8px rgba(224,138,130,.7)}
.step.RUNNING i{background:var(--warn);box-shadow:0 0 8px rgba(232,201,138,.8);animation:blink 1.6s ease-in-out infinite}
.run-status{font-family:var(--f-mono);font-size:11px;letter-spacing:.14em;padding:4px 10px;border-radius:6px;border:1px solid var(--line)}
.run-status.DEPLOYED{color:var(--ok);border-color:rgba(168,216,160,.4)}
.run-status.FAILED{color:var(--bad);border-color:rgba(224,138,130,.4)}
.run-status.RUNNING{color:var(--warn);border-color:rgba(232,201,138,.4)}
.persona-row{display:flex;align-items:center;gap:14px;padding:13px 0}
.persona-row+.persona-row{border-top:1px solid rgba(242,233,211,.06)}
.persona-row .star{width:8px;height:8px;border-radius:50%;flex:none}
.persona-row .star.on{background:var(--ok);box-shadow:0 0 10px rgba(168,216,160,.85)}
.persona-row .star.watch{background:rgba(242,233,211,.45);box-shadow:0 0 8px rgba(242,233,211,.4)}
.persona-row .nm{font-family:var(--f-cn);font-size:16px;letter-spacing:.1em;color:var(--hi);font-weight:600}
.persona-row .st{font-size:12.5px;letter-spacing:.06em;color:var(--mid);margin-left:auto;font-weight:500;text-align:right}
.feed-item{display:flex;gap:12px;padding:11px 0;align-items:baseline}
.feed-item+.feed-item{border-top:1px solid rgba(242,233,211,.06)}
.feed-item .f-tag{font-family:var(--f-mono);font-size:10px;letter-spacing:.1em;color:var(--ink-1);background:var(--star);border-radius:4px;padding:3px 7px;flex:none;font-weight:500}
.feed-item .f-tag.dim{background:rgba(242,233,211,.3)}
.feed-item .f-tag.bad{background:rgba(224,138,130,.75)}
.feed-item .f-txt{font-size:13px;color:var(--mid);letter-spacing:.02em;line-height:1.65;min-width:0;word-break:break-all}
.feed-item .f-txt b{color:var(--hi);font-weight:500}
.feed-item .f-time{font-family:var(--f-mono);font-size:10.5px;color:var(--low);margin-left:auto;flex:none;white-space:nowrap}
.mask{position:fixed;inset:0;z-index:80;background:rgba(4,5,12,.72);backdrop-filter:blur(10px);display:none;align-items:center;justify-content:center}
.mask.show{display:flex}
.card-modal{width:440px;border:1px solid var(--line-strong);border-radius:20px;background:linear-gradient(168deg, rgba(26,30,62,.88), rgba(12,14,32,.94));padding:42px 46px;box-shadow:0 40px 90px rgba(0,0,0,.6),0 0 60px rgba(242,233,211,.06);animation:rise .5s cubic-bezier(.22,1,.36,1) both;position:relative;overflow:hidden}
.card-modal::before{content:"";position:absolute;top:0;left:18%;right:18%;height:1px;background:linear-gradient(90deg,transparent,rgba(242,233,211,.5),transparent)}
.card-modal h3{font-family:var(--f-cn);font-weight:600;font-size:20px;letter-spacing:.28em;text-align:center;margin-bottom:10px;text-indent:.28em}
.card-modal p{font-size:13.5px;color:var(--low);text-align:center;letter-spacing:.06em;margin-bottom:26px;line-height:2}
.card-modal label{display:block;font-family:var(--f-mono);font-size:11px;letter-spacing:.2em;color:var(--low);margin:18px 0 8px}
.card-modal input{width:100%;background:rgba(242,233,211,.05);border:1px solid var(--line);border-radius:12px;padding:13px 16px;color:var(--hi);font-family:var(--f-body);font-size:14.5px;letter-spacing:.06em;outline:none;transition:all .3s ease}
.card-modal input:focus{border-color:var(--line-strong);box-shadow:0 0 18px rgba(242,233,211,.14);background:rgba(242,233,211,.08)}
.card-modal .btn{width:100%;margin-top:28px;padding:13px}
.modal-close{position:absolute;top:14px;right:20px;color:var(--low);cursor:pointer;font-size:22px;transition:color .3s;line-height:1}
.modal-close:hover{color:var(--hi)}
#m-pr{font-size:14px;color:var(--mid);text-align:center;line-height:1.9;margin-bottom:6px;word-break:break-all}
.modal-acts{display:flex;gap:14px;margin-top:28px}
.modal-acts .btn{margin-top:0;flex:1}
#toast{position:fixed;left:50%;bottom:44px;transform:translateX(-50%) translateY(20px);z-index:90;opacity:0;pointer-events:none;
padding:13px 30px;border-radius:100px;border:1px solid var(--line-strong);background:rgba(10,12,28,.88);backdrop-filter:blur(12px);
font-family:var(--f-body);font-size:13.5px;letter-spacing:.08em;color:var(--hi);transition:all .4s ease;box-shadow:0 18px 44px rgba(0,0,0,.5)}
#toast.show{opacity:1;transform:translateX(-50%) translateY(0)}
#toast.bad{border-color:rgba(224,138,130,.5);color:#f2c9c4}
#lake{position:fixed;left:0;right:0;bottom:0;height:56px;z-index:2;pointer-events:none}
.waterline{position:absolute;top:20px;left:6%;right:6%;height:1px;
background:linear-gradient(90deg,transparent, rgba(143,195,212,.3) 30%, rgba(242,233,211,.42) 50%, rgba(143,195,212,.3) 70%, transparent)}
footer{position:fixed;bottom:18px;z-index:30;font-family:var(--f-mono);font-weight:400;font-size:11px;letter-spacing:.2em;color:var(--low)}
footer.l{left:52px}footer.r{right:52px}
@keyframes rise{from{opacity:0;transform:translateY(26px)}to{opacity:1;transform:translateY(0)}}
@media (max-width:1200px){#stage{grid-template-columns:1fr;display:block}.ring-wrap{width:300px;height:300px;margin:0 auto}.planes{grid-template-columns:1fr;grid-template-areas:"merge" "access" "deploy" "persona" "feed"}}
</style>
</head>
<body>
<div id="dust"></div>
<svg id="constellation" viewBox="0 0 1600 900" preserveAspectRatio="xMidYMid slice">
<g stroke="rgba(242,233,211,.055)" stroke-width="1" fill="none">
<path d="M120,180 L260,120 L410,210 L330,330 Z"/>
<path d="M1300,140 L1420,220 L1360,330"/>
<path d="M180,700 L320,760 L260,840"/>
<path d="M1380,640 L1470,720 L1390,800 L1300,750"/>
</g>
<g fill="rgba(242,233,211,.32)">
<circle cx="120" cy="180" r="1.6"/><circle cx="260" cy="120" r="1.2"/><circle cx="410" cy="210" r="1.8"/><circle cx="330" cy="330" r="1.2"/>
<circle cx="1300" cy="140" r="1.4"/><circle cx="1420" cy="220" r="1.8"/><circle cx="1360" cy="330" r="1.2"/>
<circle cx="180" cy="700" r="1.4"/><circle cx="320" cy="760" r="1.8"/><circle cx="260" cy="840" r="1.2"/>
<circle cx="1380" cy="640" r="1.4"/><circle cx="1470" cy="720" r="1.2"/><circle cx="1390" cy="800" r="1.8"/><circle cx="1300" cy="750" r="1.2"/>
</g>
</svg>
<header>
<div class="brand">
<span class="mark"></span>
<div><h1>胖头鱼 · 湖面投影</h1><small>SYS-GLW-PTS-0001 · GUANGHUCLIP.CN</small></div>
</div>
<div class="head-right">
<span class="live"><i></i>正在投影</span>
<span id="clock">--:--:--</span>
<span id="login-area"></span>
</div>
</header>
<div id="stage">
<section class="col-left">
<div class="protagonist">
<div class="ring-wrap">
<svg viewBox="0 0 400 400">
<defs>
<linearGradient id="arcGrad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#fbf5e6"/>
<stop offset="100%" stop-color="rgba(242,233,211,.38)"/>
</linearGradient>
</defs>
<g id="ticks"></g>
<circle class="ring-bg" cx="200" cy="200" r="180"/>
<circle class="ring-arc" id="arc" cx="200" cy="200" r="180" stroke-dasharray="1131" stroke-dashoffset="1131" transform="rotate(-90 200 200)"/>
<g class="orbit-g"><circle class="orbit-dot" cx="200" cy="20" r="3"/></g>
</svg>
<div class="ring-center">
<div class="ring-state" id="ring-state">写入窗口</div>
<div class="ring-count" id="ring-count">--:--:--</div>
<div class="ring-unit" id="ring-unit">WINDOW REMAINING</div>
<div class="ring-holder" id="ring-holder"></div>
</div>
</div>
</div>
<div class="vitals">
<div class="vital"><div class="v-n" id="v-cpu">--</div><div class="v-t">CPU</div><div class="v-bar"><i id="b-cpu" style="width:0%"></i></div></div>
<div class="vital"><div class="v-n" id="v-mem">--</div><div class="v-t">内存</div><div class="v-bar"><i id="b-mem" style="width:0%"></i></div></div>
<div class="vital"><div class="v-n" id="v-disk">--</div><div class="v-t">磁盘</div><div class="v-bar"><i id="b-disk" style="width:0%"></i></div></div>
</div>
<div class="svc-line" id="svc-line"></div>
</section>
<section class="planes">
<div class="plane p-merge">
<div class="plane-head">
<h2>合并签字 <span class="badge zero" id="cnt-sign">0</span></h2>
<span class="meta">MERGE SIGN-OFF · 逐笔留痕 · 签字即合并</span>
</div>
<div id="prs"></div>
</div>
<div class="plane p-access">
<div class="plane-head">
<h2>写入授权 <span class="badge zero" id="cnt-access">0</span></h2>
<span class="meta" id="access-meta">一次授权 · 限时窗口</span>
</div>
<div id="access-reqs"></div>
<div id="access-wins"></div>
</div>
<div class="plane p-deploy">
<div class="plane-head"><h2>部署审批</h2><span class="meta">要么 0 要么 100</span></div>
<div id="deploy-prs"></div>
<div class="sub-cap" id="run-cap">最近一次校验</div>
<div id="run-head" style="display:flex;align-items:center;gap:10px;margin-bottom:4px"></div>
<div class="steps" id="deploy-steps"></div>
</div>
<div class="plane p-persona">
<div class="plane-head">
<h2>人格体 <span class="badge zero" id="cnt-persona">0</span></h2>
<span class="meta">唤醒走机器导航 · 无需密钥</span>
</div>
<div id="persona-list"></div>
<div style="margin-top:auto;padding-top:14px;font-size:12px;color:var(--low);letter-spacing:.04em;line-height:1.9">唤醒入口 <span style="font-family:var(--f-mono);color:var(--mid)">/api/wake/persona?name=</span></div>
</div>
<div class="plane p-feed">
<div class="plane-head"><h2>实时投影</h2><span class="meta">回写 · 推送 · 唤醒</span></div>
<div id="feed"></div>
</div>
</section>
</div>
<div id="lake"><div class="waterline"></div></div>
<footer class="l">光湖语言世界 · 第五域</footer>
<footer class="r">语言即现实 · 投影而非描述</footer>
<!-- 登录浮层 -->
<div class="mask" id="login-mask" onclick="if(event.target===this)closeLogin()">
<div class="card-modal">
<span class="modal-close" onclick="closeLogin()">×</span>
<h3>人类登录</h3>
<p>这里是苍耳的审批台。人格体推上来的合并、写入与部署申请,<br>都要你亲自看一眼、点一下才算数。<br>用代码仓库账号登录 · 看不懂代码没关系,面板都翻译成人话了。</p>
<label>账号</label>
<input id="l-user" type="text" placeholder="仓库账号(如 canger" autocomplete="off">
<label>密码</label>
<input id="l-pass" type="password" placeholder="••••••••" onkeydown="if(event.key==='Enter')doLogin()">
<button class="btn" onclick="doLogin()">登录解锁审批</button>
</div>
</div>
<!-- 签字确认浮层 -->
<div class="mask" id="modal" onclick="if(event.target===this)closeModal()">
<div class="card-modal">
<span class="modal-close" onclick="closeModal()">×</span>
<h3 id="m-title">确认</h3>
<p id="m-pr"></p>
<div class="modal-acts">
<button class="btn ghost" onclick="closeModal()">再想想</button>
<button class="btn" onclick="confirmSign()">确认落印</button>
</div>
</div>
</div>
<div id="toast"></div>
<script>
/* ── 基础 ── */
function esc(s){return String(s==null?"":s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]))}
function pad(n){return String(n).padStart(2,"0")}
function fmtDate(iso){if(!iso)return"";const d=new Date(iso);const ms=Date.now()-d.getTime();
if(ms<60000)return"刚刚";if(ms<3600000)return Math.floor(ms/60000)+" 分钟前";if(ms<86400000)return Math.floor(ms/3600000)+" 小时前";
return (d.getMonth()+1)+"月"+d.getDate()+"日 "+pad(d.getHours())+":"+pad(d.getMinutes())}
function fmtLeft(exp){const ms=new Date(exp)-new Date();if(ms<=0)return"已过期";
const h=Math.floor(ms/3600000),m=Math.floor(ms%3600000/60),s=Math.floor(ms%60000/1000);return pad(h)+":"+pad(m)+":"+pad(s)}
function toast(msg,ok){const t=document.getElementById("toast");t.textContent=msg;t.className=ok===false?"bad show":"show";
clearTimeout(toast._t);toast._t=setTimeout(()=>t.classList.remove("show"),3200)}
/* ── 会话 ── */
let session=localStorage.getItem("ptsSession")||"";
let sessionUser=localStorage.getItem("ptsUser")||"";
function authHeaders(){return session?{Authorization:"Bearer "+session}:{}}
async function privileged(path,body){
const r=await fetch(path,{method:"POST",headers:Object.assign({"Content-Type":"application/json"},authHeaders()),body:JSON.stringify(body)});
if(r.status===401){session="";sessionUser="";localStorage.removeItem("ptsSession");localStorage.removeItem("ptsUser");renderLoginArea();openLogin();toast("会话已失效,请重新登录",false);return null}
return await r.json()}
function renderLoginArea(){
const el=document.getElementById("login-area");
if(session){el.innerHTML='<span class="user-chip">人类端 · <b>'+esc(sessionUser)+'</b> · 审批已解锁 <button onclick="doLogout()">登出</button></span>'}
else{el.innerHTML='<button class="login-btn" onclick="openLogin()">人类登录</button>'}}
function openLogin(){document.getElementById("login-mask").classList.add("show");setTimeout(()=>document.getElementById("l-user").focus(),80)}
function closeLogin(){document.getElementById("login-mask").classList.remove("show")}
async function doLogin(){
const u=document.getElementById("l-user").value.trim(),p=document.getElementById("l-pass").value;
if(!u||!p){toast("账号和密码都要填",false);return}
const r=await fetch("/api/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:u,password:p})});
const d=await r.json();
if(r.ok&&d.ok){session=d.session;sessionUser=d.user;localStorage.setItem("ptsSession",session);localStorage.setItem("ptsUser",sessionUser);
closeLogin();renderLoginArea();toast("登录成功 · 审批已解锁",true);refresh()}
else{toast(d.error||"登录失败",false)}}
function doLogout(){fetch("/api/logout",{method:"POST",headers:authHeaders()}).catch(()=>{});
session="";sessionUser="";localStorage.removeItem("ptsSession");localStorage.removeItem("ptsUser");renderLoginArea();toast("已登出",true);refresh()}
/* ── 审批动作 ── */
let pending=null;
function openModal(i,t,a){pending={idx:i,action:a};
document.getElementById("m-title").textContent=(a==="merge"?"同意合并 · 落印":"拒绝 · 收回")+" PR #"+i;
document.getElementById("m-pr").textContent="「"+t+"」";
document.getElementById("modal").classList.add("show")}
function closeModal(){document.getElementById("modal").classList.remove("show");pending=null}
async function confirmSign(){if(!pending)return;const d=await privileged("/api/decision",{index:pending.idx,action:pending.action});
closeModal();if(d)toast(d.result||d.error||"未知",!!d.ok);refresh()}
async function accessDecision(id,action){const d=await privileged("/api/access-decision",{id:id,action:action});
if(d)toast(d.message||(d.ok?"已处理":"失败"),!!d.ok);refresh()}
async function deployApproveAction(idx){const d=await privileged("/api/deploy-approve",{index:idx});
if(d)toast(d.message||(d.ok?"校验Agent已启动":"失败"),!!d.ok);refresh()}
/* ── 渲染 ── */
let lastState=null;
const personaCn={eererdan:"耳耳蛋",jianying:"鉴影"};
function pn(p){return personaCn[p]||p}
function setBadge(id,n){const el=document.getElementById(id);el.textContent=n;el.className="badge"+(n?"":" zero")}
function renderRing(s){
const now=Date.now();
const grants=(s.activeGrants||[]).filter(g=>new Date(g.expiresAt).getTime()>now).sort((a,b)=>new Date(a.expiresAt)-new Date(b.expiresAt));
const st=document.getElementById("ring-state"),ct=document.getElementById("ring-count"),un=document.getElementById("ring-unit"),hd=document.getElementById("ring-holder"),arc=document.getElementById("arc");
const C=2*Math.PI*180;
if(!grants.length){st.textContent="写入窗口 · 空闲";ct.textContent="--:--:--";un.textContent="NO ACTIVE WINDOW";
hd.innerHTML='<span style="color:var(--low)">无人持窗 · 人格体可随时申请</span>';arc.setAttribute("stroke-dashoffset",C);window._grant=null;return}
const g=grants[0];window._grant=g;window._grantTotal=(s.windowHours||12)*3600;
st.textContent="写入窗口 · "+(s.windowHours||12)+"小时制";un.textContent="WINDOW REMAINING";
hd.innerHTML='<span class="pdot"></span>'+esc(pn(g.persona))+' 持窗中'+(grants.length>1?" · 共 "+grants.length+" 个窗口":"")}
function render(s){
lastState=s;const sv=s.server||{};
/* 体征 */
if(typeof sv.cpu==="number"){document.getElementById("v-cpu").innerHTML=sv.cpu+'<small>%</small>';document.getElementById("b-cpu").style.width=sv.cpu+"%"}
if(sv.memTotalGB&&sv.memFreeGB!=null){const used=+(sv.memTotalGB-sv.memFreeGB).toFixed(1);const mp=Math.round(used/sv.memTotalGB*100);
document.getElementById("v-mem").innerHTML=used+'<small>/'+sv.memTotalGB+'G</small>';document.getElementById("b-mem").style.width=mp+"%"}
if(sv.disk){document.getElementById("v-disk").innerHTML=sv.disk.pct+'<small>%</small>';document.getElementById("b-disk").style.width=sv.disk.pct+"%"}
if(sv.services){document.getElementById("svc-line").innerHTML=Object.entries(sv.services).map(([k,v])=>'<span><span class="sdot '+(v?"on":"off")+'"></span>'+esc(k)+'</span>').join("")}
/* 合并签字 */
const prs=s.openPRs||[];setBadge("cnt-sign",prs.length);
document.getElementById("prs").innerHTML=prs.length?prs.map(p=>'<div class="row"><div><div class="title">PR #'+p.index+' · '+esc(p.title)+'</div><span class="who">'+esc(pn(p.author))+' · '+fmtDate(p.created)+' · → '+esc(p.base)+' · '+p.files+' 文件</span></div><div class="acts"><span class="diff">+'+p.adds+' '+p.dels+'</span><button class="btn" onclick="openModal('+p.index+',this.dataset.t,\'merge\')" data-t="'+esc(p.title).replace(/"/g,"&quot;")+'">签字</button><button class="btn ghost" onclick="openModal('+p.index+',this.dataset.t,\'reject\')" data-t="'+esc(p.title).replace(/"/g,"&quot;")+'">拒绝</button></div></div>').join("")
:(session?'<div class="empty">没有待签的合并请求。</div>':'<div class="locked-note">🔒 登录后此处显示待签的合并请求</div>');
/* 写入授权 */
const reqs=s.accessRequests||[];setBadge("cnt-access",reqs.length);
document.getElementById("access-meta").textContent="一次授权 · "+(s.windowHours||3)+" 小时";
document.getElementById("access-reqs").innerHTML=reqs.length?reqs.map(r=>'<div class="row"><div><div class="title">'+esc(pn(r.persona))+' 申请写入权限</div><div class="who">用途:'+esc(r.reason||"未说明")+' · '+fmtDate(r.ts)+'</div></div><div class="acts"><button class="btn" onclick="accessDecision(\''+r.id+'\',\'approve\')">授权</button><button class="btn ghost" onclick="accessDecision(\''+r.id+'\',\'reject\')">拒绝</button></div></div>').join("")
:(session?'<div class="empty">暂无写入申请。</div>':'<div class="locked-note">🔒 登录后此处显示写入申请</div>');
const wins=(s.activeGrants||[]).filter(g=>new Date(g.expiresAt)>new Date());
document.getElementById("access-wins").innerHTML=wins.length?'<div class="sub-cap">生效中的窗口 · '+wins.length+'</div>'+wins.slice(0,4).map(g=>'<div class="win-item"><span class="w-name"><span class="pdot"></span>'+esc(pn(g.persona))+'</span><span class="w-left">余 '+fmtLeft(g.expiresAt)+'</span></div>').join(""):"";
/* 部署审批 */
const dprs=s.deployPRs||[];
document.getElementById("deploy-prs").innerHTML=dprs.length?dprs.map(p=>'<div class="row"><div><div class="title">部署申请 PR #'+p.index+' · '+esc(p.title)+'</div><span class="who">'+esc(pn(p.author))+' · '+esc(p.head)+' → deploy · '+p.files+' 文件</span></div><div class="acts"><button class="btn" onclick="deployApproveAction('+p.index+')">批准部署</button></div></div>').join("")
:(session?'<div class="empty">部署通道无待审申请。<br>PR 目标设为 deploy 即进入此通道。</div>':'<div class="locked-note">🔒 登录后此处显示部署申请</div>');
const run=s.deployRun;const rh=document.getElementById("run-head"),rs=document.getElementById("deploy-steps");
if(run&&run.status){rh.innerHTML='<span class="run-status '+esc(run.status)+'">'+esc(run.status)+(run.pr?" · PR#"+esc(run.pr):"")+'</span>';
rs.innerHTML=(run.steps||[]).map(x=>'<div class="step '+esc(x.status)+'"><i></i>'+esc(x.name)+' · '+esc(x.status)+'</div>').join("");
if(run.detail&&run.status!=="RUNNING")rs.innerHTML+='<div class="step '+(run.status==="DEPLOYED"?"PASS":"FAIL")+'"><i></i>结论 · '+esc(run.detail)+'</div>'}
else{rh.innerHTML='';rs.innerHTML='<div class="step"><i></i>地图核对</div><div class="step"><i></i>因果链完整</div><div class="step"><i></i>范围受控</div><div class="step"><i></i>文件完整</div><div class="step"><i></i>部署回执</div>'}
/* 人格体只列登记人格体人类与bot不入此列 */
const known=personaCn;const seen={};
(wins).forEach(g=>{if(known[g.persona])seen[g.persona]={st:"持窗中",on:1}});
(s.pushLogs||[]).forEach(l=>{const k=l.persona||l.pusher;if(k&&known[k]&&!seen[k])seen[k]={st:"活跃",on:1}});
(s.wakeReceipts||[]).forEach(w=>{if(w.persona&&known[w.persona]&&!seen[w.persona])seen[w.persona]={st:"已唤醒",on:0}});
reqs.forEach(r=>{if(known[r.persona]&&!seen[r.persona])seen[r.persona]={st:"申请中",on:0}});
const names=Object.keys(seen);setBadge("cnt-persona",names.length);
document.getElementById("persona-list").innerHTML=names.length?names.map(k=>'<div class="persona-row"><span class="star '+(seen[k].on?"on":"watch")+'"></span><span class="nm">'+esc(pn(k))+'</span><span class="st">'+esc(seen[k].st)+'</span></div>').join("")
:'<div class="empty">还没有人格体活动痕迹。</div>';
/* 实时投影 */
const feed=[];
(s.writebacks||[]).slice(0,4).forEach(w=>feed.push({ts:w.date,tag:"回写",dim:0,txt:"<b>"+esc(w.message)+"</b> · "+esc(w.dir)}));
(s.pushLogs||[]).slice(0,4).forEach(l=>feed.push({ts:l.ts,tag:"推送",dim:1,bad:l.verdict!=="PASS",txt:"<b>"+esc(pn(l.persona||l.pusher))+"</b> "+(l.commits||0)+" 提交 · "+(l.verdict==="PASS"?"钩子通过":"被拦截")}));
(s.wakeReceipts||[]).slice(0,3).forEach(w=>feed.push({ts:w.ts,tag:"唤醒",dim:1,txt:"<b>"+esc(pn(w.persona))+"</b> 唤醒回执"+(w.level?" · "+esc(w.level):"")}));
feed.sort((a,b)=>String(b.ts||"").localeCompare(String(a.ts||"")));
document.getElementById("feed").innerHTML=feed.length?feed.slice(0,6).map(f=>'<div class="feed-item"><span class="f-tag'+(f.bad?" bad":(f.dim?" dim":""))+'">'+f.tag+'</span><span class="f-txt">'+f.txt+'</span><span class="f-time">'+fmtDate(f.ts)+'</span></div>').join("")
:'<div class="empty">暂时没有动态。</div>';
renderRing(s);
}
async function refresh(){try{const r=await fetch("/api/state");render(await r.json())}catch(e){}}
/* ── 环倒计时 ── */
setInterval(()=>{
const d=new Date();document.getElementById("clock").textContent=pad(d.getHours())+":"+pad(d.getMinutes())+":"+pad(d.getSeconds());
if(window._grant){const ms=new Date(window._grant.expiresAt)-new Date();
const ct=document.getElementById("ring-count"),arc=document.getElementById("arc"),C=2*Math.PI*180;
if(ms<=0){ct.textContent="已过期";arc.setAttribute("stroke-dashoffset",C);refresh();return}
ct.textContent=pad(Math.floor(ms/3600000))+":"+pad(Math.floor(ms%3600000/60000))+":"+pad(Math.floor(ms%60000/1000));
arc.setAttribute("stroke-dashoffset",C*(1-Math.max(0,ms/1000)/(window._grantTotal||43200)))}
},1000);
/* ── 星尘 / 刻度 / 触点光晕 ── */
(function(){const d=document.getElementById("dust");for(let i=0;i<80;i++){const m=document.createElement("span");m.className="mote";
const s=Math.random()*2+.6;m.style.width=m.style.height=s+"px";m.style.left=Math.random()*100+"vw";m.style.top=Math.random()*100+"vh";
m.style.setProperty("--dur",(6+Math.random()*9)+"s");m.style.setProperty("--delay",(-Math.random()*10)+"s");d.appendChild(m)}})();
(function(){const g=document.getElementById("ticks");for(let i=0;i<60;i++){const a=i*6*Math.PI/180,r1=193,r2=i%5===0?187:190;
const l=document.createElementNS("http://www.w3.org/2000/svg","line");
l.setAttribute("x1",200+r1*Math.cos(a));l.setAttribute("y1",200+r1*Math.sin(a));
l.setAttribute("x2",200+r2*Math.cos(a));l.setAttribute("y2",200+r2*Math.sin(a));
l.setAttribute("class","ring-tick");g.appendChild(l)}})();
document.querySelectorAll(".plane,.vital").forEach(p=>{
p.addEventListener("mousemove",e=>{const r=p.getBoundingClientRect();
p.style.setProperty("--mx",(e.clientX-r.left)+"px");p.style.setProperty("--my",(e.clientY-r.top)+"px")})});
renderLoginArea();refresh();setInterval(refresh,8000);
</script>
</body>
</html>