Compare commits
2 Commits
72253b1391
...
716bc2b106
| Author | SHA1 | Date | |
|---|---|---|---|
| 716bc2b106 | |||
| ceba41fb08 |
@@ -23,7 +23,9 @@ body,
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
background: radial-gradient(circle at 12% 12%, #254935 0%, #11251c 45%, #0a1411 100%);
|
background:
|
||||||
|
radial-gradient(circle at top, rgba(219, 171, 91, 0.16), transparent 22%),
|
||||||
|
linear-gradient(180deg, #442621 0%, #24110e 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
#app {
|
||||||
@@ -54,10 +56,12 @@ p {
|
|||||||
width: min(440px, 100%);
|
width: min(440px, 100%);
|
||||||
padding: 28px 24px;
|
padding: 28px 24px;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
border: 1px solid rgba(246, 212, 139, 0.18);
|
||||||
background: rgba(8, 27, 20, 0.82);
|
background:
|
||||||
backdrop-filter: blur(8px);
|
linear-gradient(180deg, rgba(62, 33, 26, 0.96), rgba(26, 14, 11, 0.96)),
|
||||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
|
radial-gradient(circle at top, rgba(255, 214, 134, 0.08), transparent 40%);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.42);
|
||||||
}
|
}
|
||||||
|
|
||||||
.auth-card h1 {
|
.auth-card h1 {
|
||||||
@@ -204,8 +208,10 @@ button:disabled {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
border: 1px solid rgba(244, 210, 140, 0.14);
|
||||||
background: linear-gradient(130deg, rgba(22, 57, 43, 0.9), rgba(10, 30, 22, 0.92));
|
background:
|
||||||
|
linear-gradient(180deg, rgba(62, 33, 26, 0.94), rgba(30, 15, 12, 0.92)),
|
||||||
|
radial-gradient(circle at top, rgba(255, 214, 134, 0.08), transparent 42%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hall-topbar {
|
.hall-topbar {
|
||||||
@@ -289,8 +295,8 @@ button:disabled {
|
|||||||
.panel {
|
.panel {
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(244, 210, 140, 0.12);
|
||||||
background: rgba(10, 30, 22, 0.85);
|
background: rgba(40, 21, 17, 0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel h2 {
|
.panel h2 {
|
||||||
@@ -762,16 +768,25 @@ button:disabled {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-width: 148px;
|
min-width: 154px;
|
||||||
padding: 8px 12px;
|
padding: 9px 12px;
|
||||||
border-radius: 14px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(244, 222, 163, 0.24);
|
border: 1px solid rgba(248, 226, 173, 0.24);
|
||||||
background: rgba(8, 27, 20, 0.72);
|
background:
|
||||||
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.2);
|
linear-gradient(180deg, rgba(43, 52, 73, 0.84), rgba(17, 22, 34, 0.82)),
|
||||||
|
radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 40%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.08),
|
||||||
|
0 12px 28px rgba(0, 0, 0, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-panel {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-badge.seat-top {
|
.player-badge.seat-top {
|
||||||
top: 76px;
|
top: 28px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
@@ -783,7 +798,7 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.player-badge.seat-bottom {
|
.player-badge.seat-bottom {
|
||||||
bottom: 90px;
|
bottom: 136px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
@@ -805,22 +820,29 @@ button:disabled {
|
|||||||
.avatar-card {
|
.avatar-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
width: 42px;
|
width: 48px;
|
||||||
height: 42px;
|
height: 48px;
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(145deg, #ecd995, #d3b767);
|
border: 1px solid rgba(255, 248, 215, 0.32);
|
||||||
color: #1c2d23;
|
background:
|
||||||
|
linear-gradient(145deg, #b3e79c, #4eaf4a 46%, #2f7e28 100%);
|
||||||
|
color: #f7fff7;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 2px 4px rgba(255, 255, 255, 0.18),
|
||||||
|
0 6px 14px rgba(0, 0, 0, 0.22);
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-meta p {
|
.player-meta p {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
color: #eef5ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.player-meta strong {
|
.player-meta strong {
|
||||||
font-size: 13px;
|
font-size: 15px;
|
||||||
color: #f7e4b0;
|
color: #ffd85c;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 216, 92, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.dealer-mark,
|
.dealer-mark,
|
||||||
@@ -828,22 +850,39 @@ button:disabled {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-width: 24px;
|
min-width: 28px;
|
||||||
height: 24px;
|
height: 28px;
|
||||||
padding: 0 6px;
|
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dealer-mark {
|
.dealer-mark {
|
||||||
background: rgba(236, 188, 84, 0.88);
|
position: absolute;
|
||||||
color: #1c2d23;
|
right: -8px;
|
||||||
|
bottom: -6px;
|
||||||
|
background: linear-gradient(180deg, #ffe38a 0%, #f1b92e 100%);
|
||||||
|
color: #5f3200;
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.missing-mark {
|
.missing-mark {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
background: rgba(255, 255, 255, 0.08);
|
width: 34px;
|
||||||
color: #d6eadf;
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
background: linear-gradient(180deg, rgba(114, 219, 149, 0.2) 0%, rgba(21, 148, 88, 0.34) 100%);
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-mark img {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.missing-mark span {
|
||||||
|
color: #effff5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wall {
|
.wall {
|
||||||
|
|||||||
712
src/assets/styles/room.css
Normal file
712
src/assets/styles/room.css
Normal file
@@ -0,0 +1,712 @@
|
|||||||
|
.picture-scene {
|
||||||
|
min-height: 100vh;
|
||||||
|
min-height: 100dvh;
|
||||||
|
padding: 18px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top, rgba(116, 58, 41, 0.28), transparent 20%),
|
||||||
|
linear-gradient(180deg, #3f2119 0%, #27140f 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picture-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
|
gap: 18px;
|
||||||
|
align-items: stretch;
|
||||||
|
min-height: calc(100vh - 36px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-stage {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
align-content: start;
|
||||||
|
width: 100%;
|
||||||
|
min-height: calc(100vh - 36px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-desk,
|
||||||
|
.table-felt {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: calc(100dvh - 72px);
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-desk {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
display: block;
|
||||||
|
margin-top: 18px;
|
||||||
|
border-radius: 26px;
|
||||||
|
object-fit: cover;
|
||||||
|
box-shadow: 0 24px 44px rgba(0, 0, 0, 0.34);
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-felt {
|
||||||
|
grid-area: 1 / 1;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 18px;
|
||||||
|
border-radius: 26px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-surface {
|
||||||
|
position: absolute;
|
||||||
|
inset: 18px;
|
||||||
|
border-radius: 20px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at center, rgba(255, 255, 255, 0.04), transparent 34%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(0, 0, 0, 0.04) 52%, rgba(0, 0, 0, 0.08));
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-surface::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 20px;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 50% 50%, rgba(255, 255, 255, 0.03), transparent 40%),
|
||||||
|
linear-gradient(180deg, rgba(255, 255, 255, 0.01), rgba(0, 0, 0, 0.06));
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-outline {
|
||||||
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-outline.outer {
|
||||||
|
inset: 22px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(24, 63, 35, 0.82);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2px rgba(177, 112, 69, 0.3),
|
||||||
|
inset 0 0 0 6px rgba(8, 36, 18, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-outline.mid {
|
||||||
|
inset: 74px 92px 122px;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 2px solid rgba(180, 224, 187, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-outline.diamond {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 280px;
|
||||||
|
height: 280px;
|
||||||
|
border: 2px solid rgba(143, 199, 155, 0.08);
|
||||||
|
transform: translate(-50%, -50%) rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-left-tools {
|
||||||
|
position: absolute;
|
||||||
|
top: 28px;
|
||||||
|
left: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metal-circle {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #d7f0ff;
|
||||||
|
font-size: 26px;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(156, 171, 191, 0.82), rgba(79, 94, 114, 0.86)),
|
||||||
|
radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.28), transparent 42%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.22),
|
||||||
|
0 8px 16px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-counter {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #fff4cf;
|
||||||
|
background: rgba(13, 15, 20, 0.78);
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter-light {
|
||||||
|
width: 16px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: linear-gradient(180deg, #54e061 0%, #179928 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.36),
|
||||||
|
0 0 8px rgba(84, 224, 97, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-right-clock {
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
right: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #f1f6ff;
|
||||||
|
background: rgba(14, 16, 22, 0.46);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.signal-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-watermark {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 38%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(7, 42, 19, 0.18);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-watermark strong {
|
||||||
|
display: block;
|
||||||
|
font-size: clamp(30px, 4vw, 52px);
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-watermark span,
|
||||||
|
.scene-watermark small {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: rgba(7, 42, 19, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
filter: drop-shadow(0 8px 8px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall img {
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-top,
|
||||||
|
.wall-bottom {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left,
|
||||||
|
.wall-right {
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-top {
|
||||||
|
top: 54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-top img,
|
||||||
|
.wall-bottom img {
|
||||||
|
width: 24px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-right {
|
||||||
|
right: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left {
|
||||||
|
left: 110px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left img,
|
||||||
|
.wall-right img {
|
||||||
|
width: 36px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-bottom {
|
||||||
|
bottom: 126px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-desk {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 46px);
|
||||||
|
grid-template-areas:
|
||||||
|
'north west'
|
||||||
|
'count count'
|
||||||
|
'south east';
|
||||||
|
gap: 8px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 2px solid rgba(69, 55, 38, 0.72);
|
||||||
|
background:
|
||||||
|
linear-gradient(135deg, rgba(107, 51, 41, 0.94), rgba(33, 35, 32, 0.96) 50%, rgba(26, 69, 36, 0.92)),
|
||||||
|
radial-gradient(circle at center, rgba(255, 255, 255, 0.06), transparent 52%);
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.center-desk strong {
|
||||||
|
grid-area: count;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 32px;
|
||||||
|
color: #3ec37f;
|
||||||
|
letter-spacing: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.18);
|
||||||
|
color: rgba(255, 255, 255, 0.68);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind.north {
|
||||||
|
grid-area: north;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind.west {
|
||||||
|
grid-area: west;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind.south {
|
||||||
|
grid-area: south;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wind.east {
|
||||||
|
grid-area: east;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-status {
|
||||||
|
position: absolute;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #ffcf4d;
|
||||||
|
font-size: clamp(24px, 2.6vw, 42px);
|
||||||
|
font-weight: 800;
|
||||||
|
text-shadow:
|
||||||
|
0 0 12px rgba(255, 195, 0, 0.32),
|
||||||
|
0 3px 0 rgba(130, 74, 0, 0.4);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-status img {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
object-fit: contain;
|
||||||
|
filter: drop-shadow(0 6px 10px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-status.top {
|
||||||
|
top: 48px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-status.left {
|
||||||
|
left: 24%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-status.right {
|
||||||
|
right: 24%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-banner {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 58%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
min-width: 380px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
border-radius: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: #eef7ef;
|
||||||
|
background: rgba(13, 31, 17, 0.24);
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-banner span {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-banner strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-control-panel {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 8px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: min(100% - 120px, 1180px);
|
||||||
|
padding: 8px 14px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-copy {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(239, 247, 237, 0.84);
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-copy p {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-copy small {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-orbs {
|
||||||
|
position: absolute;
|
||||||
|
right: 38px;
|
||||||
|
top: -66px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-button {
|
||||||
|
width: 76px;
|
||||||
|
height: 76px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 3px 8px rgba(255, 255, 255, 0.18),
|
||||||
|
0 10px 20px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-button.theme-gold {
|
||||||
|
color: #8a4e00;
|
||||||
|
background: radial-gradient(circle at 35% 28%, #fff7bf 0%, #ffd85d 42%, #d89a19 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-button.theme-jade {
|
||||||
|
color: #efffff;
|
||||||
|
background: radial-gradient(circle at 35% 28%, #c4fff2 0%, #3ad8b4 42%, #00876e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-button.theme-blue {
|
||||||
|
color: #effff2;
|
||||||
|
background: radial-gradient(circle at 35% 28%, #bff2c8 0%, #6bc77c 42%, #2e7a43 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.player-hand {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-chip {
|
||||||
|
min-width: 90px;
|
||||||
|
height: 126px;
|
||||||
|
border: 1px solid rgba(70, 80, 92, 0.18);
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #14181d;
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 700;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, #ffffff 0%, #f8fafc 68%, #dfe6ed 100%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 -4px 0 #1ea328,
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||||
|
0 6px 12px rgba(0, 0, 0, 0.18);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-chip.selected {
|
||||||
|
transform: translateY(-18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-hand {
|
||||||
|
text-align: center;
|
||||||
|
color: rgba(237, 244, 253, 0.82);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-side-buttons {
|
||||||
|
position: absolute;
|
||||||
|
right: 18px;
|
||||||
|
top: 42%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-round {
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: #edf8ef;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(126, 140, 122, 0.82), rgba(59, 72, 57, 0.86)),
|
||||||
|
radial-gradient(circle at 35% 28%, rgba(255, 255, 255, 0.22), transparent 40%);
|
||||||
|
box-shadow:
|
||||||
|
inset 0 1px 0 rgba(255, 255, 255, 0.2),
|
||||||
|
0 8px 14px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-round.gold {
|
||||||
|
color: #7a4600;
|
||||||
|
background: radial-gradient(circle at 35% 28%, #fff6c2 0%, #ffe16c 42%, #e3aa23 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: calc(100vh - 36px);
|
||||||
|
min-height: calc(100vh - 36px);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 18px;
|
||||||
|
border: 1px solid rgba(255, 226, 175, 0.12);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(45, 24, 18, 0.94), rgba(26, 14, 11, 0.96)),
|
||||||
|
radial-gradient(circle at top, rgba(255, 219, 154, 0.06), transparent 40%);
|
||||||
|
box-shadow: 0 16px 28px rgba(0, 0, 0, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #ffe2a0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-head small {
|
||||||
|
color: rgba(248, 233, 199, 0.68);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-btn {
|
||||||
|
min-width: 76px;
|
||||||
|
height: 38px;
|
||||||
|
border: 1px solid rgba(255, 223, 164, 0.16);
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #ffe9b7;
|
||||||
|
background: rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-stats {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-stat {
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-stat span {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(244, 233, 208, 0.62);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-stat strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
color: #fff0c2;
|
||||||
|
font-size: 15px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-error {
|
||||||
|
margin-top: 14px;
|
||||||
|
color: #ffc1c1;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-log {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(9, 12, 19, 0.34);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-empty,
|
||||||
|
.sidebar-line {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #e6eef8;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-line + .sidebar-line {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding-top: 8px;
|
||||||
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.picture-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-desk,
|
||||||
|
.table-felt {
|
||||||
|
width: min(100%, calc((100dvh - 290px) * 16 / 9));
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-sidebar {
|
||||||
|
height: auto;
|
||||||
|
min-height: 240px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.picture-scene {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-desk,
|
||||||
|
.table-felt {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-right {
|
||||||
|
right: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left {
|
||||||
|
left: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-outline.mid {
|
||||||
|
inset: 70px 72px 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-chip {
|
||||||
|
min-width: 70px;
|
||||||
|
height: 102px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-orbs {
|
||||||
|
right: 18px;
|
||||||
|
top: -54px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orb-button {
|
||||||
|
width: 62px;
|
||||||
|
height: 62px;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.table-desk,
|
||||||
|
.table-felt {
|
||||||
|
aspect-ratio: 9 / 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-outline.mid {
|
||||||
|
inset: 92px 34px 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner-outline.diamond {
|
||||||
|
width: 180px;
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-top,
|
||||||
|
.wall-bottom {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-left {
|
||||||
|
left: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wall-right {
|
||||||
|
right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-status.left,
|
||||||
|
.floating-status.right {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.claim-banner {
|
||||||
|
min-width: 0;
|
||||||
|
width: calc(100% - 40px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-control-panel {
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-orbs {
|
||||||
|
position: static;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tile-chip {
|
||||||
|
min-width: 48px;
|
||||||
|
height: 76px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-side-buttons {
|
||||||
|
right: 10px;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.side-round {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,27 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import wanIcon from '../../assets/images/flowerClolor/wan.png'
|
||||||
|
import tongIcon from '../../assets/images/flowerClolor/tong.png'
|
||||||
|
import tiaoIcon from '../../assets/images/flowerClolor/tiao.png'
|
||||||
import type { SeatPlayerCardModel } from './seat-player-card'
|
import type { SeatPlayerCardModel } from './seat-player-card'
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
seatClass: string
|
seatClass: string
|
||||||
player: SeatPlayerCardModel
|
player: SeatPlayerCardModel
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const missingSuitIcon = computed(() => {
|
||||||
|
if (props.player.missingSuitLabel === '万') {
|
||||||
|
return wanIcon
|
||||||
|
}
|
||||||
|
if (props.player.missingSuitLabel === '筒') {
|
||||||
|
return tongIcon
|
||||||
|
}
|
||||||
|
if (props.player.missingSuitLabel === '条') {
|
||||||
|
return tiaoIcon
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -12,12 +29,19 @@ defineProps<{
|
|||||||
class="player-badge"
|
class="player-badge"
|
||||||
:class="[seatClass, { 'is-turn': player.isTurn, offline: !player.isOnline }]"
|
:class="[seatClass, { 'is-turn': player.isTurn, offline: !player.isOnline }]"
|
||||||
>
|
>
|
||||||
|
<div class="avatar-panel">
|
||||||
<div class="avatar-card">{{ player.avatar }}</div>
|
<div class="avatar-card">{{ player.avatar }}</div>
|
||||||
|
<span v-if="player.dealer" class="dealer-mark">庄</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="player-meta">
|
<div class="player-meta">
|
||||||
<p>{{ player.name }}</p>
|
<p>{{ player.name }}</p>
|
||||||
<strong>{{ player.money }}</strong>
|
<strong>{{ player.money }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<span v-if="player.dealer" class="dealer-mark">庄</span>
|
|
||||||
<span class="missing-mark">{{ player.missingSuitLabel }}</span>
|
<div class="missing-mark">
|
||||||
|
<img v-if="missingSuitIcon" :src="missingSuitIcon" alt="" />
|
||||||
|
<span v-else>{{ player.missingSuitLabel }}</span>
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ function humanizeSuit(value: string): string {
|
|||||||
W: '万',
|
W: '万',
|
||||||
B: '筒',
|
B: '筒',
|
||||||
T: '条',
|
T: '条',
|
||||||
wan: '万',
|
|
||||||
tong: '筒',
|
|
||||||
tiao: '条',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return suitMap[value] ?? value
|
return suitMap[value] ?? value
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {computed, onBeforeUnmount, onMounted, ref} from 'vue'
|
import {computed, onBeforeUnmount, onMounted, ref} from 'vue'
|
||||||
import {useRoute, useRouter} from 'vue-router'
|
import {useRoute, useRouter} from 'vue-router'
|
||||||
import deskImage from '../assets/images/desk/desk_01.png'
|
import deskImage from '../assets/images/desk/desk_01.png'
|
||||||
|
import wanIcon from '../assets/images/flowerClolor/wan.png'
|
||||||
|
import tongIcon from '../assets/images/flowerClolor/tong.png'
|
||||||
|
import tiaoIcon from '../assets/images/flowerClolor/tiao.png'
|
||||||
|
import '../assets/styles/room.css'
|
||||||
import topBackImage from '../assets/images/tiles/top/tbgs_2.png'
|
import topBackImage from '../assets/images/tiles/top/tbgs_2.png'
|
||||||
import rightBackImage from '../assets/images/tiles/right/tbgs_1.png'
|
import rightBackImage from '../assets/images/tiles/right/tbgs_1.png'
|
||||||
import bottomBackImage from '../assets/images/tiles/bottom/tdbgs_4.png'
|
import bottomBackImage from '../assets/images/tiles/bottom/tdbgs_4.png'
|
||||||
@@ -11,7 +15,7 @@ import RightPlayerCard from '../components/game/RightPlayerCard.vue'
|
|||||||
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
|
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
|
||||||
import LeftPlayerCard from '../components/game/LeftPlayerCard.vue'
|
import LeftPlayerCard from '../components/game/LeftPlayerCard.vue'
|
||||||
import type {SeatPlayerCardModel} from '../components/game/seat-player-card'
|
import type {SeatPlayerCardModel} from '../components/game/seat-player-card'
|
||||||
import { useChengduGameRoom, type SeatKey } from '../features/chengdu-game/useChengduGameRoom'
|
import {type SeatKey, useChengduGameRoom} from '../features/chengdu-game/useChengduGameRoom'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -86,10 +90,6 @@ const formattedClock = computed(() => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const statusRibbon = computed(() => {
|
|
||||||
return roomState.value.game?.rule?.name || currentPhaseText.value
|
|
||||||
})
|
|
||||||
|
|
||||||
const wallBacks = computed<Record<SeatKey, string[]>>(() => {
|
const wallBacks = computed<Record<SeatKey, string[]>>(() => {
|
||||||
const wallSize = roomState.value.game?.state?.wall.length ?? 0
|
const wallSize = roomState.value.game?.state?.wall.length ?? 0
|
||||||
const perSide = Math.max(6, Math.ceil((wallSize || 48) / 4 / 2))
|
const perSide = Math.max(6, Math.ceil((wallSize || 48) / 4 / 2))
|
||||||
@@ -138,42 +138,24 @@ const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
|||||||
dealer: seat.player.index === dealerIndex,
|
dealer: seat.player.index === dealerIndex,
|
||||||
isTurn: seat.isTurn,
|
isTurn: seat.isTurn,
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
missingSuitLabel: defaultMissingSuitLabel,
|
missingSuitLabel: missingSuitLabel(seat.player.missingSuit),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
const seatMarkers = computed(() => {
|
|
||||||
const seatTitleMap: Record<SeatKey, string> = {
|
|
||||||
top: '上家',
|
|
||||||
right: '右家',
|
|
||||||
bottom: '本家',
|
|
||||||
left: '左家',
|
|
||||||
}
|
|
||||||
|
|
||||||
return seatViews.value.map((seat) => ({
|
|
||||||
key: seat.key,
|
|
||||||
occupied: Boolean(seat.player),
|
|
||||||
isSelf: seat.isSelf,
|
|
||||||
isTurn: seat.isTurn,
|
|
||||||
label: seat.player ? seatTitleMap[seat.key] : '空位',
|
|
||||||
subLabel: seat.player ? `座位 ${seat.player.index}` : '',
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
const centerTimer = computed(() => {
|
const centerTimer = computed(() => {
|
||||||
const wallLeft = roomState.value.game?.state?.wall.length
|
const wallLeft = roomState.value.game?.state?.wall.length
|
||||||
if (typeof wallLeft === 'number' && Number.isFinite(wallLeft)) {
|
if (typeof wallLeft === 'number' && Number.isFinite(wallLeft)) {
|
||||||
return `余牌 ${wallLeft}`
|
return String(wallLeft).padStart(2, '0')
|
||||||
}
|
}
|
||||||
|
|
||||||
return roomState.value.playerCount > 0
|
return String(roomState.value.playerCount).padStart(2, '0')
|
||||||
? `${roomState.value.playerCount}/${roomState.value.maxPlayers} 人`
|
|
||||||
: '等待中'
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const selectedTileText = computed(() => selectedTile.value ?? '未选择')
|
||||||
|
|
||||||
const pendingClaimText = computed(() => {
|
const pendingClaimText = computed(() => {
|
||||||
const claim = roomState.value.game?.state?.pendingClaim
|
const claim = roomState.value.game?.state?.pendingClaim
|
||||||
if (!claim) {
|
if (!claim) {
|
||||||
@@ -183,12 +165,24 @@ const pendingClaimText = computed(() => {
|
|||||||
try {
|
try {
|
||||||
return JSON.stringify(claim)
|
return JSON.stringify(claim)
|
||||||
} catch {
|
} catch {
|
||||||
return '存在响应窗口'
|
return '有待响应动作'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const selectedTileText = computed(() => {
|
const rightMessages = computed(() => wsMessages.value.slice(-16).reverse())
|
||||||
return selectedTile.value ?? '未选择'
|
|
||||||
|
const floatingMissingSuit = computed(() => {
|
||||||
|
const suitMap: Record<string, string> = {
|
||||||
|
万: wanIcon,
|
||||||
|
筒: tongIcon,
|
||||||
|
条: tiaoIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
top: suitMap[seatDecor.value.top.missingSuitLabel] ?? '',
|
||||||
|
left: suitMap[seatDecor.value.left.missingSuitLabel] ?? '',
|
||||||
|
right: suitMap[seatDecor.value.right.missingSuitLabel] ?? '',
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function missingSuitLabel(value: string | null | undefined): string {
|
function missingSuitLabel(value: string | null | undefined): string {
|
||||||
@@ -216,6 +210,16 @@ function getBackImage(seat: SeatKey): string {
|
|||||||
return imageMap[seat]
|
return imageMap[seat]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function actionTheme(type: string): 'gold' | 'jade' | 'blue' {
|
||||||
|
if (type === 'hu' || type === 'gang') {
|
||||||
|
return 'gold'
|
||||||
|
}
|
||||||
|
if (type === 'pass') {
|
||||||
|
return 'jade'
|
||||||
|
}
|
||||||
|
return 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
clockTimer = window.setInterval(() => {
|
clockTimer = window.setInterval(() => {
|
||||||
now.value = Date.now()
|
now.value = Date.now()
|
||||||
@@ -231,121 +235,37 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="hall-page game-page">
|
<section class="picture-scene">
|
||||||
<header class="hall-header game-header">
|
<div class="picture-layout">
|
||||||
<div class="topbar-left">
|
<section class="table-stage">
|
||||||
<button class="ghost-btn topbar-back-btn" type="button" :disabled="leaveRoomPending" @click="backHall">
|
<img class="table-desk" :src="deskImage" alt=""/>
|
||||||
{{ leaveRoomPending ? '退出中...' : '返回大厅' }}
|
|
||||||
</button>
|
<div class="table-felt">
|
||||||
<div class="topbar-room-meta">
|
<div class="table-surface"></div>
|
||||||
<p class="eyebrow">成都麻将对局</p>
|
<div class="inner-outline outer"></div>
|
||||||
<p class="topbar-room-name">{{ roomState.name || roomName || '未命名房间' }}</p>
|
<div class="inner-outline mid"></div>
|
||||||
<p v-if="loggedInUserName" class="sub-title">玩家:{{ loggedInUserName }}</p>
|
<div class="inner-outline diamond"></div>
|
||||||
|
|
||||||
|
<div class="top-left-tools">
|
||||||
|
<button class="metal-circle" type="button" :disabled="leaveRoomPending" @click="backHall">☰</button>
|
||||||
|
<div class="left-counter">
|
||||||
|
<span class="counter-light"></span>
|
||||||
|
<strong>{{ roomState.game?.state?.wall.length ?? 48 }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="topbar-center">
|
<div class="top-right-clock">
|
||||||
<div class="title-stack">
|
<div class="signal-chip">
|
||||||
<p class="game-title">成都麻将实战桌</p>
|
|
||||||
<p class="game-subtitle">{{ roomStatusText }} · {{ currentPhaseText }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="topbar-right">
|
|
||||||
<div class="status-chip net-chip">
|
|
||||||
<span class="wifi-dot" :class="`is-${wsStatus}`"></span>
|
<span class="wifi-dot" :class="`is-${wsStatus}`"></span>
|
||||||
<strong>{{ networkLabel }}</strong>
|
<strong>{{ networkLabel }}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-chip clock-chip">{{ formattedClock }}</div>
|
<span>{{ formattedClock }}</span>
|
||||||
<button class="header-btn ghost-btn" type="button" @click="connectWs">重连</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section class="table-panel game-table-panel">
|
|
||||||
<div class="room-brief">
|
|
||||||
<span class="room-brief-title">当前房间</span>
|
|
||||||
<span class="room-brief-item">
|
|
||||||
<em>房间名:</em>
|
|
||||||
<strong>{{ roomState.name || roomName || '未命名房间' }}</strong>
|
|
||||||
</span>
|
|
||||||
<span class="room-brief-item room-brief-id">
|
|
||||||
<em>room_id:</em>
|
|
||||||
<strong>{{ roomId || '未选择房间' }}</strong>
|
|
||||||
</span>
|
|
||||||
<span class="room-brief-item">
|
|
||||||
<em>状态:</em>
|
|
||||||
<strong>{{ roomStatusText }}</strong>
|
|
||||||
</span>
|
|
||||||
<span class="room-brief-item">
|
|
||||||
<em>人数:</em>
|
|
||||||
<strong>{{ roomState.playerCount }}/{{ roomState.maxPlayers }}</strong>
|
|
||||||
</span>
|
|
||||||
<button
|
|
||||||
class="ghost-btn ws-reconnect"
|
|
||||||
type="button"
|
|
||||||
:disabled="!canStartGame || startGamePending"
|
|
||||||
@click="sendStartGame"
|
|
||||||
>
|
|
||||||
{{ startGamePending ? '开局请求中...' : '开始游戏' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="table-panel game-table-panel action-panel">
|
|
||||||
<div class="action-grid">
|
|
||||||
<div class="action-card">
|
|
||||||
<h3>我的手牌</h3>
|
|
||||||
<p class="action-hint">当前仅渲染 `my_hand` 事件下发的真实手牌。</p>
|
|
||||||
<div class="hand-wall" v-if="roomState.myHand.length > 0">
|
|
||||||
<button
|
|
||||||
v-for="(tile, index) in roomState.myHand"
|
|
||||||
:key="`${tile}-${index}`"
|
|
||||||
class="tile-chip"
|
|
||||||
type="button"
|
|
||||||
:class="{ selected: selectedTile === tile }"
|
|
||||||
@click="selectTile(tile)"
|
|
||||||
>
|
|
||||||
{{ tile }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p v-else class="action-empty">尚未收到 `my_hand`。</p>
|
|
||||||
<p class="action-meta">已选牌:{{ selectedTileText }}</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="action-card">
|
<div class="scene-watermark">
|
||||||
<h3>对局动作</h3>
|
|
||||||
<p class="action-hint">已接入 `draw / discard / peng / gang / hu / pass` WS 发包。</p>
|
|
||||||
<div class="action-buttons">
|
|
||||||
<button
|
|
||||||
v-for="action in actionButtons"
|
|
||||||
:key="action.type"
|
|
||||||
class="primary-btn action-btn"
|
|
||||||
type="button"
|
|
||||||
:disabled="action.disabled"
|
|
||||||
@click="sendGameAction(action.type)"
|
|
||||||
>
|
|
||||||
{{ action.label }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p class="action-meta">当前响应窗口:{{ pendingClaimText }}</p>
|
|
||||||
<p class="action-meta">
|
|
||||||
最近弃牌:{{ roomState.game?.state?.lastDiscardTile || '无' }}
|
|
||||||
/ {{ roomState.game?.state?.lastDiscardBy || '无' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="table-shell">
|
|
||||||
<img class="table-desk" :src="deskImage" alt="" />
|
|
||||||
<div class="table-felt">
|
|
||||||
<div class="felt-frame outer"></div>
|
|
||||||
<div class="felt-frame inner"></div>
|
|
||||||
|
|
||||||
<div class="table-watermark">
|
|
||||||
<span>{{ statusRibbon }}</span>
|
|
||||||
<strong>指尖四川麻将</strong>
|
<strong>指尖四川麻将</strong>
|
||||||
<small>底注 6 番 · 封顶 32 倍</small>
|
<span>{{ roomState.name || roomName || '成都麻将房' }}</span>
|
||||||
|
<small>底注 6 亿 · 封顶 32 倍</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TopPlayerCard :player="seatDecor.top"/>
|
<TopPlayerCard :player="seatDecor.top"/>
|
||||||
@@ -366,114 +286,117 @@ onBeforeUnmount(() => {
|
|||||||
<img v-for="key in wallBacks.left" :key="key" :src="getBackImage('left')" alt=""/>
|
<img v-for="key in wallBacks.left" :key="key" :src="getBackImage('left')" alt=""/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="center-deck" :class="`state-${roomState.status}`">
|
<div class="center-desk">
|
||||||
<span class="wind north">北</span>
|
<span class="wind north">北</span>
|
||||||
<span class="wind west">西</span>
|
<span class="wind west">西</span>
|
||||||
|
<strong>{{ centerTimer }}</strong>
|
||||||
<span class="wind south">南</span>
|
<span class="wind south">南</span>
|
||||||
<span class="wind east">东</span>
|
<span class="wind east">东</span>
|
||||||
<strong>{{ centerTimer }}</strong>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="floating-status top">
|
||||||
v-for="seat in seatMarkers"
|
<img v-if="floatingMissingSuit.top" :src="floatingMissingSuit.top" alt=""/>
|
||||||
:key="seat.key"
|
<span>{{ seatDecor.top.missingSuitLabel }}</span>
|
||||||
class="seat"
|
</div>
|
||||||
:class="[
|
<div class="floating-status left">
|
||||||
`seat-${seat.key}`,
|
<img v-if="floatingMissingSuit.left" :src="floatingMissingSuit.left" alt=""/>
|
||||||
{ occupied: seat.occupied, 'seat-me': seat.isSelf, 'seat-turn': seat.isTurn },
|
<span>{{ seatDecor.left.missingSuitLabel }}</span>
|
||||||
]"
|
</div>
|
||||||
|
<div class="floating-status right">
|
||||||
|
<img v-if="floatingMissingSuit.right" :src="floatingMissingSuit.right" alt=""/>
|
||||||
|
<span>{{ seatDecor.right.missingSuitLabel }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="claim-banner">
|
||||||
|
<span>{{ roomStatusText }}</span>
|
||||||
|
<strong>{{ currentPhaseText }}</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottom-control-panel">
|
||||||
|
<div class="control-copy">
|
||||||
|
<p>房间 {{ roomId || '--' }}</p>
|
||||||
|
<small>当前选择:{{ selectedTileText }} · 待响应:{{ pendingClaimText }}</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-orbs">
|
||||||
|
<button
|
||||||
|
v-for="action in actionButtons"
|
||||||
|
:key="action.type"
|
||||||
|
class="orb-button"
|
||||||
|
:class="`theme-${actionTheme(action.type)}`"
|
||||||
|
type="button"
|
||||||
|
:disabled="action.disabled"
|
||||||
|
@click="sendGameAction(action.type)"
|
||||||
>
|
>
|
||||||
<strong>{{ seat.label }}</strong>
|
{{ action.label }}
|
||||||
<small v-if="seat.subLabel">{{ seat.subLabel }}</small>
|
</button>
|
||||||
<span v-if="seat.isTurn" class="turn-indicator">出牌中</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-center">
|
<div class="player-hand" v-if="roomState.myHand.length > 0">
|
||||||
<p>成都麻将</p>
|
<button
|
||||||
<p>{{ roomState.id || roomId || '等待中...' }}</p>
|
v-for="(tile, index) in roomState.myHand"
|
||||||
|
:key="`${tile}-${index}`"
|
||||||
|
class="tile-chip"
|
||||||
|
:class="{ selected: selectedTile === tile }"
|
||||||
|
type="button"
|
||||||
|
@click="selectTile(tile)"
|
||||||
|
>
|
||||||
|
{{ tile }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p v-else class="empty-hand">等待服务端下发 `my_hand`。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-side-buttons">
|
||||||
|
<button class="side-round" type="button" @click="connectWs">聊</button>
|
||||||
|
<button class="side-round" type="button">赏</button>
|
||||||
|
<button
|
||||||
|
class="side-round gold"
|
||||||
|
type="button"
|
||||||
|
:disabled="!canStartGame || startGamePending"
|
||||||
|
@click="sendStartGame"
|
||||||
|
>
|
||||||
|
{{ startGamePending ? '开局中' : '开局' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<aside class="ws-sidebar">
|
||||||
|
<div class="sidebar-head">
|
||||||
|
<div>
|
||||||
|
<p class="sidebar-title">WebSocket 消息</p>
|
||||||
|
<small>{{ networkLabel }} · {{ loggedInUserName || '未登录昵称' }}</small>
|
||||||
|
</div>
|
||||||
|
<button class="sidebar-btn" type="button" @click="connectWs">重连</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sidebar-stats">
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span>房间</span>
|
||||||
|
<strong>{{ roomState.name || roomName || '未命名' }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span>阶段</span>
|
||||||
|
<strong>{{ currentPhaseText }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span>人数</span>
|
||||||
|
<strong>{{ roomState.playerCount }}/{{ roomState.maxPlayers }}</strong>
|
||||||
|
</div>
|
||||||
|
<div class="sidebar-stat">
|
||||||
|
<span>状态</span>
|
||||||
|
<strong>{{ roomStatusText }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="ws-panel">
|
<p v-if="wsError" class="sidebar-error">{{ wsError }}</p>
|
||||||
<div class="ws-panel-head">
|
|
||||||
<strong>实时消息</strong>
|
<div class="sidebar-log">
|
||||||
<div class="ws-actions">
|
<p v-if="rightMessages.length === 0" class="sidebar-empty">等待服务器消息...</p>
|
||||||
<span class="ws-state" :class="`is-${wsStatus}`">{{ networkLabel }}</span>
|
<p v-for="(line, index) in rightMessages" :key="index" class="sidebar-line">{{ line }}</p>
|
||||||
<button class="ghost-btn ws-reconnect" type="button" @click="connectWs">重连</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="wsError" class="message error">{{ wsError }}</p>
|
|
||||||
<div class="ws-log">
|
|
||||||
<p v-if="wsMessages.length === 0" class="ws-empty">等待服务器消息...</p>
|
|
||||||
<p v-for="(line, idx) in wsMessages" :key="idx" class="ws-line">{{ line }}</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.action-panel {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-card {
|
|
||||||
background: rgba(10, 27, 22, 0.72);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-card h3 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-hint,
|
|
||||||
.action-meta,
|
|
||||||
.action-empty {
|
|
||||||
margin: 8px 0 0;
|
|
||||||
font-size: 13px;
|
|
||||||
color: rgba(255, 255, 255, 0.72);
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hand-wall,
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-chip {
|
|
||||||
min-width: 56px;
|
|
||||||
padding: 10px 12px;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
||||||
background: rgba(247, 239, 220, 0.9);
|
|
||||||
color: #1c1b18;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile-chip.selected {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
border-color: #f4c76a;
|
|
||||||
box-shadow: 0 10px 24px rgba(244, 199, 106, 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
min-width: 88px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 960px) {
|
|
||||||
.action-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user