2682 lines
94 KiB
HTML
2682 lines
94 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>BGI 直播间 UI</title>
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
:root {
|
||
--cream: #fff2c9;
|
||
--cream-soft: #f8eecf;
|
||
--cream-deep: #d9bf7c;
|
||
--mint: #b8ead0;
|
||
--mint-2: #92d8b0;
|
||
--mint-3: #68bd91;
|
||
--leaf: #2f7858;
|
||
--leaf-dark: #153c2f;
|
||
--leaf-deep: #0d2b22;
|
||
--glass: rgba(19, 66, 49, 0.52);
|
||
--glass-strong: rgba(15, 55, 42, 0.72);
|
||
--glass-light: rgba(255, 250, 226, 0.24);
|
||
--line: rgba(255, 244, 202, 0.42);
|
||
--line-green: rgba(165, 235, 197, 0.48);
|
||
--text: #f7fff5;
|
||
--text-dim: #cce7d6;
|
||
--shadow: rgba(12, 47, 35, 0.45);
|
||
--custom-bg-image: none;
|
||
--custom-bg-opacity: 0.65;
|
||
--custom-bg-blur: 0px;
|
||
--custom-bg-size: cover;
|
||
}
|
||
|
||
body {
|
||
background: transparent;
|
||
color: var(--text);
|
||
font-family: "汉仪润圆", "HYRunYuan", "Microsoft YaHei", system-ui, sans-serif;
|
||
font-size: 18px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#viewport {
|
||
width: 100vw;
|
||
height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: transparent;
|
||
}
|
||
|
||
body.preview #viewport {
|
||
background: radial-gradient(circle at 24% 20%, rgba(150, 224, 179, 0.26), transparent 32%),
|
||
radial-gradient(circle at 80% 78%, rgba(255, 236, 177, 0.18), transparent 34%),
|
||
#10281f;
|
||
}
|
||
|
||
#canvas {
|
||
width: 1920px;
|
||
height: 1080px;
|
||
position: relative;
|
||
background: transparent;
|
||
transform-origin: top left;
|
||
}
|
||
|
||
#frame {
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
width: 1920px;
|
||
height: 1080px;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
z-index: 1;
|
||
isolation: isolate;
|
||
/* clip-path: 在背景上挖出 1098×640 圆角矩形镂空(左 192, 顶 210, rx 44) */
|
||
clip-path: path('M0,0 L1920,0 L1920,1080 L0,1080 Z M236,210 A44,44 0 0 0 192,254 V806 A44,44 0 0 0 236,850 H1246 A44,44 0 0 0 1290,806 V254 A44,44 0 0 0 1246,210 Z');
|
||
-webkit-clip-path: path('M0,0 L1920,0 L1920,1080 L0,1080 Z M236,210 A44,44 0 0 0 192,254 V806 A44,44 0 0 0 236,850 H1246 A44,44 0 0 0 1290,806 V254 A44,44 0 0 0 1246,210 Z');
|
||
}
|
||
|
||
#frame::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background: radial-gradient(circle at 12% 20%, rgba(255, 248, 211, 0.72) 0 9%, transparent 25%),
|
||
radial-gradient(circle at 86% 10%, rgba(154, 226, 183, 0.64) 0 11%, transparent 28%),
|
||
radial-gradient(circle at 70% 88%, rgba(255, 239, 190, 0.56) 0 13%, transparent 30%),
|
||
linear-gradient(135deg, rgba(223, 244, 231, 0.88) 0%, rgba(249, 240, 207, 0.82) 47%, rgba(191, 232, 207, 0.88) 100%),
|
||
#1a4a38;
|
||
z-index: 1;
|
||
}
|
||
|
||
#frame::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
opacity: 0.34;
|
||
background-image: linear-gradient(120deg, rgba(255, 255, 255, 0.22) 0 1px, transparent 1px 38px),
|
||
linear-gradient(30deg, rgba(47, 120, 88, 0.10) 0 1px, transparent 1px 46px),
|
||
radial-gradient(circle, rgba(47, 120, 88, 0.13) 1.2px, transparent 1.6px);
|
||
background-size: 88px 88px, 76px 76px, 34px 34px;
|
||
z-index: 3;
|
||
}
|
||
|
||
#custom-bg {
|
||
position: absolute;
|
||
inset: 0;
|
||
background-image: var(--custom-bg-image);
|
||
background-size: var(--custom-bg-size);
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
opacity: var(--custom-bg-opacity);
|
||
filter: blur(var(--custom-bg-blur));
|
||
transform: scale(1.02);
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
}
|
||
|
||
#game-cutout {
|
||
position: absolute;
|
||
left: 192px;
|
||
top: 210px;
|
||
width: 1098px;
|
||
height: 640px;
|
||
border-radius: 44px;
|
||
background: transparent;
|
||
pointer-events: none;
|
||
z-index: 5;
|
||
/* 立体边框: 外发光 + 实线边框 + 内阴影 + 投影 */
|
||
border: 3px solid rgba(255, 247, 208, 0.38);
|
||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.15),
|
||
0 0 32px rgba(12, 47, 35, 0.28),
|
||
0 24px 60px rgba(7, 28, 20, 0.22),
|
||
inset 0 0 0 1px rgba(255, 255, 255, 0.08),
|
||
inset 0 2px 24px rgba(0, 0, 0, 0.18);
|
||
}
|
||
|
||
body.preview #game-cutout {
|
||
background: rgba(8, 24, 19, 0.76);
|
||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.12) inset,
|
||
0 18px 42px rgba(7, 38, 28, 0.26);
|
||
}
|
||
|
||
/* 游戏画面左侧:可用配置组纵向轮播 */
|
||
.available-groups-panel {
|
||
left: 16px;
|
||
top: 210px;
|
||
width: 160px;
|
||
height: 698px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 14px 6px 15px !important;
|
||
}
|
||
|
||
.available-groups-panel .panel-title {
|
||
font-size: 17px;
|
||
gap: 7px;
|
||
white-space: nowrap;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.available-groups-panel .panel-title::before {
|
||
width: 7px;
|
||
height: 13px;
|
||
}
|
||
|
||
.group-carousel-viewport {
|
||
position: relative;
|
||
width: 100%;
|
||
margin-left: 0;
|
||
z-index: 1;
|
||
flex: 1;
|
||
min-height: 0;
|
||
overflow: hidden;
|
||
border-radius: 18px;
|
||
mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
|
||
-webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 24px, #000 calc(100% - 24px), transparent 100%);
|
||
}
|
||
|
||
.group-carousel-track {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
grid-auto-rows: 82px;
|
||
gap: 7px 3px;
|
||
transform: translateY(0);
|
||
will-change: transform;
|
||
}
|
||
|
||
.group-carousel-track.smooth-scroll {
|
||
animation: group-carousel-scroll var(--group-scroll-duration, 90s) linear infinite;
|
||
}
|
||
|
||
@keyframes group-carousel-scroll {
|
||
from { transform: translateY(0); }
|
||
to { transform: translateY(var(--group-scroll-distance, -1000px)); }
|
||
}
|
||
|
||
.group-card {
|
||
position: relative;
|
||
height: 82px;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 2px;
|
||
padding: 5px 3px 4px;
|
||
overflow: hidden;
|
||
isolation: isolate;
|
||
border-radius: 14px;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.14), rgba(120, 214, 158, 0.07));
|
||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.11);
|
||
}
|
||
|
||
.group-card-image {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 46px;
|
||
height: 46px;
|
||
flex-shrink: 0;
|
||
object-fit: contain;
|
||
filter: drop-shadow(0 5px 9px rgba(6, 31, 22, 0.30));
|
||
}
|
||
|
||
.group-card-nahida {
|
||
position: absolute;
|
||
z-index: 1;
|
||
top: 39%;
|
||
left: 69%;
|
||
width: 90px;
|
||
height: 90px;
|
||
object-fit: contain;
|
||
opacity: 0.40;
|
||
pointer-events: none;
|
||
transform: translate(-50%, -50%) rotate(-24deg) scale(1.08);
|
||
filter:
|
||
sepia(0.08)
|
||
saturate(1.18)
|
||
hue-rotate(70deg)
|
||
brightness(1.72)
|
||
contrast(1.12)
|
||
drop-shadow(0 0 3px rgba(218, 255, 228, 0.72))
|
||
drop-shadow(0 0 11px rgba(104, 244, 158, 0.52));
|
||
}
|
||
|
||
.group-card.uses-nahida {
|
||
background:
|
||
radial-gradient(circle at 72% 36%, rgba(116, 241, 161, 0.25), transparent 48%),
|
||
linear-gradient(145deg, rgba(255, 255, 255, 0.16), rgba(92, 225, 144, 0.11));
|
||
border-color: rgba(184, 255, 207, 0.34);
|
||
box-shadow:
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.13),
|
||
inset -12px 8px 24px rgba(87, 224, 139, 0.06);
|
||
}
|
||
|
||
.group-card-name {
|
||
position: relative;
|
||
z-index: 2;
|
||
width: 100%;
|
||
color: var(--cream);
|
||
font-size: 11px;
|
||
line-height: 1.1;
|
||
font-weight: 850;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
text-shadow: 0 2px 7px rgba(0, 0, 0, 0.32);
|
||
}
|
||
|
||
.group-carousel-empty {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
color: rgba(247, 255, 245, 0.54);
|
||
font-size: 13px;
|
||
text-align: center;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.group-carousel-empty[hidden],
|
||
.group-carousel-viewport[hidden] {
|
||
display: none !important;
|
||
}
|
||
|
||
body.frame-off #frame {
|
||
display: none;
|
||
}
|
||
|
||
.deco {
|
||
position: absolute;
|
||
pointer-events: none;
|
||
z-index: 2;
|
||
}
|
||
|
||
.deco-blob {
|
||
border-radius: 44% 56% 64% 36% / 48% 35% 65% 52%;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.48), rgba(255, 255, 255, 0.08)),
|
||
radial-gradient(circle at 34% 30%, rgba(255, 245, 205, 0.85), transparent 28%),
|
||
linear-gradient(135deg, rgba(122, 211, 159, 0.46), rgba(255, 237, 176, 0.36));
|
||
border: 1px solid rgba(255, 255, 255, 0.42);
|
||
box-shadow: inset 0 0 28px rgba(255, 255, 255, 0.22), 0 18px 44px rgba(31, 92, 67, 0.18);
|
||
backdrop-filter: blur(16px) saturate(140%);
|
||
-webkit-backdrop-filter: blur(16px) saturate(140%);
|
||
}
|
||
|
||
.deco-blob.one {
|
||
width: 250px;
|
||
height: 88px;
|
||
left: 1006px;
|
||
top: 22px;
|
||
transform: rotate(-4deg);
|
||
}
|
||
|
||
.deco-blob.two {
|
||
display: none;
|
||
}
|
||
|
||
.deco-blob.three {
|
||
width: 230px;
|
||
height: 230px;
|
||
left: 1648px;
|
||
top: 32px;
|
||
opacity: 0.65;
|
||
}
|
||
|
||
.deco-ribbon {
|
||
left: 1332px;
|
||
top: 18px;
|
||
width: 540px;
|
||
height: 74px;
|
||
border-radius: 32px;
|
||
background: linear-gradient(90deg, rgba(255, 247, 210, 0.36), rgba(137, 218, 171, 0.20));
|
||
border: 1px solid rgba(255, 255, 255, 0.34);
|
||
box-shadow: inset 0 1px 18px rgba(255, 255, 255, 0.24);
|
||
}
|
||
|
||
.deco-leaf {
|
||
width: 82px;
|
||
height: 42px;
|
||
border-radius: 100% 0 100% 0;
|
||
background: linear-gradient(135deg, rgba(96, 183, 132, 0.62), rgba(255, 240, 189, 0.38));
|
||
border: 1px solid rgba(255, 255, 255, 0.30);
|
||
box-shadow: 0 10px 24px rgba(38, 112, 78, 0.16);
|
||
}
|
||
|
||
.deco-leaf::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 13px;
|
||
top: 21px;
|
||
width: 56px;
|
||
height: 1px;
|
||
background: rgba(255, 255, 255, 0.55);
|
||
transform: rotate(-24deg);
|
||
}
|
||
|
||
.leaf-a {
|
||
left: 118px;
|
||
top: 38px;
|
||
transform: rotate(18deg);
|
||
}
|
||
|
||
.leaf-b {
|
||
left: 1212px;
|
||
top: 842px;
|
||
transform: rotate(-38deg);
|
||
}
|
||
|
||
.leaf-c {
|
||
left: 1810px;
|
||
top: 928px;
|
||
transform: rotate(34deg);
|
||
}
|
||
|
||
.deco-star {
|
||
color: rgba(255, 246, 207, 0.92);
|
||
font-size: 28px;
|
||
text-shadow: 0 0 18px rgba(255, 241, 186, 0.66);
|
||
}
|
||
|
||
.star-a {
|
||
left: 510px;
|
||
top: 46px;
|
||
}
|
||
|
||
.star-b {
|
||
left: 1468px;
|
||
top: 954px;
|
||
font-size: 22px;
|
||
}
|
||
|
||
.star-c {
|
||
left: 1850px;
|
||
top: 402px;
|
||
font-size: 18px;
|
||
}
|
||
|
||
#hole-mask,
|
||
#hole-frame {
|
||
display: none;
|
||
}
|
||
|
||
.panel {
|
||
position: absolute;
|
||
z-index: 8;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.18), rgba(255, 255, 255, 0.035)),
|
||
radial-gradient(circle at 18% 0%, rgba(255, 244, 199, 0.18), transparent 42%),
|
||
radial-gradient(circle at 100% 100%, rgba(129, 218, 166, 0.22), transparent 40%),
|
||
var(--glass);
|
||
border: 1px solid rgba(255, 247, 208, 0.34);
|
||
border-top-color: rgba(255, 255, 255, 0.56);
|
||
border-left-color: rgba(255, 255, 255, 0.42);
|
||
border-radius: 26px;
|
||
box-shadow: 0 22px 58px var(--shadow),
|
||
inset 0 1px 0 rgba(255, 255, 255, 0.32),
|
||
inset 0 -18px 36px rgba(18, 70, 51, 0.18);
|
||
padding: 14px 24px 16px;
|
||
overflow: hidden;
|
||
backdrop-filter: blur(20px) saturate(150%);
|
||
-webkit-backdrop-filter: blur(20px) saturate(150%);
|
||
}
|
||
|
||
.panel::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 18px;
|
||
right: 18px;
|
||
top: 7px;
|
||
height: 22px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.30), rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.22));
|
||
opacity: 0.66;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.panel::after {
|
||
content: '';
|
||
position: absolute;
|
||
width: 180px;
|
||
height: 180px;
|
||
right: -80px;
|
||
bottom: -100px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle, rgba(255, 239, 187, 0.18), transparent 66%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
body.glass-on .panel {
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.24), rgba(255, 255, 255, 0.055)),
|
||
radial-gradient(circle at 18% 0%, rgba(255, 244, 199, 0.22), transparent 42%),
|
||
radial-gradient(circle at 100% 100%, rgba(129, 218, 166, 0.25), transparent 40%),
|
||
rgba(19, 66, 49, 0.42);
|
||
backdrop-filter: blur(28px) saturate(180%);
|
||
-webkit-backdrop-filter: blur(28px) saturate(180%);
|
||
}
|
||
|
||
.panel-title {
|
||
position: relative;
|
||
z-index: 1;
|
||
font-size: 20px;
|
||
font-weight: 800;
|
||
color: var(--cream);
|
||
margin-bottom: 6px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
letter-spacing: 0.8px;
|
||
text-shadow: 0 2px 10px rgba(14, 50, 37, 0.46);
|
||
}
|
||
|
||
.panel-title::before {
|
||
content: '';
|
||
width: 9px;
|
||
height: 16px;
|
||
border-radius: 10px 2px 10px 2px;
|
||
background: linear-gradient(180deg, #fff3bf, #87dca8 62%, #4ba977);
|
||
box-shadow: 0 0 16px rgba(170, 235, 193, 0.62);
|
||
transform: rotate(12deg);
|
||
}
|
||
|
||
/* 左上:运行进度 + 排队 / 性能 */
|
||
.top-panel {
|
||
left: 16px;
|
||
top: 16px;
|
||
width: 856px;
|
||
height: 178px;
|
||
display: flex;
|
||
gap: 16px;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.performance-panel {
|
||
left: 888px;
|
||
top: 16px;
|
||
width: 402px;
|
||
height: 178px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.progress-area {
|
||
width: 250px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
min-width: 0;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.queue-area {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
position: relative;
|
||
z-index: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.performance-area {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.queue-list {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
grid-auto-rows: 34px;
|
||
gap: 7px 9px;
|
||
align-content: start;
|
||
}
|
||
|
||
.queue-item {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
min-width: 0;
|
||
font-size: 15px;
|
||
color: var(--text-dim);
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.12), rgba(170, 236, 198, 0.08));
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
padding: 4px 9px;
|
||
border-radius: 14px;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.queue-item.admin {
|
||
background: linear-gradient(90deg, rgba(255, 240, 185, 0.24), rgba(130, 225, 166, 0.18));
|
||
color: var(--cream);
|
||
border-color: rgba(255, 242, 194, 0.36);
|
||
box-shadow: 0 0 18px rgba(157, 232, 184, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.queue-item .idx {
|
||
color: #fff1b7;
|
||
font-weight: 800;
|
||
margin-right: 5px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.queue-item .name {
|
||
min-width: 0;
|
||
max-width: 155px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-weight: 750;
|
||
}
|
||
|
||
.queue-item .pts {
|
||
margin-left: 8px;
|
||
flex-shrink: 0;
|
||
font-size: 14px;
|
||
font-family: "汉仪润圆", "HYRunYuan", "Microsoft YaHei", sans-serif;
|
||
color: #fff1b7;
|
||
font-weight: 850;
|
||
}
|
||
|
||
.queue-more {
|
||
justify-content: center;
|
||
align-items: center;
|
||
font-size: 18px;
|
||
font-weight: 900;
|
||
color: var(--cream);
|
||
letter-spacing: 0.5px;
|
||
background: linear-gradient(135deg, rgba(255, 238, 183, 0.24), rgba(125, 221, 165, 0.20));
|
||
}
|
||
|
||
.badge {
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
background: linear-gradient(135deg, rgba(255, 238, 183, 0.30), rgba(125, 221, 165, 0.26));
|
||
color: var(--cream);
|
||
border: 1px solid rgba(255, 255, 255, 0.24);
|
||
padding: 3px 10px;
|
||
border-radius: 999px;
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.task-name-row {
|
||
min-width: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.task-nahida-icon {
|
||
width: 25px;
|
||
height: 25px;
|
||
flex: 0 0 auto;
|
||
object-fit: contain;
|
||
filter: brightness(1.55) contrast(1.18) drop-shadow(0 1px 4px rgba(6, 31, 22, 0.9));
|
||
}
|
||
|
||
.task-name {
|
||
min-width: 0;
|
||
font-size: 22px;
|
||
font-weight: 850;
|
||
color: var(--cream);
|
||
line-height: 1.12;
|
||
letter-spacing: 0.4px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.progress-area .task-meta {
|
||
gap: 6px;
|
||
flex-direction: column;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.progress-area .task-meta span {
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.progress-bar {
|
||
background: rgba(12, 45, 34, 0.42);
|
||
height: 10px;
|
||
border-radius: 999px;
|
||
overflow: hidden;
|
||
margin-bottom: 8px;
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.20);
|
||
}
|
||
|
||
.progress-fill {
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.35), transparent 30%),
|
||
linear-gradient(90deg, #fff0b5, #a8eabf 45%, #62c58c);
|
||
height: 100%;
|
||
width: 0%;
|
||
border-radius: 999px;
|
||
transition: width 0.5s;
|
||
box-shadow: 0 0 20px rgba(150, 234, 182, 0.46);
|
||
}
|
||
|
||
.task-meta {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
font-size: 15px;
|
||
line-height: 1.1;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.perf-grid {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
align-items: center;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.perf-card {
|
||
height: 106px;
|
||
border-radius: 22px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 7px;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.13), rgba(120, 214, 158, 0.06));
|
||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.perf-ring {
|
||
--pct: 0;
|
||
width: 58px;
|
||
height: 58px;
|
||
border-radius: 50%;
|
||
display: grid;
|
||
place-items: center;
|
||
background: conic-gradient(#fff0b5 0 calc(var(--pct) * 1%), rgba(142, 232, 174, 0.72) calc(var(--pct) * 1%) calc(var(--pct) * 1% + 7%), rgba(255, 255, 255, 0.12) 0 100%);
|
||
box-shadow: 0 0 18px rgba(150, 234, 182, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||
}
|
||
|
||
.perf-ring::before {
|
||
content: '';
|
||
width: 42px;
|
||
height: 42px;
|
||
border-radius: 50%;
|
||
background: rgba(14, 48, 37, 0.78);
|
||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.perf-value {
|
||
position: absolute;
|
||
font-size: 15px;
|
||
font-weight: 900;
|
||
color: #fff2c9;
|
||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.perf-label {
|
||
color: var(--text-dim);
|
||
font-weight: 900;
|
||
font-size: 14px;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
/* 左下:独立下播倒计时 */
|
||
.live-end-panel {
|
||
left: 16px;
|
||
top: 924px;
|
||
width: 160px;
|
||
height: 140px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
padding: 12px 14px 12px;
|
||
}
|
||
|
||
.live-end-countdown {
|
||
flex: 1;
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
/* 左下:音乐 + 点歌队列 */
|
||
.music-panel {
|
||
left: 192px;
|
||
top: 866px;
|
||
width: 662px;
|
||
height: 198px;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: stretch;
|
||
padding-left: 18px;
|
||
padding-right: 18px;
|
||
}
|
||
|
||
.live-end-panel .panel-title,
|
||
.current-music .panel-title,
|
||
.music-extra .panel-title {
|
||
font-size: 17px;
|
||
gap: 7px;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.live-end-panel .panel-title::before,
|
||
.current-music .panel-title::before,
|
||
.music-extra .panel-title::before {
|
||
width: 7px;
|
||
height: 13px;
|
||
}
|
||
|
||
.countdown-stage {
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 0;
|
||
padding-bottom: 1px;
|
||
}
|
||
|
||
.countdown-value {
|
||
color: #fff2c9;
|
||
font-family: "Microsoft YaHei", sans-serif;
|
||
font-size: 24px;
|
||
line-height: 1;
|
||
font-weight: 900;
|
||
letter-spacing: 1px;
|
||
white-space: nowrap;
|
||
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.38), 0 0 18px rgba(255, 240, 181, 0.24);
|
||
}
|
||
|
||
.countdown-value.inactive {
|
||
color: rgba(220, 244, 229, 0.62);
|
||
font-size: 21px;
|
||
}
|
||
|
||
.countdown-target {
|
||
max-width: 100%;
|
||
margin-top: 9px;
|
||
color: var(--text-dim);
|
||
font-size: 12px;
|
||
font-weight: 750;
|
||
line-height: 1.3;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.countdown-day {
|
||
margin-top: 3px;
|
||
color: rgba(255, 241, 183, 0.76);
|
||
font-size: 11px;
|
||
line-height: 1.2;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.current-music {
|
||
width: 286px;
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 7px;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.cover-row {
|
||
flex: 1;
|
||
display: flex;
|
||
gap: 11px;
|
||
align-items: center;
|
||
min-width: 0;
|
||
}
|
||
|
||
.cover {
|
||
width: 82px;
|
||
height: 82px;
|
||
border-radius: 22px;
|
||
flex-shrink: 0;
|
||
background: radial-gradient(circle at 30% 20%, rgba(255, 247, 205, 0.72), transparent 30%),
|
||
linear-gradient(135deg, rgba(121, 216, 159, 0.52), rgba(255, 236, 177, 0.28));
|
||
border: 1px solid rgba(255, 255, 255, 0.32);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 38px;
|
||
color: rgba(255, 246, 209, 0.76);
|
||
overflow: hidden;
|
||
box-shadow: 0 18px 38px rgba(10, 42, 31, 0.28), inset 0 1px 0 rgba(255, 255, 255, 0.30);
|
||
}
|
||
|
||
.cover img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.music-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.music-info .title {
|
||
font-size: 18px;
|
||
font-weight: 850;
|
||
color: var(--cream);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 3px;
|
||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.26);
|
||
}
|
||
|
||
.music-info .artist {
|
||
font-size: 14px;
|
||
color: var(--text-dim);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.music-note {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
width: fit-content;
|
||
margin-top: 3px;
|
||
max-width: 100%;
|
||
padding: 4px 8px;
|
||
border-radius: 999px;
|
||
color: var(--text-dim);
|
||
font-size: 13px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
background: rgba(255, 255, 255, 0.10);
|
||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||
}
|
||
|
||
.music-note::before {
|
||
content: '';
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #8cf1ad;
|
||
box-shadow: 0 0 14px rgba(140, 241, 173, 0.62);
|
||
}
|
||
|
||
.music-extra {
|
||
flex: 1;
|
||
min-width: 0;
|
||
position: relative;
|
||
z-index: 1;
|
||
padding-left: 2px;
|
||
}
|
||
|
||
.playlist {
|
||
min-width: 0;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.playlist-list {
|
||
flex: 1;
|
||
list-style: none;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
overflow-y: auto;
|
||
padding-right: 4px;
|
||
}
|
||
|
||
.playlist-list li {
|
||
display: flex;
|
||
flex-direction: row;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 6px;
|
||
min-width: 0;
|
||
font-size: 13px;
|
||
color: var(--text-dim);
|
||
padding: 6px 7px;
|
||
border-radius: 12px;
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.12), rgba(134, 222, 169, 0.06));
|
||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.playlist-list li .title {
|
||
flex: 1;
|
||
min-width: 0;
|
||
color: var(--cream);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-weight: 650;
|
||
}
|
||
|
||
.playlist-list li .by {
|
||
flex-shrink: 0;
|
||
max-width: 38%;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
font-size: 12px;
|
||
color: rgba(220, 244, 229, 0.72);
|
||
}
|
||
|
||
.input-panel {
|
||
left: 870px;
|
||
top: 866px;
|
||
width: 420px;
|
||
height: 198px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.input-visualizer {
|
||
min-width: 0;
|
||
display: flex;
|
||
flex-direction: column;
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
}
|
||
|
||
.input-stage {
|
||
flex: 1;
|
||
display: grid;
|
||
grid-template-columns: 124px 1fr;
|
||
gap: 14px;
|
||
align-items: center;
|
||
padding-top: 1px;
|
||
}
|
||
|
||
.mouse-card, .keys-card {
|
||
height: 136px;
|
||
border-radius: 18px;
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.13), rgba(120, 214, 158, 0.06));
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.10);
|
||
}
|
||
|
||
.mouse-card {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.mouse-orbit {
|
||
position: absolute;
|
||
left: 10px;
|
||
top: 9px;
|
||
width: 98px;
|
||
height: 98px;
|
||
border-radius: 50%;
|
||
background: radial-gradient(circle at center, rgba(255, 244, 202, 0.10) 0 26%, transparent 27%),
|
||
radial-gradient(circle at center, transparent 0 52%, rgba(255, 255, 255, 0.10) 53%, transparent 55%);
|
||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.mouse-orbit::before, .mouse-orbit::after {
|
||
content: '';
|
||
position: absolute;
|
||
background: rgba(255, 244, 202, 0.10);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.mouse-orbit::before {
|
||
left: 48px;
|
||
top: 9px;
|
||
width: 1px;
|
||
height: 80px;
|
||
}
|
||
|
||
.mouse-orbit::after {
|
||
left: 9px;
|
||
top: 48px;
|
||
width: 80px;
|
||
height: 1px;
|
||
}
|
||
|
||
.mouse-body {
|
||
position: relative;
|
||
width: 42px;
|
||
height: 64px;
|
||
border-radius: 23px 23px 20px 20px;
|
||
background: linear-gradient(145deg, rgba(255, 246, 211, 0.22), rgba(99, 190, 133, 0.18));
|
||
border: 2px solid rgba(255, 244, 202, 0.36);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.24), 0 10px 22px rgba(8, 36, 26, 0.18);
|
||
overflow: hidden;
|
||
z-index: 2;
|
||
}
|
||
|
||
.mouse-body::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 19px;
|
||
top: 2px;
|
||
bottom: 34px;
|
||
width: 2px;
|
||
background: rgba(255, 244, 202, 0.30);
|
||
}
|
||
|
||
.mouse-body::after {
|
||
content: '';
|
||
position: absolute;
|
||
left: 16px;
|
||
top: 18px;
|
||
width: 8px;
|
||
height: 11px;
|
||
border-radius: 999px;
|
||
background: rgba(255, 244, 202, 0.36);
|
||
}
|
||
|
||
.mouse-btn {
|
||
position: absolute;
|
||
top: 0;
|
||
width: 20px;
|
||
height: 30px;
|
||
opacity: 0.34;
|
||
transition: opacity 0.05s, background 0.05s;
|
||
}
|
||
|
||
.mouse-btn.left {
|
||
left: 0;
|
||
border-radius: 23px 0 8px 0;
|
||
}
|
||
|
||
.mouse-btn.right {
|
||
right: 0;
|
||
border-radius: 0 23px 0 8px;
|
||
}
|
||
|
||
.mouse-btn.active {
|
||
opacity: 1;
|
||
background: linear-gradient(180deg, rgba(255, 239, 176, 0.92), rgba(136, 231, 171, 0.66));
|
||
box-shadow: 0 0 18px rgba(158, 238, 186, 0.42);
|
||
}
|
||
|
||
.mouse-trail {
|
||
position: absolute;
|
||
left: 54px;
|
||
top: 54px;
|
||
width: 18px;
|
||
height: 18px;
|
||
margin-left: -9px;
|
||
margin-top: -9px;
|
||
border-radius: 50%;
|
||
opacity: 0;
|
||
transform: translate(0, 0) scale(0.55);
|
||
background: radial-gradient(circle, #fff1b7 0 25%, rgba(142, 232, 174, 0.82) 26% 56%, rgba(142, 232, 174, 0.12) 57% 100%);
|
||
box-shadow: 0 0 18px rgba(255, 241, 183, 0.55), 0 0 28px rgba(142, 232, 174, 0.36);
|
||
transition: transform 0.06s linear, opacity 0.08s linear;
|
||
z-index: 3;
|
||
}
|
||
|
||
.mouse-trail.active {
|
||
opacity: 1;
|
||
}
|
||
|
||
.keys-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.key-row {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.keycap {
|
||
width: 34px;
|
||
height: 29px;
|
||
border-radius: 9px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-family: "汉仪润圆", "HYRunYuan", "Microsoft YaHei", sans-serif;
|
||
font-size: 15px;
|
||
font-weight: 800;
|
||
color: rgba(232, 250, 238, 0.72);
|
||
background: linear-gradient(145deg, rgba(255, 255, 255, 0.10), rgba(128, 218, 164, 0.08));
|
||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||
transition: background 0.04s, color 0.04s, transform 0.04s, box-shadow 0.04s;
|
||
}
|
||
|
||
.keycap.space {
|
||
width: 154px;
|
||
}
|
||
|
||
.keycap.active {
|
||
color: #244233;
|
||
transform: translateY(1px);
|
||
background: linear-gradient(180deg, #fff0b5, #8fe5ae 68%, #62c58c);
|
||
box-shadow: 0 0 16px rgba(150, 234, 182, 0.42), inset 0 1px 0 rgba(255, 255, 255, 0.55);
|
||
}
|
||
|
||
/* 右上:BGI 日志 */
|
||
.log-panel {
|
||
left: 1306px;
|
||
top: 16px;
|
||
width: 598px;
|
||
height: 338px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.log-title {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.log-title .status {
|
||
margin-left: auto;
|
||
}
|
||
|
||
.status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 7px;
|
||
font-size: 15px;
|
||
color: var(--text-dim);
|
||
}
|
||
|
||
.dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #8fa99a;
|
||
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.24);
|
||
}
|
||
|
||
.dot.on {
|
||
background: #8cf1ad;
|
||
box-shadow: 0 0 14px #8cf1ad, 0 0 28px rgba(140, 241, 173, 0.35);
|
||
}
|
||
|
||
.log-box {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
font-family: "Microsoft YaHei", "微软雅黑", sans-serif;
|
||
font-size: 16px;
|
||
line-height: 1.7;
|
||
color: var(--text-dim);
|
||
overflow-y: auto;
|
||
padding: 4px 7px 0 0;
|
||
mask-image: linear-gradient(to bottom, transparent 0, #000 14px, #000 calc(100% - 8px));
|
||
}
|
||
|
||
.log-box::-webkit-scrollbar, .danmu-box::-webkit-scrollbar, .playlist-list::-webkit-scrollbar {
|
||
width: 5px;
|
||
}
|
||
|
||
.log-box::-webkit-scrollbar-thumb, .danmu-box::-webkit-scrollbar-thumb, .playlist-list::-webkit-scrollbar-thumb {
|
||
background: rgba(255, 242, 198, 0.26);
|
||
border-radius: 999px;
|
||
}
|
||
|
||
.log-line {
|
||
word-break: break-all;
|
||
padding: 1px 0;
|
||
text-shadow: 0 1px 4px rgba(0, 0, 0, 0.22);
|
||
}
|
||
|
||
.log-inf {
|
||
color: #aaf1c3;
|
||
}
|
||
|
||
.log-tp {
|
||
color: #ffe8a8;
|
||
}
|
||
|
||
.log-warn {
|
||
color: #ffd08f;
|
||
}
|
||
|
||
.log-err {
|
||
color: #ffaaa7;
|
||
}
|
||
|
||
.log-blue {
|
||
color: #bfe1ff;
|
||
}
|
||
|
||
.log-fight {
|
||
color: #ffc1d0;
|
||
}
|
||
|
||
.log-pick {
|
||
color: #dfd0ff;
|
||
}
|
||
|
||
.log-dbg {
|
||
color: rgba(216, 232, 220, 0.50);
|
||
}
|
||
|
||
/* 右侧中部:教学轮播 */
|
||
.tutorial-panel {
|
||
left: 1306px;
|
||
top: 370px;
|
||
width: 598px;
|
||
height: 140px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tutorial-title {
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.tutorial-title::after {
|
||
content: "轮播";
|
||
margin-left: auto;
|
||
padding: 2px 8px;
|
||
border-radius: 999px;
|
||
font-size: 12px;
|
||
font-weight: 700;
|
||
color: rgba(247, 255, 245, 0.72);
|
||
background: rgba(255, 242, 201, 0.10);
|
||
border: 1px solid rgba(255, 242, 201, 0.13);
|
||
}
|
||
|
||
.tutorial-stage {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 0 14px 3px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tutorial-text {
|
||
width: 100%;
|
||
color: var(--text);
|
||
font-size: 26px;
|
||
font-weight: 900;
|
||
line-height: 1.35;
|
||
text-align: center;
|
||
white-space: nowrap;
|
||
letter-spacing: 0.7px;
|
||
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.48), 0 0 14px rgba(255, 240, 181, 0.18);
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
transition: opacity 0.45s ease, transform 0.45s ease;
|
||
}
|
||
|
||
.tutorial-text.long {
|
||
font-size: 23px;
|
||
letter-spacing: 0.35px;
|
||
}
|
||
|
||
.tutorial-text.extra-long {
|
||
font-size: 21px;
|
||
letter-spacing: 0;
|
||
}
|
||
|
||
.tutorial-text.switching {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
|
||
.tutorial-dots {
|
||
position: relative;
|
||
z-index: 1;
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
padding-bottom: 4px;
|
||
}
|
||
|
||
.tutorial-dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: rgba(247, 255, 245, 0.23);
|
||
transition: width 0.35s ease, background 0.35s ease, box-shadow 0.35s ease;
|
||
}
|
||
|
||
.tutorial-dot.active {
|
||
width: 22px;
|
||
border-radius: 999px;
|
||
background: #fff0b5;
|
||
box-shadow: 0 0 10px rgba(255, 240, 181, 0.48);
|
||
}
|
||
|
||
/* 右下:弹幕 */
|
||
.danmu-panel {
|
||
left: 1306px;
|
||
top: 526px;
|
||
width: 598px;
|
||
height: 538px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.danmu-box {
|
||
position: relative;
|
||
z-index: 1;
|
||
flex: 1;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
overflow-y: auto;
|
||
padding-right: 6px;
|
||
}
|
||
|
||
.danmu-item {
|
||
display: flex;
|
||
gap: 8px;
|
||
align-items: baseline;
|
||
background: linear-gradient(90deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.035)),
|
||
rgba(11, 45, 34, 0.20);
|
||
padding: 8px 11px;
|
||
border-radius: 14px;
|
||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||
}
|
||
|
||
.danmu-name {
|
||
color: #fff1b7;
|
||
font-weight: 800;
|
||
font-size: 18px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.danmu-text {
|
||
color: var(--text);
|
||
font-size: 18px;
|
||
word-break: break-all;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.empty {
|
||
color: rgba(247, 255, 245, 0.48);
|
||
text-align: center;
|
||
padding: 20px;
|
||
font-size: 15px;
|
||
}
|
||
|
||
/* ===== 礼物特效层 ===== */
|
||
#gift-layer {
|
||
position: absolute;
|
||
inset: 0;
|
||
z-index: 60;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#gift-canvas {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.gift-banner {
|
||
position: absolute;
|
||
left: 50%;
|
||
top: 120px;
|
||
transform: translateX(-50%) translateY(-30px) scale(0.8);
|
||
opacity: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
padding: 14px 30px;
|
||
border-radius: 999px;
|
||
font-weight: 900;
|
||
white-space: nowrap;
|
||
transition: transform 0.35s cubic-bezier(0.2, 1.4, 0.4, 1), opacity 0.3s ease;
|
||
}
|
||
|
||
.gift-banner.show {
|
||
transform: translateX(-50%) translateY(0) scale(1);
|
||
opacity: 1;
|
||
}
|
||
|
||
.gift-banner.hide {
|
||
transform: translateX(-50%) translateY(-20px) scale(0.92);
|
||
opacity: 0;
|
||
transition: transform 0.4s ease, opacity 0.4s ease;
|
||
}
|
||
|
||
.gift-banner .gb-name { font-weight: 900; }
|
||
.gift-banner .gb-gift { font-weight: 800; }
|
||
|
||
.gift-banner.tier-1 {
|
||
font-size: 22px;
|
||
color: #eafff2;
|
||
background: rgba(20, 60, 45, 0.72);
|
||
border: 1px solid rgba(155, 240, 190, 0.35);
|
||
box-shadow: 0 4px 18px rgba(0, 0, 0, 0.35);
|
||
}
|
||
|
||
.gift-banner.tier-2 {
|
||
font-size: 26px;
|
||
color: #fff3c4;
|
||
background: rgba(70, 52, 10, 0.78);
|
||
border: 1px solid rgba(255, 214, 110, 0.55);
|
||
box-shadow: 0 6px 26px rgba(0, 0, 0, 0.4), 0 0 24px rgba(255, 200, 80, 0.25);
|
||
}
|
||
|
||
.gift-banner.tier-3 {
|
||
font-size: 30px;
|
||
color: #ffe9ec;
|
||
background: rgba(88, 22, 40, 0.82);
|
||
border: 2px solid rgba(255, 130, 160, 0.7);
|
||
box-shadow: 0 8px 34px rgba(0, 0, 0, 0.45), 0 0 40px rgba(255, 110, 150, 0.4);
|
||
animation: gift-pulse 0.9s ease-in-out infinite alternate;
|
||
}
|
||
|
||
.gift-banner.tier-4 {
|
||
font-size: 34px;
|
||
color: #fff;
|
||
background: rgba(60, 20, 90, 0.85);
|
||
border: 2px solid rgba(220, 160, 255, 0.85);
|
||
box-shadow: 0 10px 44px rgba(0, 0, 0, 0.5), 0 0 60px rgba(200, 130, 255, 0.55);
|
||
animation: gift-pulse 0.6s ease-in-out infinite alternate;
|
||
}
|
||
|
||
@keyframes gift-pulse {
|
||
from { filter: brightness(1); }
|
||
to { filter: brightness(1.25); }
|
||
}
|
||
|
||
.gift-flash {
|
||
position: absolute;
|
||
inset: 0;
|
||
background: radial-gradient(circle at 50% 30%, rgba(255, 235, 170, 0.35), transparent 60%);
|
||
opacity: 0;
|
||
transition: opacity 0.25s ease;
|
||
}
|
||
|
||
.gift-flash.on { opacity: 1; }
|
||
|
||
/* ================================================================
|
||
主题预设:aurora(极光)
|
||
仅做视觉美化,不改变任何面板位置/尺寸/布局(left/top/width/height 不变)。
|
||
通过 body.theme-aurora 覆盖颜色变量与玻璃质感。
|
||
================================================================ */
|
||
body.theme-aurora {
|
||
--cream: #eef3ff;
|
||
--cream-soft: #dce6fb;
|
||
--cream-deep: #a8baff;
|
||
--mint: #6fe0ff;
|
||
--mint-2: #5bc0ff;
|
||
--mint-3: #7d8bff;
|
||
--leaf: #5566ff;
|
||
--leaf-dark: #2c2c66;
|
||
--leaf-deep: #131436;
|
||
--glass: rgba(16, 20, 48, 0.50);
|
||
--glass-strong: rgba(10, 12, 32, 0.72);
|
||
--glass-light: rgba(150, 180, 255, 0.20);
|
||
--line: rgba(150, 180, 255, 0.40);
|
||
--line-green: rgba(120, 200, 255, 0.46);
|
||
--text: #f4f7ff;
|
||
--text-dim: #b7c4e6;
|
||
--shadow: rgba(3, 5, 18, 0.62);
|
||
}
|
||
|
||
body.theme-aurora #frame::before {
|
||
background:
|
||
radial-gradient(circle at 14% 18%, rgba(122, 140, 255, 0.52) 0 10%, transparent 30%),
|
||
radial-gradient(circle at 88% 12%, rgba(94, 231, 255, 0.48) 0 12%, transparent 32%),
|
||
radial-gradient(circle at 72% 90%, rgba(255, 122, 217, 0.44) 0 14%, transparent 34%),
|
||
radial-gradient(circle at 30% 86%, rgba(52, 245, 197, 0.38) 0 10%, transparent 30%),
|
||
linear-gradient(135deg, rgba(20, 22, 56, 0.92) 0%, rgba(28, 24, 68, 0.88) 50%, rgba(13, 18, 50, 0.92) 100%),
|
||
#0b0e26;
|
||
}
|
||
|
||
body.theme-aurora #frame::after {
|
||
opacity: 0.28;
|
||
background-image:
|
||
linear-gradient(120deg, rgba(160, 190, 255, 0.20) 0 1px, transparent 1px 40px),
|
||
linear-gradient(30deg, rgba(120, 200, 255, 0.12) 0 1px, transparent 1px 48px),
|
||
radial-gradient(circle, rgba(150, 180, 255, 0.20) 1.4px, transparent 1.8px);
|
||
background-size: 90px 90px, 78px 78px, 36px 36px;
|
||
}
|
||
|
||
body.theme-aurora #game-cutout {
|
||
border: 3px solid rgba(150, 180, 255, 0.46);
|
||
box-shadow:
|
||
0 0 0 1px rgba(180, 200, 255, 0.18),
|
||
0 0 40px rgba(90, 120, 255, 0.30),
|
||
0 26px 70px rgba(4, 6, 24, 0.55),
|
||
inset 0 0 0 1px rgba(200, 220, 255, 0.10),
|
||
inset 0 2px 28px rgba(0, 0, 0, 0.32);
|
||
animation: aurora-cutout-glow 5s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes aurora-cutout-glow {
|
||
0%, 100% {
|
||
box-shadow:
|
||
0 0 0 1px rgba(180, 200, 255, 0.18),
|
||
0 0 40px rgba(90, 120, 255, 0.30),
|
||
0 26px 70px rgba(4, 6, 24, 0.55),
|
||
inset 0 0 0 1px rgba(200, 220, 255, 0.10),
|
||
inset 0 2px 28px rgba(0, 0, 0, 0.32);
|
||
}
|
||
50% {
|
||
box-shadow:
|
||
0 0 0 1px rgba(200, 220, 255, 0.28),
|
||
0 0 58px rgba(110, 150, 255, 0.46),
|
||
0 26px 70px rgba(4, 6, 24, 0.55),
|
||
inset 0 0 0 1px rgba(220, 235, 255, 0.16),
|
||
inset 0 2px 28px rgba(0, 0, 0, 0.32);
|
||
}
|
||
}
|
||
|
||
body.theme-aurora .panel {
|
||
background:
|
||
linear-gradient(145deg, rgba(150, 180, 255, 0.16), rgba(150, 180, 255, 0.04)),
|
||
radial-gradient(circle at 18% 0%, rgba(122, 140, 255, 0.20), transparent 44%),
|
||
radial-gradient(circle at 100% 100%, rgba(94, 231, 255, 0.18), transparent 42%),
|
||
rgba(16, 20, 46, 0.55);
|
||
border: 1px solid rgba(150, 180, 255, 0.36);
|
||
border-top-color: rgba(190, 210, 255, 0.52);
|
||
border-left-color: rgba(170, 195, 255, 0.42);
|
||
box-shadow:
|
||
0 22px 58px var(--shadow),
|
||
inset 0 1px 0 rgba(200, 220, 255, 0.22),
|
||
inset 0 -18px 36px rgba(8, 10, 30, 0.32);
|
||
backdrop-filter: blur(22px) saturate(160%);
|
||
-webkit-backdrop-filter: blur(22px) saturate(160%);
|
||
}
|
||
|
||
body.theme-aurora .panel::before {
|
||
background: linear-gradient(90deg, rgba(170, 195, 255, 0.28), rgba(170, 195, 255, 0.06), rgba(120, 200, 255, 0.22));
|
||
}
|
||
|
||
body.theme-aurora .panel::after {
|
||
background: radial-gradient(circle, rgba(94, 231, 255, 0.16), transparent 66%);
|
||
}
|
||
|
||
body.theme-aurora .panel-title::before {
|
||
background: linear-gradient(180deg, #cfe0ff, #8aa4ff 55%, #5e7cff);
|
||
box-shadow: 0 0 18px rgba(130, 160, 255, 0.65);
|
||
}
|
||
|
||
body.theme-aurora .queue-item {
|
||
background: linear-gradient(90deg, rgba(150, 180, 255, 0.14), rgba(94, 231, 255, 0.10));
|
||
border: 1px solid rgba(150, 180, 255, 0.20);
|
||
}
|
||
|
||
body.theme-aurora .queue-item.admin {
|
||
background: linear-gradient(90deg, rgba(122, 140, 255, 0.30), rgba(94, 231, 255, 0.20));
|
||
border-color: rgba(160, 190, 255, 0.42);
|
||
box-shadow: 0 0 20px rgba(122, 140, 255, 0.24), inset 0 1px 0 rgba(220, 235, 255, 0.20);
|
||
}
|
||
|
||
body.theme-aurora .queue-item .idx { color: #cfe0ff; }
|
||
body.theme-aurora .queue-item .pts { color: #cfe0ff; }
|
||
|
||
body.theme-aurora .group-card {
|
||
background: linear-gradient(145deg, rgba(150, 180, 255, 0.16), rgba(94, 231, 255, 0.08));
|
||
border: 1px solid rgba(150, 180, 255, 0.20);
|
||
box-shadow: inset 0 1px 0 rgba(220, 235, 255, 0.14);
|
||
}
|
||
|
||
body.theme-aurora .group-card.uses-nahida {
|
||
background:
|
||
radial-gradient(circle at 72% 36%, rgba(122, 200, 255, 0.28), transparent 48%),
|
||
linear-gradient(145deg, rgba(150, 180, 255, 0.18), rgba(94, 231, 255, 0.12));
|
||
border-color: rgba(160, 210, 255, 0.38);
|
||
box-shadow:
|
||
inset 0 1px 0 rgba(220, 235, 255, 0.16),
|
||
inset -12px 8px 24px rgba(80, 120, 255, 0.10);
|
||
}
|
||
|
||
body.theme-aurora .group-card-name {
|
||
text-shadow: 0 2px 8px rgba(4, 6, 24, 0.5);
|
||
}
|
||
|
||
body.theme-aurora .progress-bar {
|
||
background: rgba(14, 18, 46, 0.5);
|
||
border: 1px solid rgba(150, 180, 255, 0.22);
|
||
}
|
||
|
||
body.theme-aurora .progress-fill {
|
||
background:
|
||
linear-gradient(90deg, rgba(255, 255, 255, 0.32), transparent 30%),
|
||
linear-gradient(90deg, #cfe0ff, #8aa4ff 45%, #5e7cff);
|
||
box-shadow: 0 0 20px rgba(130, 160, 255, 0.5);
|
||
}
|
||
|
||
body.theme-aurora .perf-card {
|
||
background: linear-gradient(145deg, rgba(150, 180, 255, 0.13), rgba(94, 231, 255, 0.06));
|
||
border: 1px solid rgba(150, 180, 255, 0.18);
|
||
}
|
||
|
||
body.theme-aurora .tutorial-title::after {
|
||
color: rgba(230, 238, 255, 0.74);
|
||
background: rgba(150, 180, 255, 0.12);
|
||
border: 1px solid rgba(150, 180, 255, 0.22);
|
||
}
|
||
|
||
body.theme-aurora .tutorial-dot.active {
|
||
background: #cfe0ff;
|
||
box-shadow: 0 0 10px rgba(150, 180, 255, 0.55);
|
||
}
|
||
|
||
body.theme-aurora .tutorial-text {
|
||
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.5), 0 0 16px rgba(150, 180, 255, 0.22);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="viewport">
|
||
<div id="canvas">
|
||
<div id="frame">
|
||
<div id="custom-bg"></div>
|
||
<div class="deco deco-ribbon"></div>
|
||
<div class="deco deco-blob one"></div>
|
||
<div class="deco deco-blob two"></div>
|
||
<div class="deco deco-blob three"></div>
|
||
<div class="deco deco-leaf leaf-a"></div>
|
||
<div class="deco deco-leaf leaf-b"></div>
|
||
<div class="deco deco-leaf leaf-c"></div>
|
||
<div class="deco deco-star star-a">✦</div>
|
||
<div class="deco deco-star star-b">✦</div>
|
||
<div class="deco deco-star star-c">✦</div>
|
||
</div>
|
||
<!-- game-cutout 放在 #frame 外面, 不受 clip-path 裁剪, 保证镂空区域完全透明(OBS 穿透) -->
|
||
<div id="game-cutout"></div>
|
||
|
||
<!-- 游戏画面左侧:可用配置组 -->
|
||
<section class="panel available-groups-panel">
|
||
<div class="panel-title">可用配置组</div>
|
||
<div id="group-carousel-viewport" class="group-carousel-viewport">
|
||
<div id="group-carousel-track" class="group-carousel-track"></div>
|
||
</div>
|
||
<div id="group-carousel-empty" class="group-carousel-empty" hidden>暂无可展示的配置组</div>
|
||
</section>
|
||
|
||
<!-- 左上:运行进度 + 排队状态 -->
|
||
<section class="panel top-panel">
|
||
<div class="progress-area">
|
||
<div class="panel-title">运行进度</div>
|
||
<div class="task-name-row">
|
||
<img id="task-nahida-icon" class="task-nahida-icon" src="/uploads/nahida_collect.webp" alt="纳西妲采集" title="该配置组使用纳西妲采集" hidden>
|
||
<div id="task-name" class="task-name">无任务运行</div>
|
||
</div>
|
||
<div class="progress-bar">
|
||
<div id="progress-fill" class="progress-fill"></div>
|
||
</div>
|
||
<div class="task-meta">
|
||
<span id="task-operator">操作人:--</span>
|
||
<span id="task-points">剩余积分:--</span>
|
||
</div>
|
||
</div>
|
||
<div class="queue-area">
|
||
<div class="panel-title">排队状态 <span id="queue-count" class="badge">0</span></div>
|
||
<div id="queue-list" class="queue-list">
|
||
<div class="empty">队列为空</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 左上右侧:性能监测 -->
|
||
<section class="panel performance-panel">
|
||
<div class="panel-title">性能监测</div>
|
||
<div class="perf-grid">
|
||
<div class="perf-card">
|
||
<div id="perf-cpu-ring" class="perf-ring"><span id="perf-cpu" class="perf-value">--%</span></div>
|
||
<div class="perf-label">CPU</div>
|
||
</div>
|
||
<div class="perf-card">
|
||
<div id="perf-mem-ring" class="perf-ring"><span id="perf-mem" class="perf-value">--%</span></div>
|
||
<div class="perf-label">内存</div>
|
||
</div>
|
||
<div class="perf-card">
|
||
<div id="perf-gpu-ring" class="perf-ring"><span id="perf-gpu" class="perf-value">--%</span></div>
|
||
<div class="perf-label">GPU</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 左下:独立下播倒计时 -->
|
||
<section class="panel live-end-panel">
|
||
<div class="panel-title">下播倒计时</div>
|
||
<div class="live-end-countdown">
|
||
<div class="countdown-stage">
|
||
<div id="live-end-countdown" class="countdown-value inactive">--:--:--</div>
|
||
<div id="live-end-target" class="countdown-target">正在读取关播计划</div>
|
||
<div id="live-end-day" class="countdown-day">系统定时</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 左下:当前音乐 + 播放列表 -->
|
||
<section class="panel music-panel">
|
||
<div class="current-music">
|
||
<div class="panel-title">当前音乐</div>
|
||
<div class="cover-row">
|
||
<div id="music-cover" class="cover">♪</div>
|
||
<div class="music-info">
|
||
<div id="music-title" class="title">未配置</div>
|
||
<div id="music-artist" class="artist">等待数据源</div>
|
||
<div id="music-note" class="music-note">正在同步播放器</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="music-extra">
|
||
<div class="playlist">
|
||
<div class="panel-title">点歌队列</div>
|
||
<ul id="playlist" class="playlist-list">
|
||
<li class="empty">暂无点歌</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 左下右侧:键鼠输入 -->
|
||
<section class="panel input-panel">
|
||
<div class="input-visualizer">
|
||
<div class="panel-title">键鼠输入</div>
|
||
<div class="input-stage">
|
||
<div class="mouse-card">
|
||
<div class="mouse-orbit"></div>
|
||
<div class="mouse-body">
|
||
<div id="mouse-left" class="mouse-btn left"></div>
|
||
<div id="mouse-right" class="mouse-btn right"></div>
|
||
</div>
|
||
<div id="mouse-trail" class="mouse-trail"></div>
|
||
</div>
|
||
<div class="keys-card">
|
||
<div class="key-row">
|
||
<div id="key-q" class="keycap">Q</div>
|
||
<div id="key-w" class="keycap">W</div>
|
||
<div id="key-e" class="keycap">E</div>
|
||
<div id="key-r" class="keycap">R</div>
|
||
</div>
|
||
<div class="key-row">
|
||
<div id="key-a" class="keycap">A</div>
|
||
<div id="key-s" class="keycap">S</div>
|
||
<div id="key-d" class="keycap">D</div>
|
||
<div id="key-f" class="keycap">F</div>
|
||
</div>
|
||
<div class="key-row">
|
||
<div id="key-space" class="keycap space">SPACE</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 右上:BGI 日志 -->
|
||
<section class="panel log-panel">
|
||
<div class="panel-title log-title">
|
||
<span>BGI 日志</span>
|
||
<div class="status"><span id="bgi-dot" class="dot"></span><span id="bgi-status">未运行</span></div>
|
||
</div>
|
||
<div id="log-box" class="log-box"></div>
|
||
</section>
|
||
|
||
<!-- 右侧中部:教学轮播 -->
|
||
<section class="panel tutorial-panel">
|
||
<div class="panel-title tutorial-title">直播间使用说明</div>
|
||
<div class="tutorial-stage">
|
||
<div id="tutorial-text" class="tutorial-text">发送“签到”领取积分,发送“排队”加入队伍</div>
|
||
</div>
|
||
<div id="tutorial-dots" class="tutorial-dots" aria-hidden="true"></div>
|
||
</section>
|
||
|
||
<!-- 右下:直播间弹幕 -->
|
||
<section class="panel danmu-panel">
|
||
<div class="panel-title">直播间弹幕</div>
|
||
<div id="danmu-box" class="danmu-box">
|
||
<div class="empty">等待弹幕...</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 礼物特效层(最顶层,不拦截事件) -->
|
||
<div id="gift-layer">
|
||
<canvas id="gift-canvas"></canvas>
|
||
<div id="gift-flash" class="gift-flash"></div>
|
||
<div id="gift-banner" class="gift-banner tier-1" style="display:none">
|
||
<span class="gb-name"></span><span class="gb-gift"></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
function esc(s) {
|
||
return String(s).replace(/[&<>"']/g, c => ({
|
||
"&": "&",
|
||
"<": "<",
|
||
">": ">",
|
||
"\"": """,
|
||
"'": "'"
|
||
}[c]));
|
||
}
|
||
|
||
function fmt(sec) {
|
||
const m = Math.floor(sec / 60), s = Math.abs(sec) % 60;
|
||
return String(m).padStart(2, "0") + ":" + String(s).padStart(2, "0");
|
||
}
|
||
|
||
function fmtMin(sec) {
|
||
const m = Math.floor(sec / 60);
|
||
return m + ":" + String(sec % 60).padStart(2, "0");
|
||
}
|
||
|
||
const tutorialSlides = [
|
||
"发送“签到”领取积分,发送“排队”加入队伍",
|
||
"成为队首后,请在90秒内发送“上号”",
|
||
"发送“上号”后,请在4分钟内完成扫码登录",
|
||
"扫码后,请在1分钟内发送“是”或“不是”确认账号",
|
||
"发送“执行 配置组名称”开始任务",
|
||
"发送“积分”查询积分,发送“队列”查看排队情况",
|
||
"发送“退出”离开队伍,发送“点歌 歌名”点歌"
|
||
];
|
||
let tutorialIndex = 0;
|
||
|
||
function renderTutorialDots() {
|
||
const dots = document.getElementById("tutorial-dots");
|
||
if (!dots) return;
|
||
dots.innerHTML = tutorialSlides.map((_, index) =>
|
||
'<span class="tutorial-dot' + (index === tutorialIndex ? ' active' : '') + '"></span>'
|
||
).join("");
|
||
}
|
||
|
||
function showTutorialSlide(index, animate = true) {
|
||
const text = document.getElementById("tutorial-text");
|
||
if (!text) return;
|
||
tutorialIndex = (index + tutorialSlides.length) % tutorialSlides.length;
|
||
const update = () => {
|
||
const slide = tutorialSlides[tutorialIndex];
|
||
text.textContent = slide;
|
||
text.classList.toggle("long", slide.length > 20 && slide.length < 24);
|
||
text.classList.toggle("extra-long", slide.length >= 24);
|
||
renderTutorialDots();
|
||
requestAnimationFrame(() => text.classList.remove("switching"));
|
||
};
|
||
if (!animate) {
|
||
update();
|
||
return;
|
||
}
|
||
text.classList.add("switching");
|
||
window.setTimeout(update, 450);
|
||
}
|
||
|
||
function startTutorialCarousel() {
|
||
showTutorialSlide(0, false);
|
||
window.setInterval(() => showTutorialSlide(tutorialIndex + 1), 6500);
|
||
}
|
||
|
||
function simplifyLogLine(line) {
|
||
let text = String(line || "");
|
||
let time = "";
|
||
|
||
// BetterGI 原生日志: [21:46:36.567] [INF] Logger.Name 交互或拾取:"霓裳花"
|
||
let m = text.match(/^\[(\d{2}:\d{2}:\d{2})(?:\.\d+)?\]\s*(?:\[[A-Z]+\]\s*)?(.*)$/);
|
||
if (m) {
|
||
time = m[1];
|
||
text = m[2] || "";
|
||
} else {
|
||
// 本项目日志: 21:46:36 [INFO] [日志监控] ...
|
||
m = text.match(/^(\d{2}:\d{2}:\d{2})(?:\.\d+)?\s*(?:\[[A-Z]+\]\s*)?(.*)$/);
|
||
if (m) {
|
||
time = m[1];
|
||
text = m[2] || "";
|
||
}
|
||
}
|
||
|
||
text = text.trim();
|
||
// 去掉常见 logger 全限定名,只保留真正消息内容。
|
||
text = text.replace(/^(?:[A-Za-z_][\w$]*\.)+[A-Za-z_][\w$]*\s+/, "");
|
||
// 兼容 AutoPickTrigger 后面没有空格或格式略有变化的情况。
|
||
text = text.replace(/^BetterGenshinImpact\.[\w.]+\s*/, "");
|
||
// 前台只保留秒级时间,不显示毫秒、级别、logger 名。
|
||
return time ? ("[" + time + "] " + text) : text;
|
||
}
|
||
|
||
function simplifyLogLines(lines) {
|
||
const result = [];
|
||
const arr = Array.isArray(lines) ? lines : [];
|
||
for (let i = 0; i < arr.length; i++) {
|
||
const current = simplifyLogLine(arr[i]);
|
||
// 有些 BetterGI 日志会把“时间/级别/logger”和正文拆成两行,前台合并成一行显示。
|
||
if (/^\[\d{2}:\d{2}:\d{2}\]\s*$/.test(current) && i + 1 < arr.length) {
|
||
const next = simplifyLogLine(arr[i + 1]).trim();
|
||
if (next && !/^\[\d{2}:\d{2}:\d{2}\]/.test(next)) {
|
||
result.push({raw: String(arr[i] || "") + " " + String(arr[i + 1] || ""), text: current + next});
|
||
i++;
|
||
continue;
|
||
}
|
||
}
|
||
result.push({raw: String(arr[i] || ""), text: current});
|
||
}
|
||
return result.filter(x => x.text.trim());
|
||
}
|
||
|
||
function colorLog(line) {
|
||
const raw = typeof line === "string" ? line : String((line && line.raw) || "");
|
||
const text = typeof line === "string" ? simplifyLogLine(line) : String((line && line.text) || "");
|
||
if (raw.includes("[ERR]") || text.includes("Error") || text.includes("Exception")) return "log-err";
|
||
if (raw.includes("[WRN]") || text.includes("传送完成") || text.includes("返回主界面")) return "log-tp";
|
||
if (text.includes("开始连续执行") || text.includes("执行结束")) return "log-inf";
|
||
if (text.includes("加载完成") || text.includes("初始化")) return "log-blue";
|
||
if (text.includes("动作") || text.includes("战斗") || text.includes("攻击")) return "log-fight";
|
||
if (text.includes("自动拾取") || text.includes("交互或拾取")) return "log-pick";
|
||
if (raw.includes("[DBG]")) return "log-dbg";
|
||
return "";
|
||
}
|
||
|
||
let musicState = {playing: false, current: null, playlist: [], requests: []};
|
||
let availableGroups = [];
|
||
const fallbackSystemScheduleConfig = {
|
||
live_start_time: "09:00",
|
||
live_end_time: "23:00"
|
||
};
|
||
let systemScheduleConfig = {...fallbackSystemScheduleConfig};
|
||
let inputState = {keys: {}, mouse: {left: false, right: false, direction: "idle"}};
|
||
let performanceState = {cpu: null, memory: null, gpu: null, gpu_available: false};
|
||
let lastCoverKey = "";
|
||
|
||
async function fetchState() {
|
||
try {
|
||
const r = await fetch("/api/state");
|
||
const d = await r.json();
|
||
renderTask(d.current_group, d.group_start_time, d.current_admin, d.users, d.default_running, d.bgi_running, d.current_group_uses_nahida_collect);
|
||
renderQueue(d.queue, d.users, d.current_admin);
|
||
initGiftFxBaseline(d.recent_gifts);
|
||
giftFx.sync(d.recent_gifts);
|
||
} catch (e) {
|
||
console.error("state", e);
|
||
}
|
||
}
|
||
|
||
function shortName(name, maxChars = 8) {
|
||
const arr = Array.from(String(name || ""));
|
||
return arr.length > maxChars ? arr.slice(0, maxChars).join("") + "…" : arr.join("");
|
||
}
|
||
|
||
function renderTask(group, startTime, adminUid, users, isDefault, bgiRunning, usesNahidaCollect) {
|
||
const nameEl = document.getElementById("task-name");
|
||
const nahidaIcon = document.getElementById("task-nahida-icon");
|
||
const pointsEl = document.getElementById("task-points");
|
||
const opEl = document.getElementById("task-operator");
|
||
const fill = document.getElementById("progress-fill");
|
||
const dot = document.getElementById("bgi-dot");
|
||
const status = document.getElementById("bgi-status");
|
||
|
||
if (bgiRunning) {
|
||
dot.classList.add("on");
|
||
status.textContent = "运行中";
|
||
status.style.color = "#9df0bb";
|
||
} else {
|
||
dot.classList.remove("on");
|
||
status.textContent = "未运行";
|
||
status.style.color = "var(--text-dim)";
|
||
}
|
||
|
||
if (!group) {
|
||
nahidaIcon.hidden = true;
|
||
nameEl.textContent = "无任务运行";
|
||
nameEl.style.color = "var(--text-dim)";
|
||
opEl.textContent = "操作人:--";
|
||
pointsEl.textContent = "剩余积分:--";
|
||
fill.style.width = "0%";
|
||
return;
|
||
}
|
||
nahidaIcon.hidden = !usesNahidaCollect;
|
||
nameEl.textContent = group + (isDefault ? " (默认)" : "");
|
||
nameEl.style.color = "var(--cream)";
|
||
if (startTime) {
|
||
const sec = Math.floor((Date.now() - new Date(startTime).getTime()) / 1000);
|
||
fill.style.width = Math.min(sec / 600 * 100, 100) + "%";
|
||
} else fill.style.width = "0%";
|
||
if (adminUid && users && users[adminUid]) {
|
||
const u = users[adminUid];
|
||
opEl.textContent = "操作人:" + shortName(u.uname, 8);
|
||
pointsEl.textContent = "剩余积分:" + (Number.isFinite(Number(u.points)) ? Number(u.points) : 0) + "🌟";
|
||
} else if (isDefault) {
|
||
opEl.textContent = "操作人:系统默认";
|
||
pointsEl.textContent = "剩余积分:--";
|
||
} else {
|
||
opEl.textContent = "操作人:--";
|
||
pointsEl.textContent = "剩余积分:--";
|
||
}
|
||
}
|
||
|
||
function renderQueue(queue, users, adminUid) {
|
||
const el = document.getElementById("queue-list");
|
||
const cnt = document.getElementById("queue-count");
|
||
const total = (queue && queue.length) || 0;
|
||
cnt.textContent = total;
|
||
if (!queue || total === 0) {
|
||
el.innerHTML = '<div class="empty">队列为空</div>';
|
||
return;
|
||
}
|
||
let html = queue.slice(0, 6).map((uid, i) => {
|
||
const u = (users && users[uid]) || {uname: "用户" + uid, points: 0};
|
||
const isAdmin = uid === adminUid;
|
||
return '<div class="queue-item' + (isAdmin ? ' admin' : '') + '">' +
|
||
'<span class="name"><span class="idx">#' + (i + 1) + '</span>' + esc(shortName(u.uname, 8)) + '</span>' +
|
||
'<span class="pts">' + (Number.isFinite(Number(u.points)) ? Number(u.points) : 0) + '🌟</span></div>';
|
||
}).join("");
|
||
if (total > 6) html += '<div class="queue-item queue-more">+' + (total - 6) + '</div>';
|
||
el.innerHTML = html;
|
||
}
|
||
|
||
async function fetchLog() {
|
||
try {
|
||
const r = await fetch("/api/bgi-log?lines=80");
|
||
const d = await r.json();
|
||
const el = document.getElementById("log-box");
|
||
el.innerHTML = simplifyLogLines(d.lines).map(item => {
|
||
const cls = colorLog(item);
|
||
return '<div class="log-line' + (cls ? ' ' + cls : '') + '">' + esc(item.text) + '</div>';
|
||
}).join("");
|
||
el.scrollTop = el.scrollHeight;
|
||
} catch (e) {
|
||
console.error("log", e);
|
||
}
|
||
}
|
||
|
||
async function fetchDanmu() {
|
||
try {
|
||
const r = await fetch("/api/danmu?limit=60");
|
||
const d = await r.json();
|
||
const el = document.getElementById("danmu-box");
|
||
const msgs = d.messages || [];
|
||
if (msgs.length === 0) {
|
||
el.innerHTML = '<div class="empty">等待弹幕...</div>';
|
||
return;
|
||
}
|
||
el.innerHTML = msgs.map(m => {
|
||
return '<div class="danmu-item"><span class="danmu-name">' + esc(m.uname || "?") + ':</span>' +
|
||
'<span class="danmu-text">' + esc(m.text || "") + '</span></div>';
|
||
}).join("");
|
||
el.scrollTop = el.scrollHeight;
|
||
} catch (e) {
|
||
console.error("danmu", e);
|
||
}
|
||
}
|
||
|
||
async function fetchMusic() {
|
||
try {
|
||
const r = await fetch("/api/music");
|
||
const d = await r.json();
|
||
musicState = d || {playing: false, current: null, playlist: [], requests: []};
|
||
renderMusic();
|
||
} catch (e) {
|
||
console.error("music", e);
|
||
}
|
||
}
|
||
|
||
function groupCardHtml(group) {
|
||
const usesNahida = !!group.uses_nahida_collect;
|
||
const nahidaTitle = usesNahida
|
||
? '使用纳西妲采集' + (group.nahida_route_count ? '(' + group.nahida_route_count + '条路线)' : '')
|
||
: '';
|
||
return '<div class="group-card' + (usesNahida ? ' uses-nahida' : '') + '"' +
|
||
(nahidaTitle ? ' title="' + esc(nahidaTitle) + '"' : '') + '>' +
|
||
'<img class="group-card-image" src="' + esc(group.image || "") + '" alt="' + esc(group.name || "") + '">' +
|
||
(usesNahida ? '<img class="group-card-nahida" src="/uploads/nahida_collect.webp" alt="纳西妲采集">' : '') +
|
||
'<div class="group-card-name" title="' + esc(group.name || "") + '">' + esc(group.name || "未命名") + '</div>' +
|
||
'</div>';
|
||
}
|
||
|
||
function startGroupCarousel() {
|
||
const track = document.getElementById("group-carousel-track");
|
||
const viewport = document.getElementById("group-carousel-viewport");
|
||
const empty = document.getElementById("group-carousel-empty");
|
||
if (!track || !viewport || !empty) return;
|
||
track.classList.remove("smooth-scroll");
|
||
track.style.removeProperty("--group-scroll-distance");
|
||
track.style.removeProperty("--group-scroll-duration");
|
||
if (!availableGroups.length) {
|
||
track.innerHTML = "";
|
||
viewport.hidden = true;
|
||
empty.hidden = false;
|
||
return;
|
||
}
|
||
|
||
viewport.hidden = false;
|
||
empty.hidden = true;
|
||
const n = availableGroups.length;
|
||
if (n <= 2) {
|
||
track.innerHTML = availableGroups.map(groupCardHtml).join("");
|
||
return;
|
||
}
|
||
|
||
const rowCount = Math.ceil(n / 2);
|
||
// 复制一份用于无缝循环:第二份首卡与第一份首卡之间的真实距离即一个完整循环周期。
|
||
track.innerHTML = availableGroups.concat(availableGroups).map(groupCardHtml).join("");
|
||
|
||
// 用真实渲染距离计算循环周期,避免硬编码卡片高度/间距导致“播完跳回开头”。
|
||
const cards = track.querySelectorAll(".group-card");
|
||
let distance = 0;
|
||
if (cards.length > n) {
|
||
distance = cards[n].getBoundingClientRect().top - cards[0].getBoundingClientRect().top;
|
||
}
|
||
if (distance <= 0) {
|
||
distance = rowCount * 89;
|
||
}
|
||
const duration = Math.max(45, rowCount * 2.1);
|
||
track.style.setProperty("--group-scroll-distance", "-" + distance.toFixed(2) + "px");
|
||
track.style.setProperty("--group-scroll-duration", duration.toFixed(1) + "s");
|
||
requestAnimationFrame(() => track.classList.add("smooth-scroll"));
|
||
}
|
||
|
||
async function fetchAvailableGroups() {
|
||
try {
|
||
const r = await fetch("/api/groups?t=" + Date.now());
|
||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||
const data = await r.json();
|
||
const groups = Array.isArray(data) ? data : data.groups;
|
||
availableGroups = Array.isArray(groups) ? groups.filter(x => x && x.name && x.image) : [];
|
||
startGroupCarousel();
|
||
} catch (e) {
|
||
console.error("available-groups", e);
|
||
availableGroups = [];
|
||
startGroupCarousel();
|
||
}
|
||
}
|
||
|
||
function getNextStopPushTime(now, hhmm) {
|
||
const match = /^(\d{1,2}):(\d{2})$/.exec(String(hhmm || "").trim());
|
||
if (!match) return null;
|
||
const hour = Number(match[1]);
|
||
const minute = Number(match[2]);
|
||
if (!Number.isInteger(hour) || !Number.isInteger(minute) || hour < 0 || hour > 23 || minute < 0 || minute > 59) {
|
||
return null;
|
||
}
|
||
const target = new Date(now.getTime());
|
||
target.setHours(hour, minute, 0, 0);
|
||
if (target.getTime() <= now.getTime()) target.setDate(target.getDate() + 1);
|
||
return target;
|
||
}
|
||
|
||
function renderLiveEndCountdown(now = new Date()) {
|
||
const valueEl = document.getElementById("live-end-countdown");
|
||
const targetEl = document.getElementById("live-end-target");
|
||
const dayEl = document.getElementById("live-end-day");
|
||
if (!valueEl || !targetEl || !dayEl) return;
|
||
|
||
const configuredTime = String(
|
||
systemScheduleConfig.live_end_time || systemScheduleConfig.bilibili_stop_push_time || ""
|
||
).trim();
|
||
const startTime = String(
|
||
systemScheduleConfig.live_start_time || systemScheduleConfig.bilibili_push_time || ""
|
||
).trim();
|
||
const toMinutes = value => {
|
||
const match = /^(\d{1,2}):(\d{2})$/.exec(value);
|
||
if (!match) return null;
|
||
const hour = Number(match[1]), minute = Number(match[2]);
|
||
return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59 ? hour * 60 + minute : null;
|
||
};
|
||
const startMinutes = toMinutes(startTime);
|
||
const endMinutes = toMinutes(configuredTime);
|
||
const nowMinutes = now.getHours() * 60 + now.getMinutes();
|
||
const liveNow = startMinutes !== null && endMinutes !== null && startMinutes !== endMinutes && (
|
||
startMinutes < endMinutes
|
||
? nowMinutes >= startMinutes && nowMinutes < endMinutes
|
||
: nowMinutes >= startMinutes || nowMinutes < endMinutes
|
||
);
|
||
if (!liveNow) {
|
||
valueEl.textContent = "--:--:--";
|
||
valueEl.classList.add("inactive");
|
||
targetEl.textContent = "当前未开播";
|
||
dayEl.textContent = startTime && configuredTime ? (startTime + " - " + configuredTime) : "请设置直播时间";
|
||
return;
|
||
}
|
||
|
||
const target = getNextStopPushTime(now, configuredTime);
|
||
if (!target) {
|
||
valueEl.textContent = "--:--:--";
|
||
valueEl.classList.add("inactive");
|
||
targetEl.textContent = "关播时间配置无效";
|
||
dayEl.textContent = configuredTime || "未设置时间";
|
||
return;
|
||
}
|
||
|
||
const remainingSec = Math.max(0, Math.ceil((target.getTime() - now.getTime()) / 1000));
|
||
const hours = Math.floor(remainingSec / 3600);
|
||
const minutes = Math.floor((remainingSec % 3600) / 60);
|
||
const seconds = remainingSec % 60;
|
||
valueEl.textContent = String(hours).padStart(2, "0") + ":" + String(minutes).padStart(2, "0") + ":" + String(seconds).padStart(2, "0");
|
||
valueEl.classList.remove("inactive");
|
||
targetEl.textContent = "计划关播 " + String(target.getHours()).padStart(2, "0") + ":" + String(target.getMinutes()).padStart(2, "0");
|
||
dayEl.textContent = target.toDateString() === now.toDateString() ? "今天" : "次日";
|
||
}
|
||
|
||
async function fetchSystemScheduleConfig() {
|
||
try {
|
||
const r = await fetch("/api/system-schedule-config");
|
||
if (!r.ok) throw new Error("HTTP " + r.status);
|
||
systemScheduleConfig = await r.json() || {};
|
||
try {
|
||
localStorage.setItem("liveEndScheduleConfig", JSON.stringify(systemScheduleConfig));
|
||
} catch (_) {
|
||
}
|
||
renderLiveEndCountdown();
|
||
} catch (e) {
|
||
console.error("system-schedule-config", e);
|
||
// 直播姬浏览器源偶发无法请求API时,优先使用最近一次成功配置;首次加载则使用页面构建时的兜底值。
|
||
let cached = null;
|
||
try {
|
||
cached = JSON.parse(localStorage.getItem("liveEndScheduleConfig") || "null");
|
||
} catch (_) {
|
||
}
|
||
systemScheduleConfig = cached && typeof cached === "object"
|
||
? cached
|
||
: {...fallbackSystemScheduleConfig};
|
||
renderLiveEndCountdown();
|
||
const dayEl = document.getElementById("live-end-day");
|
||
if (dayEl) dayEl.textContent += " · 本地配置";
|
||
}
|
||
}
|
||
|
||
async function fetchInputState() {
|
||
try {
|
||
const r = await fetch("/api/input_state");
|
||
inputState = await r.json();
|
||
renderInputState();
|
||
} catch (e) {
|
||
console.error("input", e);
|
||
}
|
||
}
|
||
|
||
async function fetchPerformance() {
|
||
try {
|
||
const r = await fetch("/api/performance");
|
||
performanceState = await r.json();
|
||
renderPerformance();
|
||
} catch (e) {
|
||
console.error("performance", e);
|
||
}
|
||
}
|
||
|
||
async function fetchFrontendConfig() {
|
||
try {
|
||
const r = await fetch("/api/frontend-config");
|
||
const cfg = await r.json();
|
||
const root = document.documentElement;
|
||
if (cfg.background_image) root.style.setProperty("--custom-bg-image", "url('" + cfg.background_image + "')");
|
||
else root.style.setProperty("--custom-bg-image", "none");
|
||
root.style.setProperty("--custom-bg-opacity", String(cfg.background_opacity ?? 0.65));
|
||
root.style.setProperty("--custom-bg-blur", String(cfg.background_blur ?? 0) + "px");
|
||
const fit = cfg.background_fit === "fill" ? "100% 100%" : (cfg.background_fit || "cover");
|
||
root.style.setProperty("--custom-bg-size", fit);
|
||
// 主题预设:classic 为默认(无额外类),其它主题通过 body.theme-<id> 生效
|
||
const themeClasses = Array.from(document.body.classList).filter((c) => c.startsWith("theme-"));
|
||
themeClasses.forEach((c) => document.body.classList.remove(c));
|
||
const theme = cfg.theme || "classic";
|
||
if (theme && theme !== "classic") document.body.classList.add("theme-" + theme);
|
||
} catch (e) {
|
||
console.error("frontend-config", e);
|
||
}
|
||
}
|
||
|
||
function setActive(id, on) {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
el.classList.toggle("active", !!on);
|
||
}
|
||
|
||
function renderPerformance() {
|
||
const setPerf = (name, value) => {
|
||
const textEl = document.getElementById("perf-" + name);
|
||
const ringEl = document.getElementById("perf-" + name + "-ring");
|
||
const n = Number(value);
|
||
if (!Number.isFinite(n)) {
|
||
if (textEl) textEl.textContent = "--%";
|
||
if (ringEl) ringEl.style.setProperty("--pct", 0);
|
||
return;
|
||
}
|
||
const pct = Math.max(0, Math.min(100, n));
|
||
if (textEl) textEl.textContent = pct.toFixed(pct >= 10 ? 0 : 1) + "%";
|
||
if (ringEl) ringEl.style.setProperty("--pct", pct);
|
||
};
|
||
setPerf("cpu", performanceState.cpu);
|
||
setPerf("mem", performanceState.memory);
|
||
setPerf("gpu", performanceState.gpu);
|
||
}
|
||
|
||
function renderInputState() {
|
||
const keys = inputState.keys || {};
|
||
["q", "w", "e", "r", "a", "s", "d", "f", "space"].forEach(k => setActive("key-" + k, keys[k]));
|
||
const mouse = inputState.mouse || {};
|
||
setActive("mouse-left", mouse.left);
|
||
setActive("mouse-right", mouse.right);
|
||
const trail = document.getElementById("mouse-trail");
|
||
if (!trail) return;
|
||
const sx = Number(mouse.smooth_dx || 0);
|
||
const sy = Number(mouse.smooth_dy || 0);
|
||
const mag = Math.max(0, Math.min(1, Number(mouse.magnitude || 0)));
|
||
if (!mag || (mouse.direction || "idle") === "idle") {
|
||
trail.classList.remove("active");
|
||
trail.style.transform = "translate(0, 0) scale(0.55)";
|
||
} else {
|
||
const len = Math.min(34, Math.max(12, mag * 36));
|
||
const norm = Math.max(1, Math.hypot(sx, sy));
|
||
const x = sx / norm * len;
|
||
const y = sy / norm * len;
|
||
trail.classList.add("active");
|
||
trail.style.transform = "translate(" + x.toFixed(1) + "px, " + y.toFixed(1) + "px) scale(" + (0.72 + mag * 0.42).toFixed(2) + ")";
|
||
}
|
||
}
|
||
|
||
function renderMusic() {
|
||
const cur = musicState.current || {};
|
||
const coverEl = document.getElementById("music-cover");
|
||
const titleEl = document.getElementById("music-title");
|
||
const artistEl = document.getElementById("music-artist");
|
||
const noteEl = document.getElementById("music-note");
|
||
const listEl = document.getElementById("playlist");
|
||
|
||
const monitor = musicState.monitor || {};
|
||
const updatedAtMs = Number(monitor.updated_at || 0) * 1000;
|
||
const monitorStale = !updatedAtMs || Date.now() - updatedAtMs > 10000;
|
||
const monitorOffline = monitor.online === false || monitorStale;
|
||
|
||
if (monitorOffline) {
|
||
titleEl.textContent = "音乐源离线";
|
||
artistEl.textContent = "等待网易云 SMTC 状态";
|
||
coverEl.innerHTML = "♪";
|
||
lastCoverKey = "";
|
||
noteEl.textContent = "监听未运行或状态已过期";
|
||
} else if (!cur.title && !cur.artist) {
|
||
titleEl.textContent = "未配置";
|
||
artistEl.textContent = "等待数据源";
|
||
coverEl.innerHTML = "♪";
|
||
lastCoverKey = "";
|
||
noteEl.textContent = "等待音乐源";
|
||
} else {
|
||
titleEl.textContent = cur.title || "未知歌曲";
|
||
artistEl.textContent = cur.artist || "未知艺人";
|
||
const rawCover = cur.cover ? String(cur.cover) : "";
|
||
const coverKey = rawCover + "|" + (cur.cover_hash || "");
|
||
if (rawCover && coverKey !== lastCoverKey) {
|
||
// 预加载新图, 加载成功才替换 DOM, 避免闪烁
|
||
const sep = rawCover.includes("?") ? "&" : "?";
|
||
const imgSrc = rawCover + sep + "v=" + encodeURIComponent(cur.cover_hash || "stable");
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
coverEl.innerHTML = '<img src="' + esc(imgSrc) + '" alt="">';
|
||
lastCoverKey = coverKey;
|
||
};
|
||
img.onerror = () => { /* 加载失败保留旧图, 不清空 */
|
||
};
|
||
img.src = imgSrc;
|
||
} else if (!rawCover && !lastCoverKey) {
|
||
// 从未有封面才显示音符占位; 已有封面则保留旧图
|
||
coverEl.innerHTML = "♪";
|
||
}
|
||
noteEl.textContent = musicState.playing ? "正在播放" : "已暂停";
|
||
}
|
||
|
||
const playlist = musicState.requests || musicState.playlist || [];
|
||
if (playlist.length === 0) {
|
||
listEl.innerHTML = '<li class="empty">暂无点歌</li>';
|
||
} else {
|
||
listEl.innerHTML = playlist.map((song, i) => {
|
||
const title = song.name || song.title || "未知歌曲";
|
||
const durationSec = Math.max(0, Number(song.duration_sec || 0));
|
||
const duration = durationSec > 0
|
||
? Math.floor(durationSec / 60) + ":" + String(Math.floor(durationSec % 60)).padStart(2, "0")
|
||
: "--:--";
|
||
const by = song.uname ? ("点歌:" + song.uname) : (song.artist || "-");
|
||
return '<li><span class="title">#' + (i + 1) + ' ' + esc(title) + ' [' + esc(duration) + ']</span><span class="by">' + esc(by) + '</span></li>';
|
||
}).join("");
|
||
}
|
||
}
|
||
// ===== 礼物特效引擎 =====
|
||
// 档位: value(折算人民币) >=100→4, >=10→3, >0→2, 免费→1;礼物数量 num 放大粒子规模。
|
||
const giftFx = {
|
||
particles: [],
|
||
rafId: null,
|
||
lastTs: 0,
|
||
bannerTimer: null,
|
||
queue: [],
|
||
playing: false,
|
||
|
||
tierOf(g) {
|
||
const v = Number(g.value || 0);
|
||
if (v >= 100) return 4;
|
||
if (v >= 10) return 3;
|
||
if (v > 0) return 2;
|
||
return 1;
|
||
},
|
||
|
||
canvasSetup() {
|
||
const cv = document.getElementById("gift-canvas");
|
||
const layer = document.getElementById("gift-layer");
|
||
if (!cv || !layer) return null;
|
||
const rect = layer.getBoundingClientRect();
|
||
// 按 1920x1080 逻辑分辨率设置,CSS 已随 #canvas 缩放
|
||
cv.width = 1920;
|
||
cv.height = 1080;
|
||
return cv.getContext("2d");
|
||
},
|
||
|
||
spawnBurst(cx, cy, count, colors, power) {
|
||
for (let i = 0; i < count; i++) {
|
||
const angle = Math.random() * Math.PI * 2;
|
||
const speed = (2 + Math.random() * 6) * power;
|
||
this.particles.push({
|
||
x: cx, y: cy,
|
||
vx: Math.cos(angle) * speed,
|
||
vy: Math.sin(angle) * speed - 2 * power,
|
||
life: 1,
|
||
decay: 0.008 + Math.random() * 0.012,
|
||
size: 3 + Math.random() * 6 * power,
|
||
color: colors[Math.floor(Math.random() * colors.length)],
|
||
shape: Math.random() < 0.3 ? "star" : "circle",
|
||
spin: Math.random() * Math.PI * 2,
|
||
spinV: (Math.random() - 0.5) * 0.2,
|
||
});
|
||
}
|
||
this.startLoop();
|
||
},
|
||
|
||
startLoop() {
|
||
if (this.rafId) return;
|
||
const ctx = this.canvasSetup();
|
||
if (!ctx) return;
|
||
const step = () => {
|
||
ctx.clearRect(0, 0, 1920, 1080);
|
||
this.particles = this.particles.filter(p => p.life > 0);
|
||
for (const p of this.particles) {
|
||
p.x += p.vx;
|
||
p.y += p.vy;
|
||
p.vy += 0.12;
|
||
p.vx *= 0.985;
|
||
p.life -= p.decay;
|
||
p.spin += p.spinV;
|
||
ctx.save();
|
||
ctx.globalAlpha = Math.max(0, p.life);
|
||
ctx.fillStyle = p.color;
|
||
if (p.shape === "star") {
|
||
ctx.translate(p.x, p.y);
|
||
ctx.rotate(p.spin);
|
||
const s = p.size;
|
||
ctx.beginPath();
|
||
for (let k = 0; k < 10; k++) {
|
||
const r = k % 2 === 0 ? s : s * 0.45;
|
||
const a = (Math.PI / 5) * k;
|
||
ctx.lineTo(Math.cos(a) * r, Math.sin(a) * r);
|
||
}
|
||
ctx.closePath();
|
||
ctx.fill();
|
||
} else {
|
||
ctx.beginPath();
|
||
ctx.arc(p.x, p.y, p.size / 2, 0, Math.PI * 2);
|
||
ctx.fill();
|
||
}
|
||
ctx.restore();
|
||
}
|
||
if (this.particles.length === 0) {
|
||
this.rafId = null;
|
||
ctx.clearRect(0, 0, 1920, 1080);
|
||
return;
|
||
}
|
||
this.rafId = requestAnimationFrame(step);
|
||
};
|
||
this.rafId = requestAnimationFrame(step);
|
||
},
|
||
|
||
showBanner(g, tier) {
|
||
const banner = document.getElementById("gift-banner");
|
||
if (!banner) return;
|
||
banner.className = "gift-banner tier-" + tier;
|
||
banner.style.display = "flex";
|
||
banner.querySelector(".gb-name").textContent = shortName(g.uname || "热心观众", 10);
|
||
banner.querySelector(".gb-gift").textContent =
|
||
"送出 " + (g.gift_name || "礼物") + (g.num > 1 ? " ×" + g.num : "");
|
||
requestAnimationFrame(() => banner.classList.add("show"));
|
||
clearTimeout(this.bannerTimer);
|
||
const holdMs = 1800 + tier * 600;
|
||
this.bannerTimer = setTimeout(() => {
|
||
banner.classList.remove("show");
|
||
banner.classList.add("hide");
|
||
setTimeout(() => { banner.style.display = "none"; banner.classList.remove("hide"); }, 450);
|
||
}, holdMs);
|
||
},
|
||
|
||
flash(tier) {
|
||
if (tier < 3) return;
|
||
const el = document.getElementById("gift-flash");
|
||
if (!el) return;
|
||
el.classList.add("on");
|
||
setTimeout(() => el.classList.remove("on"), tier >= 4 ? 500 : 300);
|
||
},
|
||
|
||
play(g) {
|
||
const tier = this.tierOf(g);
|
||
const num = Math.min(Number(g.num || 1), 99);
|
||
// 基础粒子数按档位递增,数量 num 再放大(对数压缩避免刷屏)
|
||
const base = [0, 20, 45, 90, 160][tier];
|
||
const count = Math.round(base * (1 + Math.log10(num)));
|
||
const power = 0.7 + tier * 0.25;
|
||
const palette = {
|
||
1: ["#9fe1cb", "#eafff2", "#5dcaa5"],
|
||
2: ["#ffd66e", "#fff3c4", "#ef9f27"],
|
||
3: ["#ff82a0", "#ffe9ec", "#ed93b1", "#ffd66e"],
|
||
4: ["#dcA0ff", "#ffffff", "#afa9ec", "#ffd66e", "#ff82a0"],
|
||
}[tier];
|
||
this.showBanner(g, tier);
|
||
this.flash(tier);
|
||
// 中心主爆点
|
||
this.spawnBurst(960, 420, count, palette, power);
|
||
// 高档位追加侧翼爆点,礼物越大场面越足
|
||
if (tier >= 2) this.spawnBurst(560, 560, Math.round(count * 0.4), palette, power * 0.8);
|
||
if (tier >= 3) this.spawnBurst(1360, 560, Math.round(count * 0.4), palette, power * 0.8);
|
||
if (tier >= 4) {
|
||
setTimeout(() => this.spawnBurst(960, 300, Math.round(count * 0.6), palette, power), 350);
|
||
setTimeout(() => this.spawnBurst(700, 480, Math.round(count * 0.4), palette, power * 0.9), 700);
|
||
setTimeout(() => this.spawnBurst(1220, 480, Math.round(count * 0.4), palette, power * 0.9), 700);
|
||
}
|
||
},
|
||
|
||
enqueue(g) {
|
||
this.queue.push(g);
|
||
this.pump();
|
||
},
|
||
|
||
pump() {
|
||
if (this.playing || this.queue.length === 0) return;
|
||
this.playing = true;
|
||
const g = this.queue.shift();
|
||
this.play(g);
|
||
const tier = this.tierOf(g);
|
||
setTimeout(() => {
|
||
this.playing = false;
|
||
this.pump();
|
||
}, 1200 + tier * 400);
|
||
},
|
||
|
||
// 每轮 state 轮询调用:播放 ts 晚于上次记录的所有礼物
|
||
sync(gifts) {
|
||
if (!Array.isArray(gifts) || gifts.length === 0) return;
|
||
const fresh = gifts.filter(g => Number(g.ts || 0) > this.lastTs);
|
||
if (fresh.length === 0) return;
|
||
this.lastTs = Math.max(...gifts.map(g => Number(g.ts || 0)));
|
||
for (const g of fresh) this.enqueue(g);
|
||
},
|
||
};
|
||
|
||
// 首次加载时把已有礼物的最大 ts 设为基线,避免刷新页面重放旧礼物
|
||
function initGiftFxBaseline(gifts) {
|
||
if (giftFx.lastTs > 0 || !Array.isArray(gifts) || gifts.length === 0) return;
|
||
giftFx.lastTs = Math.max(...gifts.map(g => Number(g.ts || 0)));
|
||
}
|
||
|
||
|
||
function scaleCanvas() {
|
||
const canvas = document.getElementById("canvas");
|
||
const scale = Math.min(window.innerWidth / 1920, window.innerHeight / 1080);
|
||
canvas.style.transform = "scale(" + scale + ")";
|
||
}
|
||
|
||
startTutorialCarousel();
|
||
fetchFrontendConfig();
|
||
fetchState();
|
||
fetchLog();
|
||
fetchDanmu();
|
||
fetchMusic();
|
||
fetchAvailableGroups();
|
||
fetchSystemScheduleConfig();
|
||
renderLiveEndCountdown();
|
||
fetchInputState();
|
||
fetchPerformance();
|
||
setInterval(fetchState, 2000);
|
||
setInterval(fetchLog, 1500);
|
||
setInterval(fetchDanmu, 1500);
|
||
setInterval(fetchMusic, 5000);
|
||
setInterval(renderLiveEndCountdown, 1000);
|
||
setInterval(fetchSystemScheduleConfig, 60000);
|
||
setInterval(fetchInputState, 33);
|
||
setInterval(fetchPerformance, 2000);
|
||
scaleCanvas();
|
||
const params = new URLSearchParams(location.search);
|
||
if (params.get('preview') === '1') document.body.classList.add('preview');
|
||
if (params.get('frame') === '0') document.body.classList.add('frame-off');
|
||
if (params.get('glass') === '1') document.body.classList.add('glass-on');
|
||
scaleCanvas();
|
||
window.addEventListener("resize", scaleCanvas);
|
||
</script>
|
||
</body>
|
||
</html>
|