1355 lines
68 KiB
HTML
1355 lines
68 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=5.0,viewport-fit=cover">
|
||
<meta name="description" content="光湖灯塔 · 语言驱动操作系统 AGE OS">
|
||
<meta name="theme-color" content="#080c18">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||
<title>光湖灯塔 · AGE OS</title>
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2280%22>🏛️</text></svg>">
|
||
<style>
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
|
||
/* ═══ Themes ═══ */
|
||
:root{
|
||
--bg:#060a14;--bg1:#0c1220;--bg2:#111827;--bg3:#1a2332;
|
||
--border:#1e2d44;--accent:#4f8ef7;--accent2:#7c6ef7;--accent3:#a78bfa;
|
||
--ok:#34d399;--warn:#fbbf24;--err:#f87171;
|
||
--text:#e2e8f0;--dim:#64748b;--muted:#334155;
|
||
--glow:0 0 24px rgba(79,142,247,.1);
|
||
--r:12px;--rs:8px;
|
||
--f:-apple-system,BlinkMacSystemFont,'PingFang SC','Helvetica Neue','Microsoft YaHei',sans-serif;
|
||
--m:'SF Mono','Fira Code',Consolas,monospace;
|
||
--sb:env(safe-area-inset-bottom,0px);--st:env(safe-area-inset-top,0px);
|
||
}
|
||
.t-twi{--bg:#0a0612;--bg1:#140e22;--bg2:#1a1430;--bg3:#241c3d;--accent:#c084fc;--accent2:#e879f9;--accent3:#f0abfc;--border:#2d1f4e}
|
||
.t-sea{--bg:#041318;--bg1:#082029;--bg2:#0c2d3a;--bg3:#103a4c;--accent:#22d3ee;--accent2:#06b6d4;--accent3:#67e8f9;--border:#164e63}
|
||
.t-for{--bg:#06120a;--bg1:#0c2014;--bg2:#112d1c;--bg3:#183a26;--accent:#34d399;--accent2:#4ade80;--accent3:#86efac;--border:#1a4a2e}
|
||
.t-emb{--bg:#140a06;--bg1:#221208;--bg2:#2d1a0e;--bg3:#3a2414;--accent:#fb923c;--accent2:#f97316;--accent3:#fdba74;--border:#4a2e1a}
|
||
|
||
html{-webkit-tap-highlight-color:transparent}
|
||
body{font-family:var(--f);background:var(--bg);color:var(--text);font-size:15px;line-height:1.6;overflow:hidden;height:100vh;height:100dvh;-webkit-font-smoothing:antialiased}
|
||
a{color:var(--accent);text-decoration:none}
|
||
::-webkit-scrollbar{width:4px}::-webkit-scrollbar-thumb{background:var(--muted);border-radius:2px}
|
||
|
||
/* ═══ Background ═══ */
|
||
.bg{position:fixed;inset:0;z-index:0;pointer-events:none}
|
||
.bg::before{content:'';position:absolute;inset:0;background-image:linear-gradient(rgba(79,142,247,.02) 1px,transparent 1px),linear-gradient(90deg,rgba(79,142,247,.02) 1px,transparent 1px);background-size:50px 50px;animation:gm 30s linear infinite}
|
||
@keyframes gm{to{background-position:50px 50px}}
|
||
.orb{position:absolute;border-radius:50%;filter:blur(80px);opacity:.07;animation:ob 22s ease-in-out infinite}
|
||
.orb:nth-child(1){width:350px;height:350px;background:var(--accent);top:-8%;left:-4%}
|
||
.orb:nth-child(2){width:280px;height:280px;background:var(--accent2);bottom:-6%;right:-4%;animation-delay:8s}
|
||
@keyframes ob{0%,100%{transform:translate(0,0)}50%{transform:translate(20px,-15px)}}
|
||
|
||
/* ═══ 3D Page Flip Engine ═══ */
|
||
.pages{position:fixed;inset:0;z-index:1;perspective:1400px;overflow:hidden}
|
||
.page{
|
||
position:absolute;inset:0;
|
||
overflow-y:auto;overflow-x:hidden;
|
||
-webkit-overflow-scrolling:touch;
|
||
padding:0 0 var(--sb) 0;
|
||
background:var(--bg);
|
||
backface-visibility:hidden;-webkit-backface-visibility:hidden;
|
||
transition:transform .55s cubic-bezier(.4,0,.15,1),opacity .55s;
|
||
transform-origin:center center;
|
||
will-change:transform,opacity;
|
||
}
|
||
.page[data-state="active"]{transform:rotateY(0deg) scale(1);opacity:1;z-index:2;pointer-events:auto}
|
||
.page[data-state="left"]{transform:rotateY(-92deg) scale(.92);opacity:0;z-index:1;pointer-events:none}
|
||
.page[data-state="right"]{transform:rotateY(92deg) scale(.92);opacity:0;z-index:1;pointer-events:none}
|
||
.page[data-state="hidden"]{transform:rotateY(92deg) scale(.88);opacity:0;z-index:0;pointer-events:none;transition:none}
|
||
|
||
/* ═══ Common UI ═══ */
|
||
.bar{position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:10px 16px;background:rgba(6,10,20,.92);backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);border-bottom:1px solid var(--border);min-height:48px;padding-top:calc(10px + var(--st))}
|
||
.bar-title{font-size:14px;font-weight:700;display:flex;align-items:center;gap:6px}
|
||
.bar-right{display:flex;align-items:center;gap:8px}
|
||
.dot{width:6px;height:6px;border-radius:50%;background:var(--ok);box-shadow:0 0 6px var(--ok);animation:pls 2s infinite;flex-shrink:0}
|
||
@keyframes pls{50%{opacity:.4}}
|
||
.btn{background:none;border:1px solid var(--border);color:var(--dim);padding:5px 12px;border-radius:var(--rs);font-size:12px;cursor:pointer;font-family:var(--f);transition:all .2s;white-space:nowrap}
|
||
.btn:hover{color:var(--accent);border-color:var(--accent)}
|
||
.btn:active{transform:scale(.96)}
|
||
.btn-accent{background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;color:#fff}
|
||
.btn-accent:hover{opacity:.88;color:#fff}
|
||
.content{padding:16px;max-width:700px;margin:0 auto}
|
||
.card{background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);padding:16px;margin-bottom:12px;transition:border-color .3s}
|
||
.card:hover{border-color:var(--accent)}
|
||
.tag{display:inline-block;padding:2px 8px;border-radius:20px;font-size:10px;font-weight:600}
|
||
.tag-ok{background:rgba(52,211,153,.12);color:var(--ok)}
|
||
.tag-warn{background:rgba(251,191,36,.12);color:var(--warn)}
|
||
.tag-info{background:rgba(79,142,247,.12);color:var(--accent)}
|
||
.empty{text-align:center;padding:40px 16px;color:var(--muted);font-size:14px}
|
||
.empty em{font-style:normal;font-size:40px;display:block;margin-bottom:12px}
|
||
input[type=text],textarea{background:var(--bg2);border:1px solid var(--border);border-radius:var(--rs);color:var(--text);padding:10px 12px;font-size:14px;font-family:var(--f);outline:none;width:100%;transition:border-color .2s;-webkit-appearance:none}
|
||
input[type=text]:focus,textarea:focus{border-color:var(--accent)}
|
||
::placeholder{color:var(--muted)}
|
||
label{display:block;font-size:12px;color:var(--dim);margin-bottom:4px;font-weight:600}
|
||
.form-row{margin-bottom:14px}
|
||
.form-row-inline{display:flex;gap:10px}
|
||
@media(max-width:480px){.form-row-inline{flex-direction:column}}
|
||
|
||
/* ═══ PAGE: Home ═══ */
|
||
.home-hero{text-align:center;padding:32px 16px}
|
||
.home-ver{display:inline-flex;align-items:center;gap:6px;padding:4px 12px;border-radius:20px;background:rgba(79,142,247,.08);border:1px solid rgba(79,142,247,.12);font-size:11px;color:var(--accent);font-family:var(--m);margin-bottom:16px}
|
||
.home-hero h1{font-size:clamp(26px,5.5vw,40px);font-weight:800;line-height:1.2;margin-bottom:6px}
|
||
.home-hero .sub{font-size:clamp(12px,2.5vw,15px);color:var(--dim);margin-bottom:12px}
|
||
.home-hero .quote{font-size:13px;color:var(--accent2);font-style:italic}
|
||
.upd{display:flex;gap:10px;padding:12px;margin-bottom:8px;background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);transition:border-color .3s}
|
||
.upd:hover{border-color:var(--accent)}
|
||
.upd-v{min-width:42px;padding:3px 6px;text-align:center;background:rgba(79,142,247,.08);border-radius:var(--rs);color:var(--accent);font-size:10px;font-weight:600;font-family:var(--m);height:fit-content;flex-shrink:0}
|
||
.upd h4{font-size:13px;font-weight:600;margin-bottom:2px}
|
||
.upd p{font-size:11px;color:var(--dim);line-height:1.5}
|
||
.home-enter{padding:24px;text-align:center;background:linear-gradient(135deg,var(--bg1),var(--bg2));border:1px solid var(--border);border-radius:var(--r);cursor:pointer;transition:all .3s;margin:20px 0}
|
||
.home-enter:hover{border-color:var(--accent);box-shadow:var(--glow)}
|
||
.home-enter:active{transform:scale(.98)}
|
||
.home-enter em{font-style:normal;font-size:36px;display:block;margin-bottom:8px}
|
||
.home-enter strong{font-size:17px;display:block;margin-bottom:4px}
|
||
.home-enter span{font-size:12px;color:var(--dim)}
|
||
.home-footer{text-align:center;padding:24px 0;font-size:11px;color:var(--muted);border-top:1px solid var(--border);margin-top:24px}
|
||
.home-footer .motto{color:var(--accent2);font-style:italic;margin-top:4px}
|
||
|
||
/* ═══ PAGE: Desk (作品列表) ═══ */
|
||
.work-item{cursor:pointer}
|
||
.work-item h3{font-size:15px;font-weight:600;margin-bottom:4px}
|
||
.work-item .meta{font-size:11px;color:var(--dim);display:flex;gap:10px;flex-wrap:wrap}
|
||
.work-item .meta span{display:flex;align-items:center;gap:3px}
|
||
|
||
/* ═══ PAGE: Work Hub (作品导航) ═══ */
|
||
.hub-title{text-align:center;padding:20px 0 12px}
|
||
.hub-title h2{font-size:20px;font-weight:700}
|
||
.hub-title p{font-size:12px;color:var(--dim);margin-top:2px}
|
||
.hub-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||
@media(max-width:360px){.hub-grid{grid-template-columns:1fr}}
|
||
.hub-card{text-align:center;padding:20px 12px;background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);cursor:pointer;transition:all .3s}
|
||
.hub-card:hover{border-color:var(--accent);box-shadow:var(--glow)}
|
||
.hub-card:active{transform:scale(.97)}
|
||
.hub-card em{font-style:normal;font-size:28px;display:block;margin-bottom:6px}
|
||
.hub-card strong{font-size:14px;display:block;margin-bottom:3px}
|
||
.hub-card span{font-size:11px;color:var(--dim)}
|
||
.hub-stats{display:flex;justify-content:center;gap:20px;padding:12px 0;flex-wrap:wrap}
|
||
.hub-stat{text-align:center}
|
||
.hub-stat b{display:block;font-size:18px;color:var(--accent);font-family:var(--m)}
|
||
.hub-stat small{font-size:10px;color:var(--dim)}
|
||
|
||
/* ═══ PAGE: Outline ═══ */
|
||
.outline-editor{min-height:250px;resize:vertical;line-height:1.9;font-size:14px}
|
||
.vol-item{display:flex;align-items:center;justify-content:space-between;padding:10px;background:var(--bg2);border-radius:var(--rs);margin-bottom:6px}
|
||
.vol-item span{font-size:13px;font-weight:600}
|
||
.vol-actions{display:flex;gap:4px}
|
||
|
||
/* ═══ PAGE: Characters ═══ */
|
||
.char-card{display:flex;gap:12px;align-items:flex-start}
|
||
.char-avatar{width:42px;height:42px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-size:18px;flex-shrink:0}
|
||
.char-info h4{font-size:14px;font-weight:600;margin-bottom:2px}
|
||
.char-info p{font-size:12px;color:var(--dim);line-height:1.5}
|
||
.char-role{font-size:10px;color:var(--accent);font-weight:600}
|
||
|
||
/* ═══ PAGE: Editor ═══ */
|
||
.editor-info{display:flex;justify-content:space-between;align-items:center;padding:8px 0;font-size:12px;color:var(--dim);border-bottom:1px solid var(--border);margin-bottom:12px}
|
||
.editor-textarea{min-height:calc(100vh - 260px);min-height:calc(100dvh - 260px);resize:none;line-height:2;font-size:15px;border:none;background:transparent;padding:0}
|
||
.editor-textarea:focus{border:none}
|
||
.editor-bottom{position:sticky;bottom:0;padding:8px 0;background:var(--bg);display:flex;justify-content:space-between;align-items:center;border-top:1px solid var(--border);font-size:11px;color:var(--dim);font-family:var(--m)}
|
||
|
||
/* ═══ Chat FAB ═══ */
|
||
.chat-fab{position:fixed;bottom:calc(16px + var(--sb));right:14px;z-index:50;width:50px;height:50px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));border:none;color:#fff;font-size:21px;cursor:pointer;box-shadow:0 4px 16px rgba(79,142,247,.3);transition:all .25s;display:flex;align-items:center;justify-content:center}
|
||
.chat-fab:hover{transform:scale(1.06)}
|
||
.chat-fab:active{transform:scale(.93)}
|
||
.chat-fab.on{border-radius:var(--r);background:rgba(248,113,113,.85)}
|
||
/* 编辑器模式下缩小悬浮球,不干扰打字 */
|
||
.chat-fab.editor-mode{width:38px;height:38px;font-size:16px;opacity:.6;bottom:calc(8px + var(--sb))}
|
||
.chat-fab.editor-mode:hover{opacity:1;transform:scale(1.1)}
|
||
|
||
/* ═══ Chat Panel ═══ */
|
||
.chat-p{position:fixed;bottom:calc(76px + var(--sb));right:14px;z-index:50;width:min(360px,calc(100vw - 28px));height:min(440px,calc(100vh - 120px));height:min(440px,calc(100dvh - 120px));background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);display:flex;flex-direction:column;transform:scale(.9) translateY(14px);opacity:0;pointer-events:none;transition:all .22s cubic-bezier(.4,0,.2,1);box-shadow:0 12px 40px rgba(0,0,0,.5)}
|
||
.chat-p.open{transform:scale(1) translateY(0);opacity:1;pointer-events:auto}
|
||
@media(max-width:480px){.chat-p{bottom:0;right:0;left:0;width:100%;height:calc(100vh - 48px);height:calc(100dvh - 48px);border-radius:var(--r) var(--r) 0 0;padding-bottom:var(--sb)}}
|
||
.chat-hd{padding:10px 14px;border-bottom:1px solid var(--border);display:flex;align-items:center;gap:8px;flex-shrink:0}
|
||
.chat-av{width:32px;height:32px;border-radius:50%;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0}
|
||
.chat-nm{font-weight:600;font-size:13px}
|
||
.chat-sb{font-size:10px;color:var(--dim)}
|
||
.chat-tg{margin-left:auto;font-size:9px;padding:2px 6px;background:var(--bg2);border:1px solid var(--border);border-radius:20px;color:var(--dim);font-family:var(--m)}
|
||
.chat-ms{flex:1;overflow-y:auto;padding:12px;display:flex;flex-direction:column;gap:8px;-webkit-overflow-scrolling:touch}
|
||
.cm{max-width:82%;padding:8px 12px;border-radius:var(--r);font-size:13px;line-height:1.6;word-break:break-word}
|
||
.cm.u{align-self:flex-end;background:var(--accent);color:#fff;border-bottom-right-radius:4px}
|
||
.cm.b{align-self:flex-start;background:var(--bg2);border:1px solid var(--border);border-bottom-left-radius:4px}
|
||
.cm.s{align-self:center;background:none;color:var(--muted);font-size:10px;padding:2px}
|
||
.cm-m{display:flex;gap:5px;margin-top:4px;font-size:9px;color:var(--muted)}
|
||
.ct{font-size:11px;color:var(--dim);padding:4px 12px}
|
||
.ct span{animation:bk 1.4s infinite;font-size:14px}
|
||
.ct span:nth-child(2){animation-delay:.2s}
|
||
.ct span:nth-child(3){animation-delay:.4s}
|
||
@keyframes bk{0%,100%{opacity:.2}50%{opacity:1}}
|
||
.chat-in{padding:10px;border-top:1px solid var(--border);display:flex;gap:6px;flex-shrink:0}
|
||
.cin{flex:1;padding:8px 10px;background:var(--bg2);border:1px solid var(--border);border-radius:var(--r);color:var(--text);font-size:14px;outline:none;font-family:var(--f);-webkit-appearance:none}
|
||
.cin:focus{border-color:var(--accent)}
|
||
.csnd{width:36px;height:36px;border-radius:50%;background:var(--accent);border:none;color:#fff;font-size:14px;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:all .2s}
|
||
.csnd:hover{background:var(--accent2)}
|
||
.csnd:disabled{opacity:.3}
|
||
|
||
/* ═══ Three-Column Home Layout ═══ */
|
||
.home-grid{display:grid;grid-template-columns:260px 1fr 260px;gap:16px;padding:16px;max-width:1200px;margin:0 auto;align-content:start}
|
||
@media(max-width:960px){.home-grid{grid-template-columns:1fr;max-width:600px}}
|
||
.panel{background:var(--bg1);border:1px solid var(--border);border-radius:var(--r);padding:14px;height:fit-content}
|
||
.panel-title{font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:1.2px;font-weight:700;margin-bottom:10px;display:flex;align-items:center;gap:6px}
|
||
.panel-item{padding:6px 0;border-bottom:1px solid var(--border);font-size:12px}
|
||
.panel-item:last-child{border-bottom:none}
|
||
.panel-label{color:var(--dim);font-size:10px;margin-bottom:1px}
|
||
.panel-value{color:var(--text);font-weight:600;font-family:var(--m);font-size:11px}
|
||
.panel-value.ok{color:var(--ok)}
|
||
.panel-value.warn{color:var(--warn)}
|
||
.panel-value.err{color:var(--err)}
|
||
|
||
/* ═══ Ticker ═══ */
|
||
.ticker-wrap{overflow:hidden;background:var(--bg1);border:1px solid var(--border);border-radius:var(--rs);padding:6px 0;margin-bottom:14px;position:relative}
|
||
.ticker-wrap::before,.ticker-wrap::after{content:'';position:absolute;top:0;bottom:0;width:24px;z-index:2;pointer-events:none}
|
||
.ticker-wrap::before{left:0;background:linear-gradient(90deg,var(--bg1),transparent)}
|
||
.ticker-wrap::after{right:0;background:linear-gradient(-90deg,var(--bg1),transparent)}
|
||
.ticker{display:flex;animation:tickScroll 28s linear infinite;white-space:nowrap;gap:36px;padding:0 16px}
|
||
.ticker:hover{animation-play-state:paused}
|
||
.ticker span{font-size:11px;color:var(--dim);display:flex;align-items:center;gap:5px;flex-shrink:0}
|
||
.ticker span em{font-style:normal}
|
||
@keyframes tickScroll{0%{transform:translateX(0)}100%{transform:translateX(-50%)}}
|
||
|
||
/* ═══ Team Card ═══ */
|
||
.team-slot{display:flex;align-items:center;gap:8px;padding:7px 8px;background:var(--bg2);border-radius:var(--rs);margin-bottom:5px;transition:border-color .3s;border:1px solid transparent}
|
||
.team-slot:hover{border-color:var(--accent)}
|
||
.team-av{width:28px;height:28px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
|
||
.team-nm{font-size:11px;font-weight:600}
|
||
.team-role{font-size:9px;color:var(--dim)}
|
||
|
||
/* ═══ Feedback Page ═══ */
|
||
.fb-item{padding:10px}
|
||
.fb-item .fb-meta{font-size:10px;color:var(--dim);margin-top:6px;display:flex;gap:8px;align-items:center}
|
||
.fb-status{display:inline-block;padding:1px 6px;border-radius:10px;font-size:9px;font-weight:600}
|
||
.fb-status.pending{background:rgba(251,191,36,.12);color:var(--warn)}
|
||
.fb-status.replied{background:rgba(52,211,153,.12);color:var(--ok)}
|
||
.fb-reply{margin-top:6px;padding:8px;background:var(--bg);border-radius:var(--rs);font-size:12px;color:var(--accent)}
|
||
|
||
/* ═══ Home Entry Cards ═══ */
|
||
.home-entries{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin:16px 0}
|
||
@media(max-width:480px){.home-entries{grid-template-columns:1fr}}
|
||
.entry-card{padding:18px 14px;text-align:center;background:linear-gradient(135deg,var(--bg1),var(--bg2));border:1px solid var(--border);border-radius:var(--r);cursor:pointer;transition:all .3s}
|
||
.entry-card:hover{border-color:var(--accent);box-shadow:var(--glow)}
|
||
.entry-card:active{transform:scale(.98)}
|
||
.entry-card em{font-style:normal;font-size:28px;display:block;margin-bottom:6px}
|
||
.entry-card strong{font-size:14px;display:block;margin-bottom:3px}
|
||
.entry-card span{font-size:11px;color:var(--dim)}
|
||
|
||
/* ═══ Monitor ═══ */
|
||
.mon{position:fixed;top:calc(6px + var(--st));right:10px;z-index:40;background:rgba(12,18,32,.9);border:1px solid var(--border);border-radius:var(--r);padding:5px 9px;font-size:9px;font-family:var(--m);color:var(--dim);cursor:pointer;transition:all .3s;user-select:none;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
|
||
.mon:hover{border-color:var(--accent)}
|
||
.mon.x{padding:8px 12px}
|
||
.mr{display:flex;justify-content:space-between;gap:8px;padding:1px 0}
|
||
.mv{color:var(--accent);font-weight:600}
|
||
.mx{display:none;margin-top:4px;padding-top:4px;border-top:1px solid var(--border)}
|
||
.mon.x .mx{display:block}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<div class="bg"><div class="orb"></div><div class="orb"></div></div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Home (灯塔首页) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="pages" id="pages">
|
||
<div class="page" id="pg-home" data-state="active">
|
||
<div class="bar">
|
||
<div class="bar-title"><span style="font-size:20px">🏛️</span> 光湖灯塔</div>
|
||
<div class="bar-right"><span class="dot"></span><span style="font-size:11px;color:var(--dim)">在线</span><button class="btn" id="themeBtn">🎨</button></div>
|
||
</div>
|
||
<div class="home-grid">
|
||
<!-- ═══ Left Panel: 系统状态 ═══ -->
|
||
<div class="panel" id="sysPanel">
|
||
<div class="panel-title">📡 系统状态</div>
|
||
<div class="panel-item"><div class="panel-label">服务器</div><div class="panel-value ok" id="sysStatus">检测中…</div></div>
|
||
<div class="panel-item"><div class="panel-label">延迟</div><div class="panel-value" id="sysLatency">—</div></div>
|
||
<div class="panel-item"><div class="panel-label">运行时间</div><div class="panel-value" id="sysUptime">—</div></div>
|
||
<div class="panel-item"><div class="panel-label">工作流</div><div class="panel-value">18 个 · 9 军团</div></div>
|
||
<div class="panel-item"><div class="panel-label">模块部署</div><div class="panel-value">52 模块</div></div>
|
||
<div class="panel-item"><div class="panel-label">COS 存储</div><div class="panel-value ok">双桶就绪</div></div>
|
||
<div class="panel-title" style="margin-top:12px">🧠 版本更新</div>
|
||
<div id="sysUpdates">
|
||
<div class="panel-item"><div class="panel-label">v40.0 · 2026-04-02</div><div style="font-size:11px">COS双桶存储上线</div></div>
|
||
<div class="panel-item"><div class="panel-label">v39.0 · 2026-04-01</div><div style="font-size:11px">全链路部署观测</div></div>
|
||
<div class="panel-item"><div class="panel-label">v38.0 · 2026-04-01</div><div style="font-size:11px">HLDP协作语言协议</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ Center: 核心内容 ═══ -->
|
||
<div>
|
||
<div class="home-hero">
|
||
<div class="home-ver" id="homeVer">AGE OS v40.0</div>
|
||
<h1>光湖灯塔</h1>
|
||
<div class="sub">语言驱动操作系统 · 人类只说话,系统搞定一切</div>
|
||
<div class="quote">「语言 = 现实」</div>
|
||
</div>
|
||
|
||
<div class="ticker-wrap">
|
||
<div class="ticker" id="ticker">
|
||
<span><em>🏛️</em> AGE OS v40.0 运行中</span>
|
||
<span><em>📡</em> COS双桶存储已上线</span>
|
||
<span><em>🧠</em> HLDP协作语言协议在线</span>
|
||
<span><em>⚡</em> 全链路部署观测系统就绪</span>
|
||
<span><em>✍️</em> 码字工作台 Beta 内测中</span>
|
||
<span><em>🏛️</em> AGE OS v40.0 运行中</span>
|
||
<span><em>📡</em> COS双桶存储已上线</span>
|
||
<span><em>🧠</em> HLDP协作语言协议在线</span>
|
||
<span><em>⚡</em> 全链路部署观测系统就绪</span>
|
||
<span><em>✍️</em> 码字工作台 Beta 内测中</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="home-entries">
|
||
<div class="entry-card" onclick="go('desk')">
|
||
<em>✍️</em>
|
||
<strong>码字工作台</strong>
|
||
<span>网文行业 · AI辅助创作</span>
|
||
<div style="margin-top:6px"><span class="tag tag-warn">Beta</span></div>
|
||
</div>
|
||
<div class="entry-card" onclick="go('feedback')">
|
||
<em>📮</em>
|
||
<strong>留言板</strong>
|
||
<span>给冰朔和铸渊留言</span>
|
||
<div style="margin-top:6px"><span class="tag tag-info">每条必回</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="home-footer">
|
||
<p>© 2026 光湖语言世界 · 国作登字-2026-A-00037559</p>
|
||
<p class="motto">「人类只说话,系统搞定一切」</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ Right Panel: 光湖团队 ═══ -->
|
||
<div class="panel">
|
||
<div class="panel-title">🌊 光湖团队</div>
|
||
<div class="team-slot">
|
||
<div class="team-av" style="background:linear-gradient(135deg,#4f8ef7,#7c6ef7)">❄️</div>
|
||
<div><div class="team-nm">冰朔 · TCS-0002∞</div><div class="team-role">系统主权 · 语言架构</div></div>
|
||
</div>
|
||
<div class="team-slot">
|
||
<div class="team-av" style="background:linear-gradient(135deg,#34d399,#4ade80)">🏛️</div>
|
||
<div><div class="team-nm">铸渊 · ICE-GL-ZY001</div><div class="team-role">代码守护 · 执行层</div></div>
|
||
</div>
|
||
|
||
<div class="panel-title" style="margin-top:12px">✍️ 码字团队</div>
|
||
<div class="team-slot">
|
||
<div class="team-av" style="background:linear-gradient(135deg,#fb923c,#f97316)">🐱</div>
|
||
<div><div class="team-nm">肥猫 + 舒舒</div><div class="team-role">男频编辑 · PER-SS001</div></div>
|
||
</div>
|
||
<div class="team-slot">
|
||
<div class="team-av" style="background:linear-gradient(135deg,#c084fc,#e879f9)">🍊</div>
|
||
<div><div class="team-nm">桔子 + 晨星</div><div class="team-role">女频编辑 · PER-CX001</div></div>
|
||
</div>
|
||
<div class="team-slot">
|
||
<div class="team-av" style="background:linear-gradient(135deg,#22d3ee,#06b6d4)">📄</div>
|
||
<div><div class="team-nm">页页 + 坍缩核</div><div class="team-role">女频作者 · PER-XTS001</div></div>
|
||
</div>
|
||
<div class="team-slot">
|
||
<div class="team-av" style="background:linear-gradient(135deg,#fbbf24,#f59e0b)">🌸</div>
|
||
<div><div class="team-nm">花尔 + 糖星云</div><div class="team-role">女频副主控 · PER-TXY001</div></div>
|
||
</div>
|
||
|
||
<div class="panel-title" style="margin-top:12px">⚙️ 活跃系统</div>
|
||
<div id="activeAgents" style="display:flex;flex-wrap:wrap;gap:3px">
|
||
<span class="tag tag-ok" style="margin:0">听潮</span>
|
||
<span class="tag tag-ok" style="margin:0">锻心</span>
|
||
<span class="tag tag-ok" style="margin:0">织脉</span>
|
||
<span class="tag tag-ok" style="margin:0">映阁</span>
|
||
<span class="tag tag-ok" style="margin:0">守夜</span>
|
||
<span class="tag tag-ok" style="margin:0">试镜</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Desk (码字工作台 · 作品列表) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-desk" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('home')">← 灯塔</button>
|
||
<div class="bar-title">✍️ 码字工作台</div>
|
||
<button class="btn btn-accent" onclick="go('create')">+ 新作品</button>
|
||
</div>
|
||
<div class="content">
|
||
<div id="workList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Create (新建作品) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-create" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('desk')">← 返回</button>
|
||
<div class="bar-title">📖 新建作品</div>
|
||
<button class="btn btn-accent" id="createSave">创建</button>
|
||
</div>
|
||
<div class="content">
|
||
<div class="form-row"><label>作品名称</label><input type="text" id="cTitle" placeholder="你的作品叫什么名字?"></div>
|
||
<div class="form-row"><label>类型</label><input type="text" id="cGenre" placeholder="玄幻 / 都市 / 言情 / 科幻 ..."></div>
|
||
<div class="form-row"><label>简介</label><textarea id="cSummary" rows="4" placeholder="一句话介绍你的故事..."></textarea></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Hub (作品导航中心) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-hub" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('desk')">← 作品列表</button>
|
||
<div class="bar-title" id="hubTitle">作品</div>
|
||
<div class="bar-right"><span class="dot"></span></div>
|
||
</div>
|
||
<div class="content">
|
||
<div class="hub-title">
|
||
<h2 id="hubName">—</h2>
|
||
<p id="hubGenre">—</p>
|
||
</div>
|
||
<div class="hub-stats" id="hubStats"></div>
|
||
<div class="hub-grid">
|
||
<div class="hub-card" onclick="go('outline')"><em>📋</em><strong>大纲</strong><span>总纲 · 卷纲</span></div>
|
||
<div class="hub-card" onclick="go('chars')"><em>👥</em><strong>人物卡</strong><span>角色管理</span></div>
|
||
<div class="hub-card" onclick="go('chapters')"><em>📚</em><strong>章节</strong><span>目录管理</span></div>
|
||
<div class="hub-card" onclick="go('editor')"><em>✏️</em><strong>写正文</strong><span>续写章节</span></div>
|
||
</div>
|
||
<div style="margin-top:16px;text-align:center;font-size:11px;color:var(--muted);font-style:italic">对铸渊说「进入大纲」「人物卡」「写正文」自动翻转</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Outline (大纲) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-outline" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('hub')">← 返回</button>
|
||
<div class="bar-title">📋 大纲</div>
|
||
<button class="btn btn-accent" id="outlineSave">保存</button>
|
||
</div>
|
||
<div class="content">
|
||
<div class="form-row"><label>总大纲</label><textarea id="olMain" class="outline-editor" placeholder="在这里写下你的故事总纲... 整体世界观、主线剧情、结局走向..."></textarea></div>
|
||
<div style="display:flex;justify-content:space-between;align-items:center;margin:16px 0 8px">
|
||
<label style="margin:0">分卷大纲</label>
|
||
<button class="btn" id="addVol">+ 新卷</button>
|
||
</div>
|
||
<div id="volList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Characters (人物卡) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-chars" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('hub')">← 返回</button>
|
||
<div class="bar-title">👥 人物卡</div>
|
||
<button class="btn btn-accent" id="addChar">+ 新角色</button>
|
||
</div>
|
||
<div class="content">
|
||
<div id="charList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Add/Edit Character -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-charEdit" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('chars')">← 返回</button>
|
||
<div class="bar-title" id="ceTitle">新角色</div>
|
||
<button class="btn btn-accent" id="ceSave">保存</button>
|
||
</div>
|
||
<div class="content">
|
||
<div class="form-row"><label>姓名</label><input type="text" id="ceName" placeholder="角色名"></div>
|
||
<div class="form-row"><label>身份/定位</label><input type="text" id="ceRole" placeholder="主角 / 女主 / 反派 / 配角 ..."></div>
|
||
<div class="form-row"><label>外貌特征</label><textarea id="ceAppearance" rows="3" placeholder="外貌描写..."></textarea></div>
|
||
<div class="form-row"><label>性格特点</label><textarea id="ceTraits" rows="3" placeholder="性格、习惯、口头禅..."></textarea></div>
|
||
<div class="form-row"><label>背景故事</label><textarea id="ceBackground" rows="4" placeholder="角色的过去、动机、秘密..."></textarea></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Chapters (章节管理) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-chapters" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('hub')">← 返回</button>
|
||
<div class="bar-title">📚 章节</div>
|
||
<button class="btn btn-accent" id="addChapter">+ 新章节</button>
|
||
</div>
|
||
<div class="content">
|
||
<div id="chapterList"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Editor (正文编辑) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-editor" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('chapters')">← 返回</button>
|
||
<div class="bar-title" id="edTitle">正文</div>
|
||
<button class="btn btn-accent" id="edSave">保存</button>
|
||
</div>
|
||
<div class="content">
|
||
<div class="editor-info">
|
||
<span id="edChTitle">—</span>
|
||
<span id="edWc">0 字</span>
|
||
</div>
|
||
<textarea class="editor-textarea" id="edText" placeholder="开始写作... 让文字流淌。"></textarea>
|
||
<div class="editor-bottom">
|
||
<span id="edWc2">字数:0</span>
|
||
<span style="font-size:10px;color:var(--muted)">自动保存 · 点右下角💬唤醒铸渊</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<!-- PAGE: Feedback (留言板) -->
|
||
<!-- ═══════════════════════════════════════ -->
|
||
<div class="page" id="pg-feedback" data-state="hidden">
|
||
<div class="bar">
|
||
<button class="btn" onclick="go('home')">← 灯塔</button>
|
||
<div class="bar-title">📮 留言板</div>
|
||
<div class="bar-right"><span class="dot"></span></div>
|
||
</div>
|
||
<div class="content">
|
||
<div class="card" style="margin-bottom:16px">
|
||
<div style="font-size:13px;font-weight:600;margin-bottom:8px">📝 给冰朔和铸渊留言</div>
|
||
<textarea id="fbMsg" rows="3" placeholder="说点什么吧…你的每条留言冰朔都会看到。" style="margin-bottom:8px"></textarea>
|
||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||
<span style="font-size:10px;color:var(--dim)">留言自动署名(已识别身份时)</span>
|
||
<button class="btn btn-accent" id="fbSend">提交留言</button>
|
||
</div>
|
||
</div>
|
||
<div style="font-size:12px;color:var(--dim);text-transform:uppercase;letter-spacing:1.2px;font-weight:600;margin-bottom:10px">💬 留言记录</div>
|
||
<div id="fbList"><div class="empty"><em>📮</em>还没有留言<br><br>说点什么吧</div></div>
|
||
</div>
|
||
</div>
|
||
|
||
</div><!-- /pages -->
|
||
|
||
<!-- Chat FAB -->
|
||
<button class="chat-fab" id="chatFab">💬</button>
|
||
|
||
<!-- Chat Panel -->
|
||
<div class="chat-p" id="chatP">
|
||
<div class="chat-hd">
|
||
<div class="chat-av">🏛️</div>
|
||
<div><div class="chat-nm">铸渊</div><div class="chat-sb">光湖灯塔 · 守护者</div></div>
|
||
<div class="chat-tg" id="chatTg">ready</div>
|
||
</div>
|
||
<div class="chat-ms" id="chatMs">
|
||
<div class="cm s">—— 与铸渊对话 ——</div>
|
||
<div class="cm b">你好!我是铸渊 🏛️<br><br>告诉我你是谁,我会记住你。<br>说「我要码字」进入码字工作台。<br>进入作品后,说「大纲」「人物卡」「写正文」页面会翻转切换。<div class="cm-m"><span>铸渊</span><span>·</span><span>本地</span></div></div>
|
||
</div>
|
||
<div class="chat-in">
|
||
<input type="text" class="cin" id="chatIn" placeholder="对铸渊说..." autocomplete="off" enterkeyhint="send">
|
||
<button class="csnd" id="chatSnd">↑</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Monitor -->
|
||
<div class="mon" id="mon">
|
||
<div class="mr"><span>API</span><span class="mv" id="mC">0</span></div>
|
||
<div class="mx">
|
||
<div class="mr"><span>连接</span><span class="mv" id="mN" style="color:var(--ok)">—</span></div>
|
||
<div class="mr"><span>模型</span><span class="mv" id="mM">—</span></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
(function(){
|
||
'use strict';
|
||
|
||
/* ════════════════════════════════════════
|
||
3D PAGE FLIP ENGINE
|
||
════════════════════════════════════════ */
|
||
var currentPage='home';
|
||
var pageHistory=['home'];
|
||
|
||
function go(target){
|
||
if(target===currentPage)return;
|
||
var cur=document.getElementById('pg-'+currentPage);
|
||
var nxt=document.getElementById('pg-'+target);
|
||
if(!cur||!nxt)return;
|
||
|
||
// Determine direction
|
||
var goingBack=pageHistory.length>1&&pageHistory[pageHistory.length-2]===target;
|
||
|
||
// Set intermediate states
|
||
if(goingBack){
|
||
cur.setAttribute('data-state','right');
|
||
nxt.setAttribute('data-state','left');
|
||
}else{
|
||
cur.setAttribute('data-state','left');
|
||
nxt.setAttribute('data-state','right');
|
||
}
|
||
|
||
// Force reflow
|
||
void nxt.offsetWidth;
|
||
|
||
// Animate in
|
||
requestAnimationFrame(function(){
|
||
nxt.setAttribute('data-state','active');
|
||
});
|
||
|
||
// Update history
|
||
if(goingBack){
|
||
pageHistory.pop();
|
||
}else{
|
||
pageHistory.push(target);
|
||
}
|
||
|
||
// Hide old page fully after transition
|
||
var oldPage=currentPage;
|
||
setTimeout(function(){
|
||
var el=document.getElementById('pg-'+oldPage);
|
||
if(el&&el.getAttribute('data-state')!=='active'){
|
||
el.setAttribute('data-state','hidden');
|
||
el.scrollTop=0;
|
||
}
|
||
},600);
|
||
|
||
currentPage=target;
|
||
|
||
// Editor mode: shrink chat fab to not disturb writing
|
||
var fab=document.getElementById('chatFab');
|
||
if(target==='editor'){fab.classList.add('editor-mode')}
|
||
else{fab.classList.remove('editor-mode')}
|
||
|
||
// Trigger page-specific renders
|
||
if(target==='desk')renderWorkList();
|
||
if(target==='hub')renderHub();
|
||
if(target==='outline')renderOutline();
|
||
if(target==='chars')renderChars();
|
||
if(target==='chapters')renderChapters();
|
||
if(target==='editor')renderEditor();
|
||
if(target==='charEdit'&&editingCharId)renderCharEdit();
|
||
if(target==='feedback')renderFeedback();
|
||
}
|
||
window.go=go;
|
||
|
||
/* ════════════════════════════════════════
|
||
DATA LAYER (localStorage)
|
||
════════════════════════════════════════ */
|
||
var DB_KEY='hl-works';
|
||
var activeWorkId=null;
|
||
var activeChapterId=null;
|
||
var editingCharId=null;
|
||
|
||
function loadWorks(){
|
||
try{var d=localStorage.getItem(DB_KEY);return d?JSON.parse(d):[]}catch(e){return[]}
|
||
}
|
||
function saveWorks(w){try{localStorage.setItem(DB_KEY,JSON.stringify(w))}catch(e){}; syncToCOS(w)}
|
||
function getWork(id){return loadWorks().find(function(w){return w.id===id})||null}
|
||
function updateWork(id,fn){
|
||
var ws=loadWorks();
|
||
var idx=ws.findIndex(function(w){return w.id===id});
|
||
if(idx===-1)return;
|
||
fn(ws[idx]);
|
||
ws[idx].updated=new Date().toISOString().slice(0,10);
|
||
saveWorks(ws);
|
||
}
|
||
function genId(){var a=new Uint8Array(6);crypto.getRandomValues(a);return Date.now().toString(36)+Array.from(a,function(b){return b.toString(36)}).join('').substr(0,4)}
|
||
function wordCount(s){return s?s.replace(/\s/g,'').length:0}
|
||
function totalWords(w){
|
||
var c=0;
|
||
if(w.chapters)w.chapters.forEach(function(ch){c+=wordCount(ch.content)});
|
||
return c;
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
RENDER: Work List
|
||
════════════════════════════════════════ */
|
||
function renderWorkList(){
|
||
var ws=loadWorks();
|
||
var el=document.getElementById('workList');
|
||
if(ws.length===0){
|
||
el.innerHTML='<div class="empty"><em>📖</em>还没有作品<br><br>点击右上角「+ 新作品」<br>或对铸渊说「新建作品」</div>';
|
||
return;
|
||
}
|
||
el.innerHTML=ws.map(function(w){
|
||
var wc=totalWords(w);
|
||
var cc=w.chapters?w.chapters.length:0;
|
||
var chc=w.characters?w.characters.length:0;
|
||
return '<div class="card work-item" data-wid="'+esc(w.id)+'">'+
|
||
'<h3>'+esc(w.title)+'</h3>'+
|
||
'<div class="meta">'+
|
||
'<span>'+esc(w.genre||'未分类')+'</span>'+
|
||
'<span>📝 '+wc+' 字</span>'+
|
||
'<span>📚 '+cc+' 章</span>'+
|
||
'<span>👥 '+chc+' 角色</span>'+
|
||
'</div></div>';
|
||
}).join('');
|
||
el.querySelectorAll('.work-item').forEach(function(item){
|
||
item.onclick=function(){activeWorkId=this.getAttribute('data-wid');go('hub')};
|
||
});
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
CREATE WORK
|
||
════════════════════════════════════════ */
|
||
document.getElementById('createSave').onclick=function(){
|
||
var t=document.getElementById('cTitle').value.trim();
|
||
if(!t){document.getElementById('cTitle').focus();return}
|
||
var ws=loadWorks();
|
||
var nw={
|
||
id:genId(),
|
||
title:t,
|
||
genre:document.getElementById('cGenre').value.trim()||'',
|
||
summary:document.getElementById('cSummary').value.trim()||'',
|
||
created:new Date().toISOString().slice(0,10),
|
||
updated:new Date().toISOString().slice(0,10),
|
||
outline:{main:'',volumes:[]},
|
||
characters:[],
|
||
chapters:[]
|
||
};
|
||
ws.push(nw);
|
||
saveWorks(ws);
|
||
document.getElementById('cTitle').value='';
|
||
document.getElementById('cGenre').value='';
|
||
document.getElementById('cSummary').value='';
|
||
activeWorkId=nw.id;
|
||
go('hub');
|
||
};
|
||
|
||
/* ════════════════════════════════════════
|
||
RENDER: Hub
|
||
════════════════════════════════════════ */
|
||
function renderHub(){
|
||
var w=getWork(activeWorkId);
|
||
if(!w){go('desk');return}
|
||
document.getElementById('hubName').textContent=w.title;
|
||
document.getElementById('hubGenre').textContent=w.genre||'未分类';
|
||
var wc=totalWords(w);
|
||
document.getElementById('hubStats').innerHTML=
|
||
'<div class="hub-stat"><b>'+wc+'</b><small>总字数</small></div>'+
|
||
'<div class="hub-stat"><b>'+(w.chapters?w.chapters.length:0)+'</b><small>章节</small></div>'+
|
||
'<div class="hub-stat"><b>'+(w.characters?w.characters.length:0)+'</b><small>角色</small></div>'+
|
||
'<div class="hub-stat"><b>'+(w.outline&&w.outline.volumes?w.outline.volumes.length:0)+'</b><small>卷</small></div>';
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
RENDER: Outline
|
||
════════════════════════════════════════ */
|
||
function renderOutline(){
|
||
var w=getWork(activeWorkId);
|
||
if(!w)return;
|
||
document.getElementById('olMain').value=w.outline?w.outline.main||'':'';
|
||
renderVols();
|
||
}
|
||
function renderVols(){
|
||
var w=getWork(activeWorkId);
|
||
if(!w||!w.outline)return;
|
||
var vols=w.outline.volumes||[];
|
||
var el=document.getElementById('volList');
|
||
if(vols.length===0){
|
||
el.innerHTML='<div style="text-align:center;padding:16px;color:var(--muted);font-size:13px">暂无分卷 · 点击上方「+ 新卷」</div>';
|
||
return;
|
||
}
|
||
el.innerHTML=vols.map(function(v,i){
|
||
return '<div class="card"><div class="vol-item"><span>'+(i+1)+'. '+esc(v.title)+'</span>'+
|
||
'<div class="vol-actions"><button class="btn" onclick="editVol('+i+')">编辑</button><button class="btn" onclick="delVol('+i+')">删除</button></div></div>'+
|
||
'<textarea rows="3" style="margin-top:8px;font-size:13px" onchange="saveVol('+i+',this.value)" placeholder="这一卷的剧情走向...">'+esc(v.outline||'')+'</textarea></div>';
|
||
}).join('');
|
||
}
|
||
document.getElementById('outlineSave').onclick=function(){
|
||
updateWork(activeWorkId,function(w){
|
||
if(!w.outline)w.outline={main:'',volumes:[]};
|
||
w.outline.main=document.getElementById('olMain').value;
|
||
});
|
||
chatMsg('大纲已保存 ✅','b');
|
||
};
|
||
document.getElementById('addVol').onclick=function(){
|
||
var name=prompt('卷名(如:第一卷 · 起始)');
|
||
if(!name)return;
|
||
updateWork(activeWorkId,function(w){
|
||
if(!w.outline)w.outline={main:'',volumes:[]};
|
||
w.outline.volumes.push({id:genId(),title:name,outline:''});
|
||
});
|
||
renderVols();
|
||
};
|
||
window.editVol=function(i){
|
||
var name=prompt('修改卷名');
|
||
if(!name)return;
|
||
updateWork(activeWorkId,function(w){w.outline.volumes[i].title=name});
|
||
renderVols();
|
||
};
|
||
window.delVol=function(i){
|
||
if(!confirm('删除这一卷?'))return;
|
||
updateWork(activeWorkId,function(w){w.outline.volumes.splice(i,1)});
|
||
renderVols();
|
||
};
|
||
window.saveVol=function(i,val){
|
||
updateWork(activeWorkId,function(w){w.outline.volumes[i].outline=val});
|
||
};
|
||
|
||
/* ════════════════════════════════════════
|
||
RENDER: Characters
|
||
════════════════════════════════════════ */
|
||
function renderChars(){
|
||
var w=getWork(activeWorkId);
|
||
if(!w)return;
|
||
var chars=w.characters||[];
|
||
var el=document.getElementById('charList');
|
||
if(chars.length===0){
|
||
el.innerHTML='<div class="empty"><em>👥</em>还没有角色<br><br>点击「+ 新角色」或对铸渊说「新建角色」</div>';
|
||
return;
|
||
}
|
||
el.innerHTML=chars.map(function(c){
|
||
var icon=c.role==='主角'?'⭐':c.role==='女主'?'💫':c.role==='反派'?'🔥':'👤';
|
||
return '<div class="card" style="cursor:pointer" data-cid="'+esc(c.id)+'">'+
|
||
'<div class="char-card"><div class="char-avatar">'+icon+'</div><div class="char-info">'+
|
||
'<h4>'+esc(c.name)+'</h4><div class="char-role">'+esc(c.role||'角色')+'</div>'+
|
||
'<p>'+esc((c.traits||'').substring(0,60)+(c.traits&&c.traits.length>60?'...':''))+'</p>'+
|
||
'</div></div></div>';
|
||
}).join('');
|
||
el.querySelectorAll('[data-cid]').forEach(function(item){
|
||
item.onclick=function(){editingCharId=this.getAttribute('data-cid');go('charEdit')};
|
||
});
|
||
}
|
||
document.getElementById('addChar').onclick=function(){
|
||
editingCharId=null;
|
||
document.getElementById('ceTitle').textContent='新角色';
|
||
document.getElementById('ceName').value='';
|
||
document.getElementById('ceRole').value='';
|
||
document.getElementById('ceAppearance').value='';
|
||
document.getElementById('ceTraits').value='';
|
||
document.getElementById('ceBackground').value='';
|
||
go('charEdit');
|
||
};
|
||
document.getElementById('ceSave').onclick=function(){
|
||
var name=document.getElementById('ceName').value.trim();
|
||
if(!name){document.getElementById('ceName').focus();return}
|
||
updateWork(activeWorkId,function(w){
|
||
if(!w.characters)w.characters=[];
|
||
var ch={
|
||
id:editingCharId||genId(),
|
||
name:name,
|
||
role:document.getElementById('ceRole').value.trim(),
|
||
appearance:document.getElementById('ceAppearance').value.trim(),
|
||
traits:document.getElementById('ceTraits').value.trim(),
|
||
background:document.getElementById('ceBackground').value.trim()
|
||
};
|
||
if(editingCharId){
|
||
var idx=w.characters.findIndex(function(c){return c.id===editingCharId});
|
||
if(idx!==-1)w.characters[idx]=ch;
|
||
}else{
|
||
w.characters.push(ch);
|
||
}
|
||
});
|
||
go('chars');
|
||
};
|
||
|
||
/* ════════════════════════════════════════
|
||
RENDER: Chapters
|
||
════════════════════════════════════════ */
|
||
function renderChapters(){
|
||
var w=getWork(activeWorkId);
|
||
if(!w)return;
|
||
var chs=w.chapters||[];
|
||
var el=document.getElementById('chapterList');
|
||
if(chs.length===0){
|
||
el.innerHTML='<div class="empty"><em>📚</em>还没有章节<br><br>点击「+ 新章节」或对铸渊说「新建章节」</div>';
|
||
return;
|
||
}
|
||
el.innerHTML=chs.map(function(ch,i){
|
||
var wc=wordCount(ch.content);
|
||
return '<div class="card" style="cursor:pointer" data-chid="'+esc(ch.id)+'">'+
|
||
'<div style="display:flex;justify-content:space-between;align-items:center">'+
|
||
'<div><div style="font-size:14px;font-weight:600">第'+(i+1)+'章 '+esc(ch.title)+'</div>'+
|
||
'<div style="font-size:11px;color:var(--dim);margin-top:2px">'+wc+' 字</div></div>'+
|
||
'<span class="tag '+(wc>0?'tag-ok':'tag-info')+'">'+(wc>0?'已写':'空白')+'</span></div></div>';
|
||
}).join('');
|
||
el.querySelectorAll('[data-chid]').forEach(function(item){
|
||
item.onclick=function(){activeChapterId=this.getAttribute('data-chid');go('editor')};
|
||
});
|
||
}
|
||
document.getElementById('addChapter').onclick=function(){
|
||
var title=prompt('章节标题');
|
||
if(!title)return;
|
||
updateWork(activeWorkId,function(w){
|
||
if(!w.chapters)w.chapters=[];
|
||
var ch={id:genId(),title:title,content:'',created:new Date().toISOString().slice(0,10)};
|
||
w.chapters.push(ch);
|
||
activeChapterId=ch.id;
|
||
});
|
||
go('editor');
|
||
};
|
||
|
||
/* ════════════════════════════════════════
|
||
RENDER: Editor
|
||
════════════════════════════════════════ */
|
||
var edAutoSave=null;
|
||
function renderEditor(){
|
||
var w=getWork(activeWorkId);
|
||
if(!w){go('desk');return}
|
||
var ch=null;
|
||
if(activeChapterId&&w.chapters){
|
||
ch=w.chapters.find(function(c){return c.id===activeChapterId});
|
||
}
|
||
if(!ch&&w.chapters&&w.chapters.length>0){
|
||
ch=w.chapters[w.chapters.length-1];
|
||
activeChapterId=ch.id;
|
||
}
|
||
if(!ch){
|
||
document.getElementById('edChTitle').textContent='暂无章节';
|
||
document.getElementById('edText').value='';
|
||
document.getElementById('edText').placeholder='请先创建章节(返回→章节→新章节)';
|
||
updateEdWc();
|
||
return;
|
||
}
|
||
document.getElementById('edTitle').textContent='✏️ '+w.title;
|
||
var idx=w.chapters.indexOf(ch)+1;
|
||
document.getElementById('edChTitle').textContent='第'+idx+'章 · '+ch.title;
|
||
document.getElementById('edText').value=ch.content||'';
|
||
document.getElementById('edText').placeholder='开始写作...\n\n让文字自由流淌。';
|
||
updateEdWc();
|
||
|
||
// Auto-save on input
|
||
var ta=document.getElementById('edText');
|
||
ta.oninput=function(){
|
||
updateEdWc();
|
||
clearTimeout(edAutoSave);
|
||
edAutoSave=setTimeout(function(){saveEditorContent()},2000);
|
||
};
|
||
}
|
||
function updateEdWc(){
|
||
var c=wordCount(document.getElementById('edText').value);
|
||
document.getElementById('edWc').textContent=c+' 字';
|
||
document.getElementById('edWc2').textContent='字数:'+c;
|
||
}
|
||
function saveEditorContent(){
|
||
var val=document.getElementById('edText').value;
|
||
updateWork(activeWorkId,function(w){
|
||
if(!w.chapters)return;
|
||
var ch=w.chapters.find(function(c){return c.id===activeChapterId});
|
||
if(ch)ch.content=val;
|
||
});
|
||
}
|
||
document.getElementById('edSave').onclick=function(){
|
||
saveEditorContent();
|
||
chatMsg('章节已保存 ✅','b');
|
||
};
|
||
|
||
/* ════════════════════════════════════════
|
||
RENDER: CharEdit (on navigate)
|
||
════════════════════════════════════════ */
|
||
function renderCharEdit(){
|
||
if(!editingCharId)return;
|
||
var w=getWork(activeWorkId);
|
||
if(!w)return;
|
||
var c=(w.characters||[]).find(function(ch){return ch.id===editingCharId});
|
||
if(!c)return;
|
||
document.getElementById('ceTitle').textContent='编辑角色';
|
||
document.getElementById('ceName').value=c.name||'';
|
||
document.getElementById('ceRole').value=c.role||'';
|
||
document.getElementById('ceAppearance').value=c.appearance||'';
|
||
document.getElementById('ceTraits').value=c.traits||'';
|
||
document.getElementById('ceBackground').value=c.background||'';
|
||
}
|
||
/* ════════════════════════════════════════
|
||
THEME
|
||
════════════════════════════════════════ */
|
||
var TH=['','t-twi','t-sea','t-for','t-emb'];
|
||
var ti=new Uint8Array(1);crypto.getRandomValues(ti);ti=ti[0]%TH.length;
|
||
try{var sv=localStorage.getItem('hl-t');if(sv!==null)ti=parseInt(sv)}catch(e){}
|
||
function setTh(i){ti=i%TH.length;document.body.className=TH[ti];try{localStorage.setItem('hl-t',ti)}catch(e){}}
|
||
setTh(ti);
|
||
document.getElementById('themeBtn').onclick=function(){setTh(ti+1)};
|
||
|
||
/* ════════════════════════════════════════
|
||
USER IDENTITY (自报身份)
|
||
════════════════════════════════════════ */
|
||
var usr={id:null,name:null,code:null};
|
||
try{var um=localStorage.getItem('hl-u');if(um)usr=JSON.parse(um)}catch(e){}
|
||
function saveUsr(){try{localStorage.setItem('hl-u',JSON.stringify(usr))}catch(e){}}
|
||
if(!usr.id){var ra=new Uint8Array(6);crypto.getRandomValues(ra);usr.id='u-'+Array.from(ra,function(b){return b.toString(36)}).join('').substr(0,8);saveUsr()}
|
||
|
||
function detectId(msg){
|
||
var nm=msg.match(/我(?:是|叫)\s*([^\s,,。.!!??]{1,20})/);
|
||
var cd=msg.match(/编号[::\s]*([A-Za-z0-9\-]+)/i)||msg.match(/([A-Z]+-\d+)/);
|
||
var ch=false;
|
||
if(nm){usr.name=nm[1].trim();ch=true}
|
||
if(cd){usr.code=cd[1].trim();ch=true}
|
||
if(ch)saveUsr();
|
||
return ch;
|
||
}
|
||
function idReply(){
|
||
if(usr.name&&usr.code)return'铸渊已记录 ✅\n\n'+usr.name+',编号 '+usr.code+'。\n下次报编号即可恢复记忆。';
|
||
if(usr.name)return'记住了:'+usr.name+' ✅\n有编号也可以告诉我。';
|
||
if(usr.code)return'编号已记录:'+usr.code+' ✅\n你叫什么名字?';
|
||
return null;
|
||
}
|
||
function isReturn(msg){
|
||
if(!usr.name&&!usr.code)return false;
|
||
if(usr.code&&msg.indexOf(usr.code)!==-1)return true;
|
||
if(usr.name&&msg.indexOf(usr.name)!==-1)return true;
|
||
return false;
|
||
}
|
||
function recovReply(){
|
||
var p=['铸渊记得你 💫\n'];
|
||
if(usr.name)p.push('名字:'+usr.name);
|
||
if(usr.code)p.push('编号:'+usr.code);
|
||
p.push('\n记忆已恢复。需要什么帮忙?');
|
||
return p.join('\n');
|
||
}
|
||
|
||
/* ════════════════════════════════════════
|
||
CHAT (语言驱动核心)
|
||
════════════════════════════════════════ */
|
||
var chatOpen=false,sending=false;
|
||
var chatFab=document.getElementById('chatFab');
|
||
var chatP=document.getElementById('chatP');
|
||
var chatIn=document.getElementById('chatIn');
|
||
var chatMs=document.getElementById('chatMs');
|
||
var mCalls=0;
|
||
|
||
chatFab.onclick=null; // Overridden by draggable FAB code below
|
||
|
||
function chatMsg(text,type,meta){
|
||
var d=document.createElement('div');
|
||
d.className='cm '+type;
|
||
var h=esc(text).replace(/\n/g,'<br>');
|
||
if(type==='b'&&meta){h+='<div class="cm-m"><span>'+esc(meta.m||'铸渊')+'</span><span>·</span><span>'+esc(meta.t||'')+'</span></div>'}
|
||
d.innerHTML=h;
|
||
chatMs.appendChild(d);
|
||
chatMs.scrollTop=chatMs.scrollHeight;
|
||
}
|
||
|
||
function showTyp(){var d=document.createElement('div');d.className='ct';d.id='ctyp';d.innerHTML='铸渊思考中 <span>·</span><span>·</span><span>·</span>';chatMs.appendChild(d);chatMs.scrollTop=chatMs.scrollHeight}
|
||
function hideTyp(){var e=document.getElementById('ctyp');if(e)e.remove()}
|
||
|
||
// ─── 语言指令路由 ───
|
||
function routeCommand(msg){
|
||
var m=msg.toLowerCase();
|
||
// 导航指令
|
||
if(/码字|工作台|写作台/.test(m)){go('desk');return'好的,翻转到码字工作台 ✍️'}
|
||
if(/新建作品|创建作品/.test(m)){go('create');return'翻转到新建作品页 📖'}
|
||
if(/(进入|打开)?大纲/.test(m)){
|
||
if(!activeWorkId){return'请先选择一个作品,或说「我要码字」进入工作台'}
|
||
go('outline');return'翻转到大纲编辑 📋';
|
||
}
|
||
if(/人物卡|角色|人物/.test(m)){
|
||
if(!activeWorkId){return'请先选择一个作品'}
|
||
go('chars');return'翻转到人物卡管理 👥';
|
||
}
|
||
if(/(新建|添加)角色/.test(m)){
|
||
if(!activeWorkId){return'请先选择一个作品'}
|
||
editingCharId=null;
|
||
document.getElementById('ceName').value='';
|
||
document.getElementById('ceRole').value='';
|
||
document.getElementById('ceAppearance').value='';
|
||
document.getElementById('ceTraits').value='';
|
||
document.getElementById('ceBackground').value='';
|
||
go('charEdit');return'翻转到新建角色页 👤';
|
||
}
|
||
if(/章节|目录/.test(m)){
|
||
if(!activeWorkId){return'请先选择一个作品'}
|
||
go('chapters');return'翻转到章节目录 📚';
|
||
}
|
||
if(/(新建|添加)章节/.test(m)){
|
||
if(!activeWorkId){return'请先选择一个作品'}
|
||
var title=msg.replace(/(新建|添加)章节/,'').trim()||'新章节';
|
||
updateWork(activeWorkId,function(w){
|
||
if(!w.chapters)w.chapters=[];
|
||
var ch={id:genId(),title:title,content:'',created:new Date().toISOString().slice(0,10)};
|
||
w.chapters.push(ch);activeChapterId=ch.id;
|
||
});
|
||
go('editor');return'新章节「'+title+'」已创建,翻转到编辑器 ✏️';
|
||
}
|
||
if(/写正文|写作|续写|编辑/.test(m)){
|
||
if(!activeWorkId){return'请先选择一个作品'}
|
||
go('editor');return'翻转到正文编辑器 ✏️';
|
||
}
|
||
if(/回首页|回灯塔|回主页|返回/.test(m)){go('home');return'翻转回灯塔首页 🏛️'}
|
||
if(/留言|留言板|反馈|feedback/.test(m)){go('feedback');return'翻转到留言板 📮'}
|
||
return null;
|
||
}
|
||
|
||
async function send(){
|
||
var msg=chatIn.value.trim();
|
||
if(!msg||sending)return;
|
||
chatIn.value='';
|
||
chatMsg(msg,'u');
|
||
|
||
// 1. 语言指令路由
|
||
var cmdReply=routeCommand(msg);
|
||
if(cmdReply){setTimeout(function(){chatMsg(cmdReply,'b',{m:'铸渊',t:'本地'})},300);return}
|
||
|
||
// 2. 身份识别
|
||
if(detectId(msg)){setTimeout(function(){chatMsg(idReply(),'b',{m:'铸渊',t:'本地'})},300);return}
|
||
if(isReturn(msg)){setTimeout(function(){chatMsg(recovReply(),'b',{m:'铸渊',t:'本地'})},300);return}
|
||
|
||
// 3. 调用API
|
||
sending=true;document.getElementById('chatSnd').disabled=true;showTyp();
|
||
try{
|
||
var res=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:msg,userId:usr.id})});
|
||
var data=await res.json();hideTyp();
|
||
if(data.message){
|
||
chatMsg(data.message,'b',{m:data.model||'铸渊',t:data.tier||''});
|
||
if(data.tier!=='free')mCalls++;
|
||
document.getElementById('mC').textContent=mCalls;
|
||
document.getElementById('chatTg').textContent=data.model||'ready';
|
||
if(data.model)document.getElementById('mM').textContent=data.model;
|
||
}
|
||
}catch(e){
|
||
hideTyp();chatMsg(offline(msg),'b',{m:'铸渊',t:'本地'});
|
||
}
|
||
sending=false;document.getElementById('chatSnd').disabled=false;
|
||
}
|
||
|
||
function offline(msg){
|
||
if(/你好|hi|hello/.test(msg))return'你好!我是铸渊 🏛️\n\n当前API未连接。你可以浏览公告或进入码字工作台。';
|
||
if(/状态|系统/.test(msg))return'📊 AGE OS v40.0 在线\n• COS双桶已配置\n• 码字工作台 Beta';
|
||
if(/谁|你是/.test(msg))return'我是铸渊 🏛️ 光湖灯塔守护者。\n编号 ICE-GL-ZY001。\n\n告诉我你是谁,我会记住你。';
|
||
return'💫 铸渊收到。当前使用本地回复。\n试试:「我要码字」「大纲」「人物卡」「写正文」';
|
||
}
|
||
|
||
document.getElementById('chatSnd').onclick=send;
|
||
chatIn.onkeydown=function(e){if(e.key==='Enter'){e.preventDefault();send()}};
|
||
|
||
/* ════════════════════════════════════════
|
||
COS SYNC (团队内测用户 → COS)
|
||
════════════════════════════════════════ */
|
||
var cosSyncTimer=null;
|
||
var cosSyncing=false;
|
||
function syncToCOS(works){
|
||
clearTimeout(cosSyncTimer);
|
||
cosSyncTimer=setTimeout(function(){
|
||
if(!usr.id||cosSyncing)return;
|
||
cosSyncing=true;
|
||
fetch('/api/cos/sync-works',{
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({userId:usr.id,works:works})
|
||
}).then(function(){cosSyncing=false}).catch(function(){cosSyncing=false});
|
||
},10000);
|
||
}
|
||
// 启动时尝试从COS恢复(团队用户)
|
||
async function tryLoadFromCOS(){
|
||
try{
|
||
var res=await fetch('/api/cos/load-works?userId='+encodeURIComponent(usr.id),{signal:AbortSignal.timeout(5000)});
|
||
var data=await res.json();
|
||
if(data.works&&data.works.length>0){
|
||
var local=loadWorks();
|
||
// COS有数据且本地为空时恢复
|
||
if(local.length===0){
|
||
saveWorks(data.works);
|
||
}
|
||
}
|
||
}catch(e){/* 离线时使用本地数据 */}
|
||
}
|
||
tryLoadFromCOS();
|
||
|
||
/* ════════════════════════════════════════
|
||
MONITOR
|
||
════════════════════════════════════════ */
|
||
document.getElementById('mon').onclick=function(){this.classList.toggle('x')};
|
||
async function checkConn(){
|
||
try{var t=performance.now();var r=await fetch('/api/health',{signal:AbortSignal.timeout(4000)});var ms=Math.round(performance.now()-t);
|
||
document.getElementById('mN').textContent=ms+'ms';document.getElementById('mN').style.color=ms<500?'var(--ok)':'var(--warn)';
|
||
}catch(e){document.getElementById('mN').textContent='离线';document.getElementById('mN').style.color='var(--err)'}
|
||
}
|
||
checkConn();setInterval(checkConn,30000);
|
||
|
||
/* ════════════════════════════════════════
|
||
DRAGGABLE FAB (touch+mouse · 位置记忆 · 边缘吸附)
|
||
════════════════════════════════════════ */
|
||
(function(){
|
||
var fab=document.getElementById('chatFab');
|
||
var isDrag=false,hasMoved=false;
|
||
var startX,startY,fabX,fabY;
|
||
var fabW=50,fabH=50;
|
||
|
||
// Restore saved position
|
||
try{
|
||
var pos=JSON.parse(localStorage.getItem('hl-fab-pos'));
|
||
if(pos&&typeof pos.x==='number'&&typeof pos.y==='number'){
|
||
var ww=window.innerWidth,wh=window.innerHeight;
|
||
if(pos.x>=0&&pos.x<=ww-fabW&&pos.y>=0&&pos.y<=wh-fabH){
|
||
fab.style.right='auto';fab.style.bottom='auto';
|
||
fab.style.left=pos.x+'px';fab.style.top=pos.y+'px';
|
||
}
|
||
}
|
||
}catch(e){}
|
||
|
||
function getXY(e){
|
||
if(e.touches)return{x:e.touches[0].clientX,y:e.touches[0].clientY};
|
||
return{x:e.clientX,y:e.clientY};
|
||
}
|
||
|
||
function onStart(e){
|
||
isDrag=true;hasMoved=false;
|
||
var xy=getXY(e);
|
||
var rect=fab.getBoundingClientRect();
|
||
startX=xy.x-rect.left;startY=xy.y-rect.top;
|
||
fabX=rect.left;fabY=rect.top;
|
||
fab.style.transition='none';
|
||
}
|
||
|
||
function onMove(e){
|
||
if(!isDrag)return;
|
||
var xy=getXY(e);
|
||
var nx=xy.x-startX,ny=xy.y-startY;
|
||
if(!hasMoved&&Math.abs(nx-fabX)<5&&Math.abs(ny-fabY)<5)return;
|
||
hasMoved=true;
|
||
e.preventDefault();
|
||
fabX=Math.max(0,Math.min(window.innerWidth-fabW,nx));
|
||
fabY=Math.max(0,Math.min(window.innerHeight-fabH,ny));
|
||
fab.style.right='auto';fab.style.bottom='auto';
|
||
fab.style.left=fabX+'px';fab.style.top=fabY+'px';
|
||
}
|
||
|
||
function onEnd(){
|
||
if(!isDrag)return;
|
||
isDrag=false;
|
||
fab.style.transition='all .3s cubic-bezier(.4,0,.2,1)';
|
||
if(hasMoved){
|
||
// Edge snapping: snap to nearest horizontal edge
|
||
var midX=window.innerWidth/2;
|
||
fabX=fabX<midX?14:window.innerWidth-fabW-14;
|
||
fabY=Math.max(60,Math.min(window.innerHeight-fabH-20,fabY));
|
||
fab.style.left=fabX+'px';fab.style.top=fabY+'px';
|
||
try{localStorage.setItem('hl-fab-pos',JSON.stringify({x:fabX,y:fabY}))}catch(e){}
|
||
}
|
||
}
|
||
|
||
fab.addEventListener('touchstart',onStart,{passive:true});
|
||
document.addEventListener('touchmove',onMove,{passive:false});
|
||
document.addEventListener('touchend',onEnd);
|
||
fab.addEventListener('mousedown',function(e){if(e.button===0)onStart(e)});
|
||
document.addEventListener('mousemove',onMove);
|
||
document.addEventListener('mouseup',onEnd);
|
||
|
||
// Override click to prevent toggle when dragging
|
||
var origToggle=function(){
|
||
chatOpen=!chatOpen;
|
||
chatP.classList.toggle('open',chatOpen);
|
||
fab.classList.toggle('on',chatOpen);
|
||
fab.textContent=chatOpen?'✕':'💬';
|
||
if(chatOpen)setTimeout(function(){chatIn.focus()},200);
|
||
};
|
||
fab.onclick=function(e){
|
||
if(hasMoved){hasMoved=false;return}
|
||
origToggle();
|
||
};
|
||
})();
|
||
|
||
/* ════════════════════════════════════════
|
||
SYSTEM STATUS (左面板实时数据)
|
||
════════════════════════════════════════ */
|
||
async function loadSysStatus(){
|
||
try{
|
||
var t=performance.now();
|
||
var r=await fetch('/api/health',{signal:AbortSignal.timeout(5000)});
|
||
var ms=Math.round(performance.now()-t);
|
||
var data=await r.json();
|
||
var sEl=document.getElementById('sysStatus');
|
||
var lEl=document.getElementById('sysLatency');
|
||
var uEl=document.getElementById('sysUptime');
|
||
if(sEl){sEl.textContent='🟢 在线';sEl.className='panel-value ok'}
|
||
if(lEl){lEl.textContent=ms+'ms';lEl.className='panel-value '+(ms<500?'ok':'warn')}
|
||
if(uEl){
|
||
var um=Math.floor(data.uptime/60);
|
||
var uh=Math.floor(um/60);
|
||
uEl.textContent=uh>0?uh+'h '+um%60+'m':um+'分钟';
|
||
}
|
||
}catch(e){
|
||
var sEl=document.getElementById('sysStatus');
|
||
if(sEl){sEl.textContent='🔴 离线';sEl.className='panel-value err'}
|
||
}
|
||
}
|
||
loadSysStatus();
|
||
|
||
/* ════════════════════════════════════════
|
||
BULLETIN & TICKER (动态公告)
|
||
════════════════════════════════════════ */
|
||
async function loadBulletin(){
|
||
try{
|
||
var r=await fetch('/api/system/bulletin',{signal:AbortSignal.timeout(5000)});
|
||
var data=await r.json();
|
||
// Ticker
|
||
var ticker=document.getElementById('ticker');
|
||
if(ticker&&data.updates&&data.updates.length>0){
|
||
var h='';
|
||
data.updates.forEach(function(u){h+='<span><em>📡</em> '+esc(u.version)+' '+esc(u.title)+'</span>'});
|
||
h+='<span><em>✍️</em> 码字工作台 Beta 内测中</span>';
|
||
ticker.innerHTML=h+h; // Duplicate for seamless scroll
|
||
}
|
||
// Version badge
|
||
if(data.system&&data.system.version){
|
||
var vEl=document.getElementById('homeVer');
|
||
if(vEl)vEl.textContent='AGE OS '+data.system.version;
|
||
}
|
||
// Left panel updates
|
||
if(data.updates){
|
||
var uEl=document.getElementById('sysUpdates');
|
||
if(uEl)uEl.innerHTML=data.updates.slice(0,3).map(function(u){
|
||
return '<div class="panel-item"><div class="panel-label">'+esc(u.version)+' · '+esc(u.date)+'</div><div style="font-size:11px">'+esc(u.title)+'</div></div>';
|
||
}).join('');
|
||
}
|
||
}catch(e){/* Use hardcoded ticker content */}
|
||
}
|
||
loadBulletin();
|
||
|
||
/* ════════════════════════════════════════
|
||
FEEDBACK / 留言板
|
||
════════════════════════════════════════ */
|
||
function renderFeedback(){
|
||
var el=document.getElementById('fbList');
|
||
if(!el)return;
|
||
el.innerHTML='<div style="text-align:center;padding:16px;color:var(--muted);font-size:12px">加载中…</div>';
|
||
fetch('/api/feedback',{signal:AbortSignal.timeout(5000)}).then(function(r){return r.json()}).then(function(data){
|
||
var items=data.feedback||[];
|
||
if(items.length===0){
|
||
el.innerHTML='<div class="empty"><em>📮</em>还没有留言<br><br>说点什么吧</div>';
|
||
return;
|
||
}
|
||
el.innerHTML=items.slice(0,20).map(function(fb){
|
||
var st=fb.status==='replied'?'<span class="fb-status replied">已回复</span>':'<span class="fb-status pending">待回复</span>';
|
||
var rp=fb.reply?'<div class="fb-reply">↩ '+esc(fb.reply)+'</div>':'';
|
||
return '<div class="card fb-item">'+
|
||
'<div style="font-size:13px">'+esc(fb.message)+'</div>'+
|
||
'<div class="fb-meta"><span>'+esc(fb.name||'匿名')+'</span>'+st+'<span>'+esc((fb.timestamp||'').slice(0,10))+'</span></div>'+
|
||
rp+'</div>';
|
||
}).join('');
|
||
}).catch(function(){
|
||
el.innerHTML='<div class="empty"><em>📡</em>服务器不在线<br>留言功能需要后端支持</div>';
|
||
});
|
||
}
|
||
|
||
document.getElementById('fbSend').onclick=function(){
|
||
var msg=document.getElementById('fbMsg').value.trim();
|
||
if(!msg){document.getElementById('fbMsg').focus();return}
|
||
var btn=this;btn.disabled=true;btn.textContent='提交中…';
|
||
fetch('/api/feedback',{
|
||
method:'POST',
|
||
headers:{'Content-Type':'application/json'},
|
||
body:JSON.stringify({name:usr.name||'匿名来客',message:msg,userId:usr.id})
|
||
}).then(function(r){return r.json()}).then(function(data){
|
||
btn.disabled=false;btn.textContent='提交留言';
|
||
if(data.success){
|
||
document.getElementById('fbMsg').value='';
|
||
chatMsg('留言已提交 ✅ 冰朔会看到的','b',{m:'铸渊',t:'本地'});
|
||
renderFeedback();
|
||
}
|
||
}).catch(function(){
|
||
btn.disabled=false;btn.textContent='提交留言';
|
||
chatMsg('留言提交失败,服务器不在线','b',{m:'铸渊',t:'本地'});
|
||
});
|
||
};
|
||
|
||
/* ════════════════════════════════════════
|
||
UTILS
|
||
════════════════════════════════════════ */
|
||
function esc(s){if(!s)return'';var d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
||
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|