Files
mahjong-web/src/views/ChengduGamePage.vue
wsy182 ceba41fb08 ```
style(global): update background gradients and visual styling

- Replace radial gradient with combined radial and linear gradients
- Update color schemes with warmer tones and improved transparency
- Adjust border colors and add subtle glow effects
- Increase blur intensity for better glassmorphism effect

style(game): enhance seat player card design

- Add avatar panel container for better layout structure
- Implement dealer mark positioning with absolute placement
- Add missing suit icons with computed property mapping
- Replace text-based missing marks with image icons when available
- Improve visual hierarchy and spacing between elements

refactor(game): add computed property for dynamic suit icon selection

- Import suit icon assets (wan, tong, tiao)
- Create computed property to map suit labels to corresponding icons
- Handle fallback to text display when no icon is available
```
2026-03-24 16:26:13 +08:00

1115 lines
25 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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 topBackImage from '../assets/images/tiles/top/tbgs_2.png'
import rightBackImage from '../assets/images/tiles/right/tbgs_1.png'
import bottomBackImage from '../assets/images/tiles/bottom/tdbgs_4.png'
import leftBackImage from '../assets/images/tiles/left/tbgs_3.png'
import TopPlayerCard from '../components/game/TopPlayerCard.vue'
import RightPlayerCard from '../components/game/RightPlayerCard.vue'
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
import LeftPlayerCard from '../components/game/LeftPlayerCard.vue'
import type { SeatPlayerCardModel } from '../components/game/seat-player-card'
import { useChengduGameRoom, type SeatKey } from '../features/chengdu-game/useChengduGameRoom'
const route = useRoute()
const router = useRouter()
const {
roomState,
roomId,
roomName,
loggedInUserName,
wsStatus,
wsError,
wsMessages,
startGamePending,
leaveRoomPending,
canStartGame,
seatViews,
selectedTile,
actionButtons,
connectWs,
sendStartGame,
selectTile,
sendGameAction,
backHall,
} = useChengduGameRoom(route, router)
const now = ref(Date.now())
let clockTimer: number | null = null
const roomStatusText = computed(() => {
if (roomState.value.status === 'playing') {
return '对局中'
}
if (roomState.value.status === 'finished') {
return '已结束'
}
return '等待中'
})
const currentPhaseText = computed(() => {
const phase = roomState.value.game?.state?.phase?.trim()
if (!phase) {
return roomState.value.status === 'playing' ? '牌局进行中' : '未开局'
}
const phaseLabelMap: Record<string, string> = {
dealing: '发牌',
draw: '摸牌',
discard: '出牌',
action: '响应',
settle: '结算',
finished: '已结束',
}
return phaseLabelMap[phase] ?? phase
})
const networkLabel = computed(() => {
if (wsStatus.value === 'connected') {
return '已连接'
}
if (wsStatus.value === 'connecting') {
return '连接中'
}
return '未连接'
})
const formattedClock = computed(() => {
return new Date(now.value).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
})
})
const wallBacks = computed<Record<SeatKey, string[]>>(() => {
const wallSize = roomState.value.game?.state?.wall.length ?? 0
const perSide = Math.max(6, Math.ceil((wallSize || 48) / 4 / 2))
return {
top: Array.from({ length: perSide }, (_, index) => `top-${index}`),
right: Array.from({ length: perSide }, (_, index) => `right-${index}`),
bottom: Array.from({ length: perSide }, (_, index) => `bottom-${index}`),
left: Array.from({ length: perSide }, (_, index) => `left-${index}`),
}
})
const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
const scoreMap = roomState.value.game?.state?.scores ?? {}
const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1
const defaultMissingSuitLabel = missingSuitLabel(null)
const emptySeat = (avatar: string): SeatPlayerCardModel => ({
avatar,
name: '空位',
money: '--',
dealer: false,
isTurn: false,
isOnline: false,
missingSuitLabel: defaultMissingSuitLabel,
})
const result: Record<SeatKey, SeatPlayerCardModel> = {
top: emptySeat('1'),
right: emptySeat('2'),
bottom: emptySeat('我'),
left: emptySeat('4'),
}
for (const [index, seat] of seatViews.value.entries()) {
if (!seat.player) {
continue
}
const playerId = seat.player.playerId
const score = scoreMap[playerId]
result[seat.key] = {
avatar: seat.isSelf ? '我' : String(index + 1),
name: seat.isSelf ? '你自己' : seat.player.displayName || `玩家${seat.player.index + 1}`,
money: typeof score === 'number' ? String(score) : '--',
dealer: seat.player.index === dealerIndex,
isTurn: seat.isTurn,
isOnline: true,
missingSuitLabel: missingSuitLabel(seat.player.missingSuit),
}
}
return result
})
const centerTimer = computed(() => {
const wallLeft = roomState.value.game?.state?.wall.length
if (typeof wallLeft === 'number' && Number.isFinite(wallLeft)) {
return String(wallLeft).padStart(2, '0')
}
return String(roomState.value.playerCount).padStart(2, '0')
})
const selectedTileText = computed(() => selectedTile.value ?? '未选择')
const pendingClaimText = computed(() => {
const claim = roomState.value.game?.state?.pendingClaim
if (!claim) {
return '无'
}
try {
return JSON.stringify(claim)
} catch {
return '有待响应动作'
}
})
const rightMessages = computed(() => wsMessages.value.slice(-16).reverse())
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 {
if (!value) {
return '未定'
}
const suitMap: Record<string, string> = {
wan: '万',
tong: '筒',
tiao: '条',
}
return suitMap[value] ?? value
}
function getBackImage(seat: SeatKey): string {
const imageMap: Record<SeatKey, string> = {
top: topBackImage,
right: rightBackImage,
bottom: bottomBackImage,
left: leftBackImage,
}
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(() => {
clockTimer = window.setInterval(() => {
now.value = Date.now()
}, 1000)
})
onBeforeUnmount(() => {
if (clockTimer !== null) {
window.clearInterval(clockTimer)
clockTimer = null
}
})
</script>
<template>
<section class="picture-scene">
<div class="picture-layout">
<section class="table-stage">
<img class="table-desk" :src="deskImage" alt="" />
<div class="table-felt">
<div class="table-surface"></div>
<div class="inner-outline outer"></div>
<div class="inner-outline mid"></div>
<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>
<button class="metal-circle" type="button" @click="connectWs"></button>
</div>
<div class="top-right-clock">
<div class="signal-chip">
<span class="wifi-dot" :class="`is-${wsStatus}`"></span>
<strong>{{ networkLabel }}</strong>
</div>
<span>{{ formattedClock }}</span>
</div>
<div class="scene-watermark">
<strong>四川麻将</strong>
<span>{{ roomState.name || roomName || '成都麻将房' }}</span>
<small>底注 6 亿 · 封顶 32 </small>
</div>
<TopPlayerCard :player="seatDecor.top" />
<RightPlayerCard :player="seatDecor.right" />
<BottomPlayerCard :player="seatDecor.bottom" />
<LeftPlayerCard :player="seatDecor.left" />
<div class="wall wall-top">
<img v-for="key in wallBacks.top" :key="key" :src="getBackImage('top')" alt="" />
</div>
<div class="wall wall-right">
<img v-for="key in wallBacks.right" :key="key" :src="getBackImage('right')" alt="" />
</div>
<div class="wall wall-bottom">
<img v-for="key in wallBacks.bottom" :key="key" :src="getBackImage('bottom')" alt="" />
</div>
<div class="wall wall-left">
<img v-for="key in wallBacks.left" :key="key" :src="getBackImage('left')" alt="" />
</div>
<div class="center-desk">
<span class="wind north"></span>
<span class="wind west">西</span>
<strong>{{ centerTimer }}</strong>
<span class="wind south"></span>
<span class="wind east"></span>
</div>
<div class="floating-status top">
<img v-if="floatingMissingSuit.top" :src="floatingMissingSuit.top" alt="" />
<span>{{ seatDecor.top.missingSuitLabel }}</span>
</div>
<div class="floating-status left">
<img v-if="floatingMissingSuit.left" :src="floatingMissingSuit.left" alt="" />
<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)"
>
{{ action.label }}
</button>
</div>
<div class="player-hand" v-if="roomState.myHand.length > 0">
<button
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>
<p v-if="wsError" class="sidebar-error">{{ wsError }}</p>
<div class="sidebar-log">
<p v-if="rightMessages.length === 0" class="sidebar-empty">等待服务器消息...</p>
<p v-for="(line, index) in rightMessages" :key="index" class="sidebar-line">{{ line }}</p>
</div>
</aside>
</div>
</section>
</template>
<style scoped>
.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: 14px;
left: 14px;
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: 18px;
height: 18px;
border-radius: 3px;
background: linear-gradient(180deg, #43d34b 0%, #159d22 100%);
}
.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;
}
}
</style>