feat(ui): 按线上 DOM 重建大厅首页与车库布局
This commit is contained in:
1 parent
7b5738187a
commit
7923c60f98
4 files changed
+834
-704
No files matched your search
@@ -220,11 +220,65 @@ TankiOffline (Kotlin/JS 客户端)
|
||||
- 资源:本地镜像已接入。
|
||||
- 已确认:技术栈 Kotlin/React、组件级还原、本地 mock、3D 现在做、首批大厅+车库、允许混跑。
|
||||
|
||||
## 8. 实现进度(滚动更新)
|
||||
## 9. 原版 DOM 实测映射(2026-10-25,登录 WYT2WYT 后)
|
||||
|
||||
视口 1296×1006,根字号 12.96px(`.ksc-37` 的 `max(min(1.48148vh,1vw),3px)`)。
|
||||
所有尺寸换算:1em ≈ 12.96px。
|
||||
|
||||
### 9.1 大厅首页
|
||||
|
||||
| 原 DOM 类 | 位置 / 尺寸 | 含义 |
|
||||
|---|---|---|
|
||||
| `MainScreenComponentStyle-containerPanel` | 0,0 1296×78 | 顶栏(6em,底边 1px 白 25%) |
|
||||
| `UserInfoContainerStyle-blockLeftPanel` | 0,0 408×78 | 等级图标 / 组队图标 / 昵称 / XP |
|
||||
| `UserInfoContainerStyle-rankIconContainerClickable` | 155,0 78×78 | 等级图标(`static/images/<rank>.<hash>.webp`) |
|
||||
| `UserInfoContainerStyle-containerProgressMainScreen` | 246,15 162×49 | 昵称 + XP 条 + 数值 |
|
||||
| `UserScoreComponentStyle-blockRightPanel` | 1017,0 279×78 | 两行货币(红宝石 / 水晶)+ 全屏 + 退出 |
|
||||
| `MainScreenComponentStyle-containerForMenuGradient` | 0,0 648×1006 | 左侧 50%,黑→透明 0.5 |
|
||||
| `MainScreenComponentStyle-playButtonContainer` | 57,130 284×139 | “开始战斗”,背景 `videoplay.79570900.gif` |
|
||||
| `MainScreenComponentStyle-blockMainMenu` | 57,283 324×645 | 菜单容器 |
|
||||
| `PrimaryMenuItemComponentStyle-menuItemContainer` | 65,283 350×58 | 菜单项:礼盒 / 任务 / 车库 / 商店 / 设置 |
|
||||
| `PrimaryMenuItemComponentStyle-itemCommonLi` | 73,290 45×45 | 图标(mask + `#bfd5ff`) |
|
||||
| `PrimaryMenuItemComponentStyle-itemName` | 133,293 | 2em 大写文字 |
|
||||
| `FooterComponentStyle-footer` | 0,928 648×78 | 新闻 / 聊天 / 好友 / 军团(65×65) |
|
||||
| `MainScreenComponentStyle-gradientBlockRight` | 936,0 360×1006 | 右侧渐变(27.75em,0.75,旋转 180°) |
|
||||
| `BattlePassLobbyComponentStyle-menuBattlePass` | 972,110 292×130 | 星级挑战面板(22.5em×10em,right 2.5em) |
|
||||
| `AnnouncementHomeScreenComponentStyle-mainContainer` | 971,778 293×196 | 公告卡(22.5em 宽,right/bottom 2.5em) |
|
||||
| `ClientInfoComponentStyle-main` | 0,989 1296×17 | UID / 版本 / 电池 |
|
||||
|
||||
### 9.2 车库
|
||||
|
||||
| 原 DOM 类 | 位置 / 尺寸 | 含义 |
|
||||
|---|---|---|
|
||||
| `BreadcrumbsComponentStyle-headerContainer` | 0,0 1296×78 | 车库顶栏(返回 / 标题 / 货币 / 退出) |
|
||||
| `BreadcrumbsComponentStyle-backButton` | 0,0 78×78 | 返回 |
|
||||
| `BreadcrumbsComponentStyle-rootTitle` | 585,2 126×73 | 当前屏幕标题 |
|
||||
| `GarageCommonStyle-garageContainer` | 0,78 1296×928 | 车库主体 |
|
||||
| `GarageCommonStyle-positionContent` | 0,130 1296×876 | 内容区 |
|
||||
| `GarageMainScreenStyle-blockParameters` | 78,130 1140×685 | 中央 3D 预览 + 左右参数 |
|
||||
| `TankParametersStyle-leftParamsContainer` | 78,130 293×209 | 左上:GS + 参数 |
|
||||
| `MountedItemsStyle-container` | 920,130 298×492 | 右上:无人机 / 手榴弹 / 炮塔 / 底盘 |
|
||||
| `MountedItemsStyle-resistanceContainer` | 78,740 52×65 ×4 | 左下:防御模块 |
|
||||
| `SuppliesComponentStyle-commonBlockSupplies` | 78,862 1140×125 | 底部道具栏 |
|
||||
| `TankPreviewComponentStyle-visible GarageComponentStyle-tankPreview` | 0,0 1296×1006 | 3D 坦克画布 |
|
||||
|
||||
### 9.3 对应关系(JS → DOM)
|
||||
|
||||
- `82347` 里 `Sq(Class, "XxxComponentStyle", ...)` 注册的样式类,就是 DOM 上的 `XxxComponentStyle-*`。
|
||||
- `Eq("containerPanel", 1, ...)` 返回的类名拼接为 `MainScreenComponentStyle-containerPanel`。
|
||||
- 样式值直接写在 `jq(Class).xxx = function(){ return this.<slot>.dk(this, Eq("...")) }` 的样式对象里;
|
||||
运行时可从 `document.styleSheets` 按类名前缀抓取(已用于本表)。
|
||||
- 组件层级:`MainScreen` → `Header` / `PrimaryMenu` / `Footer` / `BattlePass` / `Announcement`;
|
||||
车库:`GarageCommon` → `TankParameters` / `MountedItems` / `Supplies` / `TankPreview`。
|
||||
|
||||
### 9.4 当前实现状态
|
||||
|
||||
- 大厅首页已按上表重建(6em 顶栏、21.875em Play、27em 菜单、5em 底栏、22.5em 右栏)。
|
||||
- 车库正在按 9.2 重建:左侧参数 / 中央 3D / 右侧已装配件 / 底部道具栏;
|
||||
物品选择列表作为抽屉保留,后续替换为原版的分类轮盘。
|
||||
- [x] Kotlin/React 工具链接通(kotlin-react / kotlin-react-dom / kotlin-emotion,React 19.1)。
|
||||
- [x] 物品目录 fixture:`SessionCheck` 从抓包导出 `resources/fixtures/catalog.json`(1399 物品)。
|
||||
- [x] 车库第一版 React 界面:分类页签(12 类)、搜索、物品网格(120 张卡片)、右侧详情位。
|
||||
- [ ] 车库物品详情 / 3D 预览加载器。
|
||||
- [ ] 车库按 9.2 的原版布局重建 + 3D 预览加载器。
|
||||
- [ ] 协议级 mock 服务器(用户 / 大厅列表 / 任务)。
|
||||
- [ ] 大厅其余屏幕迁移到 React。
|
||||
@@ -1,74 +1,62 @@
|
||||
// LobbyView.kt —— 真实大厅 UI(接入 RootState / 物品目录 / 官方文案与图标)
|
||||
// LobbyView.kt —— 大厅主界面(按线上 DOM/CSS 复刻)
|
||||
//
|
||||
// 目标是把「诊断页」换成可持续扩展的大厅骨架:
|
||||
// 顶栏(等级 / GS / 水晶 / 金币 / VIP)+ 左侧官方菜单 + 主内容区 + 右侧聊天/公告 + 底部连接状态。
|
||||
// 文案运行时从游戏自带的 zh-CN 翻译拉取(Translations.kt),图标用 resources.json 里的官方资源路径。
|
||||
// 车库 / 商店 / 幸运礼盒直接读 ItemCatalog(抓包解出的 1399 物品 + 516 装置)。
|
||||
// 结构对照 3dtank.com/play/ 登录后的真实 DOM:
|
||||
// canvas(3D 车库场景)
|
||||
// MainScreenComponentStyle-containerPanel(顶栏 6em = 78px)
|
||||
// UserInfoContainerStyle-blockLeftPanel(等级图标 / 昵称 / XP)
|
||||
// UserScoreComponentStyle-blockRightPanel(红宝石 / 水晶 / 全屏 / 退出)
|
||||
// MainScreenComponentStyle-containerForMenuGradient(左侧 50% 渐变区)
|
||||
// playButtonContainer(21.875em × 10.75em,videoplay.gif)
|
||||
// blockMainMenu(幸运礼盒 / 任务 / 车库 / 商店 / 设置)
|
||||
// FooterComponentStyle-footer(新闻 / 聊天 / 好友 / 军团)
|
||||
// MainScreenComponentStyle-gradientBlockRight(右侧 27.75em)
|
||||
// BattlePassLobbyComponentStyle-menuBattlePass(22.5em × 10em)
|
||||
// AnnouncementHomeScreenComponentStyle-mainContainer(22.5em 宽)
|
||||
// ClientInfoComponentStyle-main(底部 UID / 版本)
|
||||
// 根字号 12.96px(原 .ksc-37),所有尺寸用 em 与线上一致。
|
||||
package App.Browser
|
||||
|
||||
import App.Browser.Ui.GarageContext
|
||||
import App.Browser.Ui.GarageMount
|
||||
import App.Browser.Ui.UiCatalog
|
||||
import App.State.GarageItemObject
|
||||
import Net.Generated.DescriptionModelCC
|
||||
import App.State.Reducers.UserRankBounds
|
||||
|
||||
private val Doc: dynamic = js("document")
|
||||
private val LDoc: dynamic = js("document")
|
||||
|
||||
/** 官方静态资源前缀(资源路径来自 resources.json)。 */
|
||||
private const val AssetRoot = "/play/"
|
||||
private const val LAssetRoot = "/play/"
|
||||
|
||||
private fun Asset(Path: String): String = AssetRoot + Path
|
||||
private fun LAsset(Path: String): String = LAssetRoot + Path
|
||||
|
||||
private fun Esc(Text: String): String = Text
|
||||
private fun LEsc(Text: String): String = Text
|
||||
.replace("&", "&")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\"", """)
|
||||
|
||||
/** 车库分类:数值来自 module_85667 的 ItemCategory 枚举。 */
|
||||
private val Categories: List<Triple<Int, String, String>> = listOf(
|
||||
Triple(0, "WEAPON", "炮塔"),
|
||||
Triple(1, "ARMOR", "底盘"),
|
||||
Triple(2, "PAINT", "迷彩"),
|
||||
Triple(3, "INVENTORY", "道具"),
|
||||
Triple(5, "KIT", "套件"),
|
||||
Triple(10, "RESISTANCE_MODULE", "防御模块"),
|
||||
Triple(11, "DEVICE", "装置"),
|
||||
Triple(13, "CONTAINER", "幸运礼盒"),
|
||||
Triple(14, "DRONE", "辅助无人机"),
|
||||
Triple(15, "SKIN", "皮肤"),
|
||||
Triple(17, "BAZOOKA", "火箭炮"),
|
||||
Triple(18, "GRENADE", "手榴弹"),
|
||||
)
|
||||
|
||||
private val RarityColors = listOf("#9aa3b2", "#5fbf6a", "#3f8fe0", "#a05fe0", "#e0a13f", "#e05f5f")
|
||||
|
||||
private fun ItemName(Item: GarageItemObject): String {
|
||||
val D = Item.description as? DescriptionModelCC
|
||||
return D?.Name ?: "物品 ${Item.id}"
|
||||
}
|
||||
|
||||
private fun ItemRarity(Item: GarageItemObject): Int = Item.rarity as? Int ?: 0
|
||||
|
||||
private fun ItemCategory(Item: GarageItemObject): Int = Item.itemCategory
|
||||
|
||||
private fun CategoryLabel(Category: Int): String {
|
||||
val Entry = Categories.firstOrNull { it.first == Category } ?: return "其它"
|
||||
return Translations.T("lobby.garage.${Entry.second}", Entry.third)
|
||||
/** 等级图标:10–31 级有官方资源;低等级回退到 10 级图标。 */
|
||||
private fun RankIcon(Rank: Int): String {
|
||||
val R = if (Rank in 10..31) Rank else 10
|
||||
val Hash = mapOf(
|
||||
10 to "17447044", 11 to "f4ddcab2", 12 to "c8158aa7", 13 to "2baa3518",
|
||||
14 to "1fcc1281", 15 to "eaafa648", 16 to "9d0599ee", 17 to "dd67d583",
|
||||
18 to "e86c45fa", 19 to "bc7f519b", 20 to "76063620", 21 to "cef08a4f",
|
||||
22 to "0bc84156", 23 to "3c31b410", 24 to "e5342906", 25 to "ef416fe8",
|
||||
26 to "128b1504", 27 to "c9a72f49", 28 to "b96f89b9", 29 to "ee5f90ba",
|
||||
30 to "5c2d92a0", 31 to "91cbb062",
|
||||
)[R] ?: "eaafa648"
|
||||
return LAsset("static/images/$R.$Hash.webp")
|
||||
}
|
||||
|
||||
class LobbyView(private val Client: LobbyClient) {
|
||||
|
||||
private var Nav: String = "home"
|
||||
private var Category: Int = 0
|
||||
private var Query: String = ""
|
||||
private var Mounted = false
|
||||
private var GarageMounted = false
|
||||
|
||||
fun Mount() {
|
||||
if (Mounted) return
|
||||
Mounted = true
|
||||
val Root = Doc.getElementById("app")
|
||||
Root.innerHTML = Shell()
|
||||
LDoc.getElementById("app").innerHTML = Shell()
|
||||
Bind()
|
||||
Update()
|
||||
}
|
||||
@@ -77,160 +65,162 @@ class LobbyView(private val Client: LobbyClient) {
|
||||
if (!Mounted) return
|
||||
val State = Client.Router.Store.State
|
||||
val U = State.User
|
||||
val Bounds = U.RankBounds as? UserRankBounds ?: UserRankBounds()
|
||||
val Low = Bounds.LowBound
|
||||
val Top = if (Bounds.TopBound > 0) Bounds.TopBound else Low + 1
|
||||
val Ratio = ((U.Score - Low).toDouble() / (Top - Low).coerceAtLeast(1)).coerceIn(0.0, 1.0)
|
||||
|
||||
SetHtml(
|
||||
"lobby-stats",
|
||||
"ms-top-left",
|
||||
"""
|
||||
<div class="user-block">
|
||||
<div class="rank-badge">${U.Rank}</div>
|
||||
<div class="user-meta">
|
||||
<b>${Esc(U.Uid.ifEmpty { "未登录" })}</b>
|
||||
<span>GS ${U.GearScore} · Rank ${U.Rank}</span>
|
||||
<div class="rank-icon"><img src="${RankIcon(U.Rank)}" alt=""></div>
|
||||
<div class="user-progress">
|
||||
<span class="user-name">${LEsc(U.Uid.ifEmpty { "未登录" })}</span>
|
||||
<div class="xp-bar"><i style="width:${(Ratio * 100).toInt()}%"></i></div>
|
||||
<div class="xp-text"><i class="xp-icon"></i>${U.Score}<span> / ${if (Bounds.TopBound > 0) Top else "—"}</span></div>
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
SetHtml(
|
||||
"ms-top-right",
|
||||
"""
|
||||
<div class="ms-currencies">
|
||||
<div class="ms-currency">
|
||||
<img class="coin" src="${LAsset("static/images/ruby.ce62efbc.svg")}" alt="">
|
||||
<span>${U.Rubies}</span>
|
||||
<img class="plus" src="${LAsset("static/images/addRuby.ff47c8be.svg")}" alt="">
|
||||
</div>
|
||||
<div class="ms-currency">
|
||||
<img class="coin" src="${LAsset("static/images/crystalSmall.242e6c15.svg")}" alt="">
|
||||
<span>${U.Crystals}</span>
|
||||
<img class="plus" src="${LAsset("static/images/addCrystal.af9a397c.svg")}" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="currencies">
|
||||
<span class="stat" title="水晶"><img src="${Asset("static/images/crystalSmall.242e6c15.svg")}">${U.Crystals}</span>
|
||||
<span class="stat" title="坦克币"><img src="${Asset("static/images/gold.e0b82d44.svg")}">${U.Score}</span>
|
||||
<span class="stat ${if (U.HasPremium) "premium" else "muted"}" title="VIP">
|
||||
<img src="${Asset("static/images/premiumIcon.e8725ab0.svg")}">${if (U.HasPremium) "VIP" else "普通"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="ms-fullscreen"><i></i><b>F11</b></div>
|
||||
<button class="ms-logout" data-go="logout"><i></i></button>
|
||||
"""
|
||||
)
|
||||
if (Nav != "garage" && GarageMounted) {
|
||||
GarageMount.Unmount()
|
||||
GarageMounted = false
|
||||
}
|
||||
if (!(Nav == "garage" && GarageMounted)) {
|
||||
|
||||
LDoc.getElementById("lobby-content").classList.toggle("hidden", Nav == "home")
|
||||
LDoc.getElementById("ms-home").classList.toggle("hidden", Nav != "home")
|
||||
LDoc.getElementById("ms-top").classList.toggle("hidden", Nav != "home")
|
||||
LDoc.getElementById("ms-back").classList.toggle("hidden", Nav == "home" || Nav == "garage")
|
||||
LDoc.getElementById("ms-back").textContent = "← 返回"
|
||||
|
||||
if (Nav == "home") {
|
||||
if (GarageMounted) {
|
||||
GarageMount.Unmount()
|
||||
GarageMounted = false
|
||||
}
|
||||
SetHtml("lobby-content", "")
|
||||
} else if (!(Nav == "garage" && GarageMounted)) {
|
||||
SetHtml("lobby-content", Content())
|
||||
if (Nav == "garage" && UiCatalog.Loaded) {
|
||||
GarageContext.Uid = U.Uid
|
||||
GarageContext.Rank = U.Rank
|
||||
GarageContext.GearScore = U.GearScore
|
||||
GarageContext.Crystals = U.Crystals
|
||||
GarageContext.Rubies = U.Rubies
|
||||
GarageContext.OnBack = {
|
||||
Nav = "home"
|
||||
Update()
|
||||
}
|
||||
GarageMount.Mount(UiCatalog.Items)
|
||||
GarageMounted = true
|
||||
}
|
||||
}
|
||||
if (Nav == "garage" && UiCatalog.Loaded) {
|
||||
GarageMount.Mount(UiCatalog.Items)
|
||||
GarageMounted = true
|
||||
}
|
||||
SetHtml("lobby-chat", Chat())
|
||||
|
||||
SetHtml(
|
||||
"lobby-status",
|
||||
"${Client.Status}|帧 ${Client.Frames}|记录 ${Client.Records}|已派发 ${Client.Router.Applied}" +
|
||||
"|未覆盖 ${Client.Router.Unmapped}|目录 ${Client.Router.Catalog.Items.size} 物品 / " +
|
||||
"${Client.Router.Catalog.Devices.size} 装置|屏幕 ${State.Screen.Current}"
|
||||
"client-info",
|
||||
"UID: ${LEsc(U.Uid.ifEmpty { "-" })}<i>|</i>TYPE: web<i>|</i>VER: Kotlin/JS<i>|</i>" +
|
||||
"SRV: offline<i>|</i>STATE: ${LEsc(Client.Status)}"
|
||||
)
|
||||
BindDynamic()
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ 骨架
|
||||
|
||||
private fun Shell(): String = """
|
||||
<div class="lobby">
|
||||
<div class="lobby-bg"></div>
|
||||
<header class="topbar">
|
||||
<div id="lobby-stats" class="stats"></div>
|
||||
<button id="connect-toggle" class="connect-toggle">连接</button>
|
||||
<div class="ms-root">
|
||||
<canvas id="lobby-canvas"></canvas>
|
||||
<div class="ms-bg"></div>
|
||||
|
||||
<header id="ms-top" class="ms-top">
|
||||
<div id="ms-top-left" class="ms-top-left"></div>
|
||||
<div class="ms-top-logo"><img src="${LAsset("static/images/logoTankiOnlineChina.0b6ba850.svg")}" alt=""></div>
|
||||
<div id="ms-top-right" class="ms-top-right"></div>
|
||||
</header>
|
||||
|
||||
<div class="lobby-body">
|
||||
<nav class="menu">
|
||||
${NavButton("battles", "iconLobbyMenuWar.ada9b0af.svg", "开始战斗", "BATTLE")}
|
||||
${NavButton("garage", "iconLobbyMenuGarage.0b623165.svg", "车库", "GARAGE")}
|
||||
${NavButton("quests", "iconLobbyMenuQuests.54801b8b.svg", "任务", "QUESTS")}
|
||||
${NavButton("shop", "iconLobbyMenuStore.e755fe14.svg", "商店", "SHOP")}
|
||||
${NavButton("containers", "iconLobbyMenuContainers.f960290f.svg", "幸运礼盒", "CONTAINERS")}
|
||||
${NavButton("clans", "iconLobbyMenuClan.f01ae646.svg", "军团", "CLANS")}
|
||||
${NavButton("friends", "Add-friend.137d3601.svg", "好友", "FRIENDS")}
|
||||
${NavButton("news", "iconLobbyMenuNews.6efe9ae3.svg", "新闻", "NEWS")}
|
||||
${NavButton("settings", "iconLobbyMenuSettings.cc00d08a.svg", "设置", "OPTIONS")}
|
||||
</nav>
|
||||
<div id="ms-home" class="ms-home">
|
||||
<div class="ms-left-gradient">
|
||||
<button class="ms-play" data-go="battles">
|
||||
<span>${LEsc(Translations.T("lobby.menu.PLAY", "开始战斗"))}</span>
|
||||
</button>
|
||||
<ul class="ms-menu">
|
||||
${MenuItem("containers", "Containers", "幸运礼盒")}
|
||||
${MenuItem("quests", "Quests", "任务")}
|
||||
${MenuItem("garage", "Garage", "车库")}
|
||||
${MenuItem("shop", "Shop", "商店")}
|
||||
${MenuItem("settings", "Option", "设置")}
|
||||
</ul>
|
||||
<footer class="ms-footer">
|
||||
<ul>
|
||||
${FooterItem("news", "News")}
|
||||
${FooterItem("chat", "Chat")}
|
||||
${FooterItem("friends", "Friends")}
|
||||
${FooterItem("clans", "Clan")}
|
||||
</ul>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<main id="lobby-content" class="content"></main>
|
||||
</div>
|
||||
|
||||
<div id="lobby-chat" class="lobby-chat"></div>
|
||||
|
||||
<button class="play-fab" data-go="battles">
|
||||
<img src="${Asset("static/images/join_battle.b0903eb3.svg")}" alt="">
|
||||
<span>${Esc(Translations.T("lobby.menu.PLAY", "开始战斗"))}</span>
|
||||
</button>
|
||||
|
||||
<footer class="statusbar"><span id="lobby-status">未连接</span></footer>
|
||||
|
||||
<div id="connect-panel" class="connect-panel hidden">
|
||||
<h3>连接中继</h3>
|
||||
<label>大厅地址<input id="lobby-url" spellcheck="false"></label>
|
||||
<label>空间地址<input id="space-url" spellcheck="false"></label>
|
||||
<div class="connect-actions">
|
||||
<button id="connect-go" class="primary">连接</button>
|
||||
<button id="connect-close">关闭</button>
|
||||
<div class="ms-right-gradient"></div>
|
||||
<div class="ms-battle-pass">
|
||||
<div class="ms-bp-head">
|
||||
<span><img src="${LAsset("static/images/star.01f7e4f8.svg")}" alt="">${Client.Router.Store.State.Challenges.Stars.values.sum()}</span>
|
||||
<span class="muted">星级挑战</span>
|
||||
</div>
|
||||
<div class="ms-bp-row"><i></i><div><b>等级 ${Client.Router.Store.State.Challenges.BattlePasses.size}</b><span class="muted">星级挑战</span></div></div>
|
||||
<div class="ms-bp-row locked"><i></i><div><b>精英通行证</b><span class="muted">未解锁</span></div></div>
|
||||
</div>
|
||||
<div class="ms-announce">
|
||||
<div class="ms-announce-art"></div>
|
||||
<div class="ms-announce-title">${LEsc(Translations.T("lobby.menu.NEWS", "新闻"))}</div>
|
||||
</div>
|
||||
<p class="hint">默认 ws://127.0.0.1:9091/(本地 wss_server 中继)。也可用 ?lobby=…&space=… 预填。</p>
|
||||
</div>
|
||||
|
||||
<button id="ms-back" class="ms-back hidden" data-go="home">← 返回</button>
|
||||
<main id="lobby-content" class="ms-content hidden"></main>
|
||||
|
||||
<footer id="client-info" class="ms-client-info"></footer>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
|
||||
private fun NavButton(Id: String, Icon: String, Label: String, Key: String): String = """
|
||||
<button class="nav" data-nav="$Id">
|
||||
<img src="${Asset("static/images/$Icon")}" alt="">
|
||||
<span>${Esc(Translations.T("lobby.menu.$Key", Label))}</span>
|
||||
</button>
|
||||
private fun MenuItem(Id: String, Key: String, Fallback: String): String = """
|
||||
<li class="ms-menu-item" data-go="$Id">
|
||||
<div class="ms-menu-icon icon-$Id"></div>
|
||||
<span>${LEsc(Translations.T("lobby.menu.${Key.uppercase()}", Fallback))}</span>
|
||||
</li>
|
||||
""".trimIndent()
|
||||
|
||||
private fun FooterItem(Id: String, Key: String): String = """
|
||||
<li class="ms-footer-item" data-go="$Id">
|
||||
<div class="ms-footer-icon icon-$Id"></div>
|
||||
</li>
|
||||
""".trimIndent()
|
||||
|
||||
private fun Bind() {
|
||||
Doc.querySelectorAll(".nav").forEach { El: dynamic ->
|
||||
LDoc.querySelectorAll("[data-go]").forEach { El: dynamic ->
|
||||
El.addEventListener("click", {
|
||||
Nav = El.getAttribute("data-nav") as String
|
||||
Update()
|
||||
})
|
||||
}
|
||||
Doc.getElementById("connect-toggle").addEventListener("click", {
|
||||
val Panel = Doc.getElementById("connect-panel")
|
||||
Panel.classList.toggle("hidden")
|
||||
})
|
||||
Doc.getElementById("connect-close").addEventListener("click", {
|
||||
Doc.getElementById("connect-panel").classList.add("hidden")
|
||||
})
|
||||
Doc.getElementById("connect-go").addEventListener("click", {
|
||||
val Lobby = (Doc.getElementById("lobby-url").value as String).trim()
|
||||
val Space = (Doc.getElementById("space-url").value as String).trim()
|
||||
if (Lobby.isEmpty()) {
|
||||
Client.Log("✗ 大厅地址不能为空")
|
||||
} else {
|
||||
Client.Connect(Lobby, if (Space.isEmpty()) Lobby else Space)
|
||||
Doc.getElementById("connect-panel").classList.add("hidden")
|
||||
}
|
||||
})
|
||||
val LobbyInput = Doc.getElementById("lobby-url")
|
||||
val SpaceInput = Doc.getElementById("space-url")
|
||||
val Params = js("new URLSearchParams(window.location.search)")
|
||||
val LobbyDefault = (Params.get("lobby") as? String) ?: "ws://127.0.0.1:9091/"
|
||||
val SpaceDefault = (Params.get("space") as? String) ?: LobbyDefault
|
||||
LobbyInput.value = LobbyDefault
|
||||
SpaceInput.value = SpaceDefault
|
||||
if (Params.get("autoconnect") == "1") {
|
||||
Client.Connect(LobbyDefault, SpaceDefault)
|
||||
}
|
||||
}
|
||||
|
||||
private fun BindDynamic() {
|
||||
Doc.querySelectorAll(".nav").forEach { El: dynamic ->
|
||||
val Id = El.getAttribute("data-nav") as String
|
||||
El.classList.toggle("active", Id == Nav)
|
||||
}
|
||||
Doc.querySelectorAll("[data-garage-cat]").forEach { El: dynamic ->
|
||||
El.addEventListener("click", {
|
||||
Category = (El.getAttribute("data-garage-cat") as String).toInt()
|
||||
Query = ""
|
||||
Update()
|
||||
})
|
||||
}
|
||||
Doc.querySelectorAll("[data-go]").forEach { El: dynamic ->
|
||||
El.addEventListener("click", {
|
||||
Nav = El.getAttribute("data-go") as String
|
||||
Update()
|
||||
})
|
||||
}
|
||||
val Search = Doc.getElementById("garage-search")
|
||||
if (Search != null) {
|
||||
Search.addEventListener("input", {
|
||||
Query = (Search.value as String)
|
||||
Refresh()
|
||||
val Target = El.getAttribute("data-go") as String
|
||||
if (Target == "logout") {
|
||||
Client.Disconnect()
|
||||
js("window.location.reload()")
|
||||
} else {
|
||||
Nav = Target
|
||||
if (Target == "garage" && !UiCatalog.Loaded) {
|
||||
UiCatalog.Ensure(Client) { Refresh() }
|
||||
}
|
||||
Update()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -255,180 +245,95 @@ class LobbyView(private val Client: LobbyClient) {
|
||||
"containers" -> Containers()
|
||||
"battles" -> Battles()
|
||||
"quests" -> Quests()
|
||||
"clans" -> Placeholder("军团", "军团系统将在 S4 之后接入。")
|
||||
"friends" -> Friends()
|
||||
"chat" -> Chat()
|
||||
"news" -> News()
|
||||
"clans" -> Placeholder("军团", "军团系统将在 S4 之后接入。")
|
||||
"friends" -> Placeholder("好友", "好友列表将在社交链路补齐后接入。")
|
||||
"settings" -> Settings()
|
||||
else -> Home()
|
||||
}
|
||||
|
||||
private fun Home(): String {
|
||||
val Connected = Client.Status == "已连接"
|
||||
return """
|
||||
<section class="hero">
|
||||
<div class="hero-copy">
|
||||
<div class="eyebrow">${Esc(Translations.T("lobby.menu.PLAY", "开始战斗"))}</div>
|
||||
<h1>${Esc(Translations.T("lobby.menu.PLAY", "开始战斗"))}</h1>
|
||||
<p>快速匹配一场战斗,或从战斗列表中选择模式与地图。</p>
|
||||
<div class="hero-actions">
|
||||
<button class="primary big" data-go="battles">${Esc(Translations.T("lobby.battle.pick.SEARCH_BATTLE", "搜索一场战斗"))}</button>
|
||||
<button class="ghost big" data-go="garage">${Esc(Translations.T("lobby.menu.GARAGE", "车库"))}</button>
|
||||
</div>
|
||||
${if (Connected) "" else """
|
||||
<p class="warn">当前未连接服务器:点右上角「连接」填入中继地址后即可接收真实大厅数据。</p>
|
||||
"""}
|
||||
</div>
|
||||
<div class="hero-art"></div>
|
||||
</section>
|
||||
|
||||
<div class="cards">
|
||||
${HomeCard("quests", "iconLobbyMenuQuests.54801b8b.svg", Translations.T("lobby.menu.QUESTS", "任务"), "每日 / 每周任务与星级挑战")}
|
||||
${HomeCard("shop", "iconLobbyMenuStore.e755fe14.svg", Translations.T("lobby.menu.SHOP", "商店"), "水晶 / 坦克币 / 礼盒")}
|
||||
${HomeCard("garage", "iconLobbyMenuGarage.0b623165.svg", Translations.T("lobby.menu.GARAGE", "车库"), "装备、皮肤与强化")}
|
||||
</div>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun HomeCard(Nav: String, Icon: String, Title: String, Text: String): String = """
|
||||
<button class="card" data-go="$Nav">
|
||||
<img src="${Asset("static/images/$Icon")}" alt="">
|
||||
<div><b>${Esc(Title)}</b><span>${Esc(Text)}</span></div>
|
||||
</button>
|
||||
""".trimIndent()
|
||||
|
||||
private fun Battles(): String {
|
||||
val Modes = listOf(
|
||||
Triple("TEAM_MODE", "快速战斗", "快速加入战斗,系统将随机分配阵营与地图。"),
|
||||
Triple("DEATHMATCH", "死亡混战", "竞技场中的每个玩家都是您的敌人。"),
|
||||
Triple("CAPTURE_THE_FLAG", "互夺军旗", "夺取最多旗子的队伍获胜。"),
|
||||
Triple("CONTROL_POINTS", "控制点", "占领敌方点位并阻止敌人占领。"),
|
||||
Triple("JUGGERNAUT", "剑圣", "摧毁尽可能多的坦克。"),
|
||||
Triple("SIEGE", "围城大战", "控制一个点并使敌人远离它。"),
|
||||
)
|
||||
return """
|
||||
<section class="section-head"><h2>${Esc(Translations.T("lobby.battle.list.TITLE", "战场列表"))}</h2>
|
||||
<button class="primary" data-go="garage">${Esc(Translations.T("lobby.menu.GARAGE", "车库"))}</button>
|
||||
</section>
|
||||
<div class="mode-grid">
|
||||
${Modes.joinToString("") { (Key, Name, Desc) -> """
|
||||
<button class="mode-card" data-go="battles">
|
||||
<b>${Esc(Translations.T("lobby.battle.pick.${Key}_NAME", Name))}</b>
|
||||
<span>${Esc(Translations.T("lobby.battle.pick.${Key}_DESCRIPTION", Desc))}</span>
|
||||
</button>
|
||||
""" }}
|
||||
</div>
|
||||
<p class="hint">战斗创建 / 匹配的完整链路属于 S4;这里先接真实状态里的战斗列表与模式文案。</p>
|
||||
""".trimIndent()
|
||||
else -> ""
|
||||
}
|
||||
|
||||
private fun Garage(): String {
|
||||
if (!UiCatalog.Loaded) {
|
||||
UiCatalog.Ensure(Client) { Refresh() }
|
||||
return """
|
||||
<section class="section-head"><h2>${Esc(Translations.T("lobby.menu.GARAGE", "车库"))}</h2></section>
|
||||
<div class="empty" id="garage-loading">正在加载物品目录…</div>
|
||||
""".trimIndent()
|
||||
return """<div class="ms-screen"><div class="ms-empty">正在加载物品目录…</div></div>"""
|
||||
}
|
||||
return """<section class="garage-host"><div id="garage-root"></div></section>"""
|
||||
return """<div class="ms-screen ms-garage-screen"><div id="garage-root"></div></div>"""
|
||||
}
|
||||
|
||||
private fun Shop(): String {
|
||||
val All = Client.Router.Catalog.Items.values.filter { it.buyable }
|
||||
val Shown = All.take(96)
|
||||
return """
|
||||
<section class="section-head"><h2>${Esc(Translations.T("lobby.menu.SHOP", "商店"))}
|
||||
<small>${All.size} 件可购买</small></h2></section>
|
||||
${if (All.isEmpty()) """
|
||||
<div class="empty">离线目录里暂无可购买物品,连接服务器后会自动填充。</div>
|
||||
""" else """
|
||||
<div class="item-grid">${Shown.joinToString("") { ItemCard(it) }}</div>
|
||||
"""}
|
||||
<div class="ms-screen">
|
||||
<h2 class="ms-screen-title">${LEsc(Translations.T("lobby.menu.SHOP", "商店"))}<small>${All.size} 件可购买</small></h2>
|
||||
<div class="ms-empty">${if (All.isEmpty()) "离线目录里暂无可购买物品。" else "商店卡片将在下一轮迁移。"}</div>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun Containers(): String {
|
||||
val All = Client.Router.Catalog.Items.values.filter { ItemCategory(it) == 13 }
|
||||
val N = Client.Router.Catalog.Items.values.count { it.itemCategory == 13 }
|
||||
return """
|
||||
<section class="section-head"><h2>${Esc(Translations.T("lobby.menu.CONTAINERS", "幸运礼盒"))}
|
||||
<small>${All.size}</small></h2></section>
|
||||
${if (All.isEmpty()) "<div class=\"empty\">没有礼盒数据。</div>"
|
||||
else "<div class=\"item-grid\">" + All.take(60).joinToString("") { ItemCard(it) } + "</div>"}
|
||||
<div class="ms-screen">
|
||||
<h2 class="ms-screen-title">${LEsc(Translations.T("lobby.menu.CONTAINERS", "幸运礼盒"))}<small>$N</small></h2>
|
||||
<div class="ms-empty">礼盒开启动画与掉落表待接入。</div>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun Quests(): String = """
|
||||
<section class="section-head"><h2>${Esc(Translations.T("lobby.quests.navigation.HEADER", "任务"))}</h2></section>
|
||||
<div class="cards">
|
||||
${HomeCard("quests", "iconLobbyMenuQuests.54801b8b.svg", Translations.T("lobby.quests.navigation.MAIN", "主要"), "主线任务")}
|
||||
${HomeCard("quests", "iconLobbyMenuQuests.54801b8b.svg", Translations.T("lobby.quests.navigation.DAILY", "每日"), "每日任务")}
|
||||
${HomeCard("quests", "iconLobbyMenuQuests.54801b8b.svg", Translations.T("lobby.quests.navigation.WEEKLY", "每周"), "每周任务")}
|
||||
${HomeCard("quests", "iconLobbyMenuQuests.54801b8b.svg", Translations.T("lobby.quests.navigation.CHALLENGES", "星级挑战"), "星际挑战")}
|
||||
private fun Battles(): String = """
|
||||
<div class="ms-screen">
|
||||
<h2 class="ms-screen-title">${LEsc(Translations.T("lobby.battle.list.TITLE", "战场列表"))}</h2>
|
||||
<div class="ms-empty">战斗选择 / 匹配属于 S4;当前先接入真实大厅外壳。</div>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
|
||||
private fun Friends(): String = """
|
||||
<section class="section-head"><h2>${Esc(Translations.T("lobby.friends.TITLE", "好友"))}</h2></section>
|
||||
<div class="empty">好友列表将在社交链路补齐后接入。</div>
|
||||
private fun Quests(): String = """
|
||||
<div class="ms-screen">
|
||||
<h2 class="ms-screen-title">${LEsc(Translations.T("lobby.quests.navigation.HEADER", "任务"))}</h2>
|
||||
<div class="ms-empty">任务列表将在下一轮迁移。</div>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
|
||||
private fun Chat(): String {
|
||||
val Lines = Client.Logs.takeLast(20).joinToString("") { "<div class=\"ms-log-line\">${LEsc(it)}</div>" }
|
||||
return """
|
||||
<div class="ms-screen">
|
||||
<h2 class="ms-screen-title">${LEsc(Translations.T("lobby.menu.CHAT", "聊天"))}</h2>
|
||||
<div class="ms-chat-box">${if (Lines.isEmpty()) "<span class=\"muted\">连接后显示服务器日志</span>" else Lines}</div>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun News(): String {
|
||||
val Items = Client.Router.Store.State.News.News
|
||||
return """
|
||||
<section class="section-head"><h2>${Esc(Translations.T("lobby.menu.NEWS", "新闻"))}</h2></section>
|
||||
${if (Items.isEmpty()) "<div class=\"empty\">暂无新闻。</div>"
|
||||
else "<div class=\"news-list\">" + Items.take(20).joinToString("") { N ->
|
||||
"<article class=\"news\"><b>${Esc(N.header)}</b><p>${Esc(N.description)}</p></article>"
|
||||
<div class="ms-screen">
|
||||
<h2 class="ms-screen-title">${LEsc(Translations.T("lobby.menu.NEWS", "新闻"))}</h2>
|
||||
${if (Items.isEmpty()) "<div class=\"ms-empty\">暂无新闻。</div>"
|
||||
else "<div class=\"ms-news-list\">" + Items.take(20).joinToString("") { N ->
|
||||
"<article class=\"ms-news\"><b>${LEsc(N.header)}</b><p>${LEsc(N.description)}</p></article>"
|
||||
} + "</div>"}
|
||||
</div>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun Settings(): String = """
|
||||
<section class="section-head"><h2>${Esc(Translations.T("lobby.menu.OPTIONS", "设置"))}</h2></section>
|
||||
<div class="cards">
|
||||
<div class="card static"><div><b>连接</b><span>大厅 / 空间中继地址</span></div></div>
|
||||
<div class="card static"><div><b>语言</b><span>简体中文(运行时读取官方 zh-CN)</span></div></div>
|
||||
<div class="card static"><div><b>诊断</b><span>帧 ${Client.Frames},记录 ${Client.Records},未覆盖 ${Client.Router.Unmapped}</span></div></div>
|
||||
<div class="ms-screen">
|
||||
<h2 class="ms-screen-title">${LEsc(Translations.T("lobby.menu.OPTIONS", "设置"))}</h2>
|
||||
<div class="ms-empty">设置页将在下一轮迁移(游戏 / 画面 / 声音 / 操控 / 账户 …)。</div>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
|
||||
private fun Placeholder(Title: String, Text: String): String = """
|
||||
<section class="section-head"><h2>${Esc(Title)}</h2></section>
|
||||
<div class="empty">${Esc(Text)}</div>
|
||||
<div class="ms-screen">
|
||||
<h2 class="ms-screen-title">${LEsc(Title)}</h2>
|
||||
<div class="ms-empty">${LEsc(Text)}</div>
|
||||
</div>
|
||||
""".trimIndent()
|
||||
|
||||
private fun Chat(): String {
|
||||
val Lines = Client.Logs.takeLast(12).joinToString("") { "<div class=\"log-line\">${Esc(it)}</div>" }
|
||||
return """
|
||||
<div class="chat-head">
|
||||
<span>${Esc(Translations.T("lobby.menu.CHAT", "聊天"))}</span>
|
||||
<span class="muted">${Client.Status}</span>
|
||||
</div>
|
||||
<div class="chat-box">${if (Lines.isEmpty()) "<span class=\"muted\">连接后显示服务器日志</span>" else Lines}</div>
|
||||
<div class="chat-input"><input placeholder="聊天暂未开放" disabled><button disabled>发送</button></div>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun ItemCard(Item: GarageItemObject): String {
|
||||
val Rarity = ItemRarity(Item)
|
||||
val Color = RarityColors.getOrElse(Rarity) { RarityColors[0] }
|
||||
val Name = ItemName(Item)
|
||||
val Category = CategoryLabel(ItemCategory(Item))
|
||||
val Premium = Item.isPremium
|
||||
val Count = Item.count
|
||||
return """
|
||||
<article class="item" style="--rarity:$Color">
|
||||
<div class="item-art"><span>${Esc(Name.take(1))}</span></div>
|
||||
<div class="item-body">
|
||||
<b title="${Esc(Name)}">${Esc(Name)}</b>
|
||||
<span class="muted">${Esc(Category)}${if (Count > 1) " · x$Count" else ""}</span>
|
||||
${if (Premium) "<span class=\"badge\">VIP</span>" else ""}
|
||||
</div>
|
||||
</article>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun SetHtml(Id: String, Html: String) {
|
||||
val El = Doc.getElementById(Id) ?: return
|
||||
val El = LDoc.getElementById(Id) ?: return
|
||||
El.innerHTML = Html
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,13 @@
|
||||
// GarageScreen.kt —— React 车库(大厅第一个正式迁移的界面)
|
||||
// GarageScreen.kt —— React 车库(按线上 GarageCommon / TankParameters / MountedItems / Supplies 布局)
|
||||
//
|
||||
// 数据:UiCatalog(真实 ItemCatalog 或抓包导出的 fixture)。
|
||||
// 结构对照原版:分类页签 + 物品网格 + 右侧详情 / 3D 预览位。
|
||||
// 线上结构(1296×1006):
|
||||
// header 78px(返回 / 车库标题 / 货币 / 退出)
|
||||
// GarageCommonStyle-garageContainer 928px
|
||||
// 左:TankParametersStyle-leftParamsContainer(GS + 参数,293×209)
|
||||
// 中:GarageMainScreenStyle-blockParameters(3D 坦克,1140×685)
|
||||
// 右:MountedItemsStyle-container(无人机 / 手榴弹 / 炮塔 / 底盘,298×492)
|
||||
// 底:SuppliesComponentStyle-commonBlockSupplies(1140×125)
|
||||
// 当前 3D 用占位画布,物品选择先用抽屉保留目录浏览。
|
||||
package App.Browser.Ui
|
||||
|
||||
import App.Browser.Translations
|
||||
@@ -14,30 +20,41 @@ import react.dom.html.ReactHTML.aside
|
||||
import react.dom.html.ReactHTML.b
|
||||
import react.dom.html.ReactHTML.button
|
||||
import react.dom.html.ReactHTML.div
|
||||
import react.dom.html.ReactHTML.h2
|
||||
import react.dom.html.ReactHTML.h3
|
||||
import react.dom.html.ReactHTML.header
|
||||
import react.dom.html.ReactHTML.i
|
||||
import react.dom.html.ReactHTML.img
|
||||
import react.dom.html.ReactHTML.input
|
||||
import react.dom.html.ReactHTML.section
|
||||
import react.dom.html.ReactHTML.small
|
||||
import react.dom.html.ReactHTML.span
|
||||
import react.useState
|
||||
import web.dom.document
|
||||
import web.html.HTMLDivElement
|
||||
|
||||
/** 大厅把当前用户/回退动作放进这个上下文,React 组件直接读。 */
|
||||
object GarageContext {
|
||||
var Uid: String = ""
|
||||
var Rank: Int = 1
|
||||
var GearScore: Int = 0
|
||||
var Crystals: Int = 0
|
||||
var Rubies: Int = 0
|
||||
var OnBack: () -> Unit = {}
|
||||
}
|
||||
|
||||
private val RarityNames = listOf("普通", "罕见", "稀有", "史诗", "传奇", "特殊")
|
||||
|
||||
private fun RarityName(R: Int): String = RarityNames.getOrElse(R) { "普通" }
|
||||
|
||||
/** kotlin-react 19 的 className 是 ClassName 类型;直接走 DOM 属性避免类型转换样板。 */
|
||||
private fun HTMLAttributes<*>.Css(Value: String) {
|
||||
val E = this.asDynamic()
|
||||
E.className = Value
|
||||
}
|
||||
|
||||
private fun Asset(Path: String): String = "/play/" + Path
|
||||
|
||||
private val GarageScreen = FC<Props> {
|
||||
var Category by useState(-1)
|
||||
var Query by useState("")
|
||||
var Selected by useState<CatalogItem?>(null)
|
||||
var Drawer by useState(false)
|
||||
|
||||
val All = UiCatalog.Items
|
||||
val Counts = GarageCategories.associate { C -> C.Id to All.count { it.Category == C.Id } }
|
||||
@@ -48,104 +65,165 @@ private val GarageScreen = FC<Props> {
|
||||
}
|
||||
val Shown = Filtered.take(120)
|
||||
|
||||
section {
|
||||
Css("garage")
|
||||
fun FirstOf(Cat: Int): CatalogItem? = All.firstOrNull { it.Category == Cat }
|
||||
val Hull = FirstOf(1)
|
||||
val Turret = FirstOf(0)
|
||||
val Paint = FirstOf(2)
|
||||
val Drone = FirstOf(14)
|
||||
val Grenade = FirstOf(18)
|
||||
|
||||
div {
|
||||
Css("garage-head")
|
||||
h2 {
|
||||
+Translations.T("lobby.menu.GARAGE", "车库")
|
||||
small { +"${Filtered.size} / ${All.size}" }
|
||||
div {
|
||||
Css("g2")
|
||||
|
||||
header {
|
||||
Css("g2-head")
|
||||
button {
|
||||
Css("g2-back")
|
||||
onClick = { GarageContext.OnBack() }
|
||||
i { Css("g2-back-icon") }
|
||||
}
|
||||
input {
|
||||
Css("garage-search")
|
||||
placeholder = "搜索物品"
|
||||
value = Query
|
||||
onChange = { E -> Query = E.target.value }
|
||||
div { Css("g2-title"); +Translations.T("lobby.menu.GARAGE", "车库") }
|
||||
div {
|
||||
Css("g2-currencies")
|
||||
span { img { src = Asset("static/images/ruby.ce62efbc.svg"); Css("coin") }; +"${GarageContext.Rubies}" }
|
||||
span { img { src = Asset("static/images/crystalSmall.242e6c15.svg"); Css("coin") }; +"${GarageContext.Crystals}" }
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
Css("garage-tabs")
|
||||
for (C in GarageCategories) {
|
||||
val N = Counts[C.Id] ?: 0
|
||||
if (N == 0) continue
|
||||
button {
|
||||
Css("garage-tab" + if (C.Id == ActiveCategory) " active" else "")
|
||||
onClick = { Category = C.Id }
|
||||
+C.Label
|
||||
span { +"$N" }
|
||||
Css("g2-body")
|
||||
|
||||
aside {
|
||||
Css("g2-left")
|
||||
div {
|
||||
Css("g2-gs")
|
||||
img { src = Asset("static/images/iconGSSmall.0c0aa8fa.svg") }
|
||||
b { +"${GarageContext.GearScore}" }
|
||||
}
|
||||
div { Css("g2-param"); span { +"最高速度" }; b { +"—" } }
|
||||
div { Css("g2-param"); span { +"伤害" }; b { +"—" } }
|
||||
div { Css("g2-param"); span { +"装填" }; b { +"—" } }
|
||||
div { Css("g2-param"); span { +"护甲" }; b { +"—" } }
|
||||
div {
|
||||
Css("g2-resists")
|
||||
repeat(4) { i -> div { Css("g2-resist"); span { +"+" } } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
Css("garage-main")
|
||||
|
||||
div {
|
||||
Css("garage-grid")
|
||||
for (Item in Shown) {
|
||||
button {
|
||||
Css(
|
||||
"garage-card rarity-${Item.Rarity}" +
|
||||
if (Selected?.Id == Item.Id) " selected" else ""
|
||||
)
|
||||
onClick = { Selected = Item }
|
||||
div { Css("garage-card-art"); +Item.Name.take(1) }
|
||||
div { Css("garage-card-name"); +Item.Name.ifEmpty { "物品 ${Item.Id}" } }
|
||||
div {
|
||||
Css("garage-card-meta")
|
||||
+CategoryLabel(Item.Category)
|
||||
if (Item.Price > 0 && Item.Buyable) {
|
||||
span { Css("garage-price"); +PriceText(Item) }
|
||||
}
|
||||
}
|
||||
if (Item.Premium) span { Css("garage-premium"); +"VIP" }
|
||||
}
|
||||
Css("g2-scene")
|
||||
div {
|
||||
Css("g2-scene-art")
|
||||
+((Selected ?: Turret ?: Hull)?.Name?.take(1) ?: "T")
|
||||
}
|
||||
if (Shown.isEmpty()) {
|
||||
div { Css("garage-empty"); +"这个分类暂时没有物品。" }
|
||||
div {
|
||||
Css("g2-scene-caption")
|
||||
+((Selected ?: Turret ?: Hull)?.Name ?: "3D 坦克预览")
|
||||
span { +"3D 加载器接入中" }
|
||||
}
|
||||
button {
|
||||
Css("g2-drawer-toggle")
|
||||
onClick = { Drawer = !Drawer }
|
||||
+"物品"
|
||||
}
|
||||
}
|
||||
|
||||
aside {
|
||||
Css("garage-detail")
|
||||
val Item = Selected
|
||||
if (Item == null) {
|
||||
h3 { +"选择一件物品" }
|
||||
div { Css("garage-preview") }
|
||||
div { Css("garage-preview-label"); +"3D 预览加载器接入中" }
|
||||
} else {
|
||||
h3 { +Item.Name.ifEmpty { "物品 ${Item.Id}" } }
|
||||
div { Css("garage-preview") }
|
||||
div { Css("garage-preview-label"); +"3D 预览加载器接入中" }
|
||||
div { Css("garage-props") }
|
||||
Prop("分类", CategoryLabel(Item.Category))
|
||||
Prop("稀有度", RarityName(Item.Rarity))
|
||||
Prop("物品 ID", Item.Id.toString())
|
||||
if (Item.Count > 0) Prop("数量", Item.Count.toString())
|
||||
if (Item.Buyable) Prop("价格", PriceText(Item))
|
||||
if (Item.Premium) Prop("VIP", "是")
|
||||
Css("g2-right")
|
||||
MountedBlock("无人机", Drone)
|
||||
MountedBlock("手榴弹", Grenade)
|
||||
MountedBlock("炮塔", Turret)
|
||||
MountedBlock("底盘", Hull)
|
||||
}
|
||||
}
|
||||
|
||||
div {
|
||||
Css("g2-supplies")
|
||||
Supply("Repair.13e5e240.svg", "维修")
|
||||
Supply("armor.b0b6d887.svg", "护甲")
|
||||
Supply("damage.af4f0289.svg", "伤害")
|
||||
Supply("Speed.3b207b8e.svg", "速度")
|
||||
Supply("mine.5a46d3d0.svg", "地雷")
|
||||
Supply("GoldBox.61e0017c.svg", "金箱")
|
||||
}
|
||||
|
||||
if (Drawer) {
|
||||
div {
|
||||
Css("g2-drawer")
|
||||
div {
|
||||
Css("g2-drawer-head")
|
||||
b { +"物品目录" }
|
||||
input {
|
||||
Css("garage-search")
|
||||
placeholder = "搜索物品"
|
||||
value = Query
|
||||
onChange = { E -> Query = E.target.value }
|
||||
}
|
||||
button { Css("g2-drawer-close"); onClick = { Drawer = false }; +"关闭" }
|
||||
}
|
||||
div {
|
||||
Css("garage-tabs")
|
||||
for (C in GarageCategories) {
|
||||
val N = Counts[C.Id] ?: 0
|
||||
if (N == 0) continue
|
||||
button {
|
||||
Css("garage-tab" + if (C.Id == ActiveCategory) " active" else "")
|
||||
onClick = { Category = C.Id }
|
||||
+C.Label
|
||||
span { +"$N" }
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
Css("garage-grid g2-drawer-grid")
|
||||
for (Item in Shown) {
|
||||
button {
|
||||
Css("garage-card rarity-${Item.Rarity}" + if (Selected?.Id == Item.Id) " selected" else "")
|
||||
onClick = { Selected = Item }
|
||||
div { Css("garage-card-art"); +Item.Name.take(1) }
|
||||
div { Css("garage-card-name"); +Item.Name.ifEmpty { "物品 ${Item.Id}" } }
|
||||
div {
|
||||
Css("garage-card-meta")
|
||||
+CategoryLabel(Item.Category)
|
||||
if (Item.Price > 0 && Item.Buyable) span { Css("garage-price"); +PriceText(Item) }
|
||||
}
|
||||
if (Item.Premium) span { Css("garage-premium"); +"VIP" }
|
||||
}
|
||||
}
|
||||
if (Shown.isEmpty()) div { Css("garage-empty"); +"这个分类暂时没有物品。" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun react.ChildrenBuilder.MountedBlock(Title: String, Item: CatalogItem?) {
|
||||
div {
|
||||
Css("g2-mounted")
|
||||
div { Css("g2-mounted-title"); +Title }
|
||||
div {
|
||||
Css("g2-mounted-art rarity-${Item?.Rarity ?: 0}")
|
||||
+(Item?.Name?.take(1) ?: "—")
|
||||
}
|
||||
div { Css("g2-mounted-name"); +(Item?.Name ?: "未装备") }
|
||||
}
|
||||
}
|
||||
|
||||
private fun react.ChildrenBuilder.Supply(Icon: String, Title: String) {
|
||||
div {
|
||||
Css("g2-supply")
|
||||
img { src = Asset("static/images/$Icon") }
|
||||
span { +Title }
|
||||
b { +"0" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun PriceText(Item: CatalogItem): String = when (Item.Currency) {
|
||||
1 -> "${Item.Price} 水晶"
|
||||
2 -> "${Item.Price} 红宝石"
|
||||
else -> "${Item.Price}"
|
||||
}
|
||||
|
||||
private fun react.ChildrenBuilder.Prop(Name: String, Value: String) {
|
||||
div {
|
||||
Css("garage-prop")
|
||||
span { +Name }
|
||||
b { +Value }
|
||||
}
|
||||
}
|
||||
|
||||
object GarageMount {
|
||||
private var Root: dynamic = null
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
/* lobby.css —— 大厅骨架(对齐 Tanki 的深色金属 + 橙色强调) */
|
||||
/* lobby.css —— 大厅主界面(尺寸/颜色对照 3dtank.com/play/ 线上计算样式)
|
||||
|
||||
线上根字号 12.96px(.ksc-37),这里用同样的 max(min(1.48148vh, 1vw), 3px),
|
||||
因此 6em = 78px、21.875em = 284px、22.5em = 292px,与真实 DOM 一致。 */
|
||||
@font-face {
|
||||
font-family: BaseFontRegular;
|
||||
src: url("/play/static/fonts/Rubik-Regular.7e001ec0.woff") format("woff");
|
||||
@@ -12,389 +15,351 @@
|
||||
src: url("/play/static/fonts/Rubik-Bold.26ae1fce.woff") format("woff");
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg0: #05080d;
|
||||
--bg1: #0b1725;
|
||||
--bg2: #122436;
|
||||
--panel: rgba(16, 28, 42, 0.88);
|
||||
--line: #22394f;
|
||||
--text: #e8eef6;
|
||||
--muted: #8ea3b8;
|
||||
--accent: #ffb300;
|
||||
--accent2: #ff7a18;
|
||||
--blue: #3f9fe0;
|
||||
--danger: #e05f5f;
|
||||
--radius: 10px;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
html, body, #app { height: 100%; }
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
body { margin: 0; overflow: hidden; background: #001926; color: #fff; }
|
||||
|
||||
.lobby {
|
||||
.ms-root {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: var(--text);
|
||||
font-family: BaseFontRegular, "Segoe UI", system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
background: var(--bg0);
|
||||
font-size: max(min(1.48148vh, 1vw), 3px);
|
||||
font-family: BaseFontRegular, "Segoe UI", sans-serif;
|
||||
color: #fff;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.lobby-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
#lobby-canvas {
|
||||
position: absolute; inset: 0; width: 100%; height: 100%;
|
||||
display: block; background: #001926;
|
||||
}
|
||||
.ms-bg {
|
||||
position: absolute; inset: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(0, 12, 20, 0.55), rgba(0, 10, 18, 0.82)),
|
||||
linear-gradient(180deg, rgba(0, 25, 38, 0.35), rgba(0, 12, 20, 0.55)),
|
||||
url("/play/static/images/background.fc2779c1.webp") center / cover no-repeat;
|
||||
filter: saturate(0.85);
|
||||
filter: saturate(0.9) brightness(0.8);
|
||||
}
|
||||
.lobby > *:not(.lobby-bg) { position: relative; z-index: 1; }
|
||||
|
||||
button, input { font: inherit; color: inherit; }
|
||||
button { cursor: pointer; }
|
||||
img { display: block; }
|
||||
|
||||
/* ---------------- 顶栏 ---------------- */
|
||||
.topbar {
|
||||
display: flex; align-items: center; gap: 18px;
|
||||
height: 72px; padding: 0 22px;
|
||||
background: linear-gradient(180deg, rgba(5, 14, 22, 0.82), rgba(5, 10, 16, 0.15));
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
/* ---------------- 顶栏 6em ---------------- */
|
||||
.ms-top {
|
||||
position: absolute; top: 0; left: 0;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
width: 100%; height: 6em; z-index: 2;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: linear-gradient(180deg, rgba(0, 12, 20, 0.55), rgba(0, 12, 20, 0.05));
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 10px; min-width: 190px; }
|
||||
.brand .logo { width: 38px; height: 38px; }
|
||||
.brand span { letter-spacing: 0.14em; font-weight: 700; font-size: 14px; color: #fff; font-family: BaseFontBold, sans-serif; }
|
||||
.brand b { color: var(--accent); margin-left: 4px; }
|
||||
|
||||
.stats { display: flex; align-items: center; justify-content: space-between; gap: 18px; flex: 1; }
|
||||
.user-block { display: flex; align-items: center; gap: 10px; }
|
||||
.rank-badge {
|
||||
width: 44px; height: 44px; border-radius: 50%;
|
||||
.ms-top-left { display: flex; align-items: center; height: 100%; }
|
||||
.rank-icon {
|
||||
width: 5.9375em; height: 6em; display: flex; align-items: center; justify-content: center;
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.25); cursor: pointer;
|
||||
}
|
||||
.rank-icon:hover { background-color: rgba(255, 255, 255, 0.05); }
|
||||
.rank-icon img { width: 4.375em; height: 4.375em; }
|
||||
.user-progress {
|
||||
display: flex; flex-direction: column; justify-content: center;
|
||||
min-width: 10em; margin-left: 1em; height: 3.75em;
|
||||
}
|
||||
.user-name {
|
||||
font: 500 1.1em/1.2 BaseFontMedium, sans-serif;
|
||||
text-shadow: 0 0 0.25em rgba(0, 0, 0, 0.5); cursor: pointer;
|
||||
}
|
||||
.user-name:hover { color: #ffbc09; }
|
||||
.xp-bar {
|
||||
width: 12.5em; height: 0.25em; margin-top: 0.375em;
|
||||
background: rgba(118, 255, 51, 0.25); border-radius: 0.375em; overflow: hidden;
|
||||
}
|
||||
.xp-bar i { display: block; height: 100%; background: rgba(118, 255, 51, 0.8); border-radius: inherit; }
|
||||
.xp-text {
|
||||
display: flex; align-items: center; margin-top: 0.35em;
|
||||
font: 500 0.9375em/1 BaseFontMedium, sans-serif; color: #76ff33;
|
||||
text-shadow: 0 0 0.25em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.xp-text span { color: rgba(255, 255, 255, 0.35); margin-left: 0.2em; }
|
||||
.xp-icon {
|
||||
width: 0.85em; height: 0.85em; margin-right: 0.25em;
|
||||
background: url("/play/static/images/experience.402435da.svg") center / contain no-repeat;
|
||||
}
|
||||
.ms-top-logo {
|
||||
position: absolute; left: 0; right: 0; margin: auto;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font: 700 18px/1 BaseFontBold, sans-serif; color: #241800;
|
||||
background: radial-gradient(circle at 35% 30%, #ffe08a, #e59a00 70%);
|
||||
border: 2px solid rgba(255, 255, 255, 0.55);
|
||||
box-shadow: 0 0 16px rgba(255, 179, 0, 0.35);
|
||||
height: 3.75em; pointer-events: none; opacity: 0.95;
|
||||
}
|
||||
.user-meta b { display: block; font: 600 15px/1.2 BaseFontMedium, sans-serif; color: #fff; }
|
||||
.user-meta span { font-size: 11px; color: rgba(255, 255, 255, 0.55); letter-spacing: 0.04em; }
|
||||
.ms-top-logo img { height: 3.75em; }
|
||||
.ms-top-right { display: flex; align-items: center; justify-content: flex-end; height: 6em; z-index: 5; }
|
||||
.ms-currencies { display: flex; flex-direction: column; justify-content: center; height: 6em; }
|
||||
.ms-currency {
|
||||
position: relative; display: flex; align-items: center; gap: 0.4em;
|
||||
width: 9.4em; height: 3em; padding: 0 0.75em 0 0.6em; cursor: pointer;
|
||||
}
|
||||
.ms-currency .coin { width: 1em; height: 1em; }
|
||||
.ms-currency span { font: 500 1.05em/1 BaseFontMedium, sans-serif; }
|
||||
.ms-currency .plus { width: 1em; height: 1em; margin-left: auto; opacity: 0.85; }
|
||||
.ms-currency:hover { background: rgba(255, 255, 255, 0.06); }
|
||||
.ms-fullscreen {
|
||||
display: flex; align-items: center; justify-content: center; gap: 0.3em;
|
||||
width: 6em; height: 6em; cursor: pointer;
|
||||
}
|
||||
.ms-fullscreen i {
|
||||
width: 1.5em; height: 1.5em; background: #bfd5ff;
|
||||
mask: url("/play/static/images/fullscreen.0b4547f5.svg") center / contain no-repeat;
|
||||
-webkit-mask: url("/play/static/images/fullscreen.0b4547f5.svg") center / contain no-repeat;
|
||||
}
|
||||
.ms-fullscreen b { font: 600 0.75em/1 BaseFontMedium, sans-serif; color: rgba(255, 255, 255, 0.65); }
|
||||
.ms-fullscreen:hover i { background: #fff; }
|
||||
.ms-logout {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 6em; height: 6em; border: 0; background: transparent; cursor: pointer;
|
||||
}
|
||||
.ms-logout i {
|
||||
width: 1.5em; height: 1.5em; background: #bfd5ff;
|
||||
mask: url("/play/static/images/iconBack.62869951.svg") center / contain no-repeat;
|
||||
-webkit-mask: url("/play/static/images/iconBack.62869951.svg") center / contain no-repeat;
|
||||
}
|
||||
.ms-logout:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
.ms-logout:hover i { background: #fff; }
|
||||
|
||||
.currencies { display: flex; align-items: center; gap: 10px; }
|
||||
.stat {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 6px 12px; border-radius: 6px;
|
||||
background: rgba(2, 10, 16, 0.62);
|
||||
border: 1px solid rgba(255, 255, 255, 0.10);
|
||||
color: #eaf3fb; font: 600 14px/1 BaseFontMedium, sans-serif;
|
||||
/* ---------------- 左侧 50% ---------------- */
|
||||
.ms-home { position: absolute; inset: 0; z-index: 1; }
|
||||
.ms-left-gradient {
|
||||
position: absolute; left: 0; top: 0; bottom: 0;
|
||||
display: flex; flex-direction: column; align-items: flex-start;
|
||||
width: 50%; padding-top: 10em;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0.55) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
.stat img { width: 18px; height: 18px; object-fit: contain; }
|
||||
.stat.premium { border-color: rgba(255, 179, 0, 0.5); color: var(--accent); }
|
||||
.stat.muted { color: var(--muted); }
|
||||
.connect-toggle {
|
||||
padding: 8px 18px; border-radius: 6px; font-weight: 700;
|
||||
border: 1px solid #8a5d00;
|
||||
background: linear-gradient(180deg, #ffc93d, #f29500);
|
||||
color: #241800;
|
||||
.ms-play {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 21.875em; height: 10.75em; margin-left: 4.375em;
|
||||
border: 0; border-radius: 0.5rem; cursor: pointer;
|
||||
background: url("/play/static/images/videoplay.79570900.gif") center / cover no-repeat;
|
||||
box-shadow: 0 0 0 0.063em rgba(118, 255, 51, 0.25);
|
||||
}
|
||||
.connect-toggle:hover { filter: brightness(1.07); }
|
||||
.ms-play span {
|
||||
font: 500 2.75em/1 BaseFontMedium, sans-serif; text-transform: uppercase;
|
||||
color: #76ff33; text-shadow: 0 0.15em 0.3em rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
.ms-play:hover { box-shadow: 0 0 0 2px #76ff33; }
|
||||
|
||||
/* ---------------- 主体 ---------------- */
|
||||
.lobby-body { flex: 1; display: flex; min-height: 0; }
|
||||
|
||||
.menu {
|
||||
width: 108px; padding: 16px 8px; display: flex; flex-direction: column; gap: 4px;
|
||||
background: linear-gradient(90deg, rgba(3, 10, 16, 0.78), rgba(3, 10, 16, 0.20) 80%, transparent);
|
||||
.ms-menu {
|
||||
list-style: none; margin: 1.1em 0 0 4.375em; padding: 0;
|
||||
display: flex; flex-direction: column; width: 27em;
|
||||
}
|
||||
.nav {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 4px; width: 100%;
|
||||
padding: 7px 4px; border-radius: 8px; border: 1px solid transparent;
|
||||
background: transparent; color: #c7d7e6; text-align: center;
|
||||
.ms-menu-item {
|
||||
display: flex; align-items: center; height: 4.5em; cursor: pointer;
|
||||
}
|
||||
.nav img { width: 46px; height: 46px; object-fit: contain; filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45)); }
|
||||
.nav span { font-size: 11px; letter-spacing: 0.02em; }
|
||||
.nav:hover { background: rgba(255, 255, 255, 0.07); border-color: rgba(255, 255, 255, 0.12); }
|
||||
.nav.active {
|
||||
background: linear-gradient(180deg, rgba(255, 179, 0, 0.22), rgba(255, 179, 0, 0.04));
|
||||
border-color: rgba(255, 179, 0, 0.45); color: #fff;
|
||||
.ms-menu-item:hover .ms-menu-icon { background-color: #fff; }
|
||||
.ms-menu-item:hover span { color: #ffbc09; }
|
||||
.ms-menu-icon {
|
||||
width: 3.5em; height: 3.5em; margin-left: 0.625em; background-color: #bfd5ff;
|
||||
mask-position: center; mask-repeat: no-repeat; mask-size: contain;
|
||||
-webkit-mask-position: center; -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain;
|
||||
transition: background-color 0.15s;
|
||||
}
|
||||
.nav.active img { filter: drop-shadow(0 0 10px rgba(255, 179, 0, 0.55)); }
|
||||
|
||||
.content { flex: 1; min-width: 0; padding: 20px 24px; overflow: auto; }
|
||||
.side {
|
||||
width: 310px; padding: 14px; display: flex; flex-direction: column; gap: 12px;
|
||||
border-left: 1px solid var(--line); background: rgba(8, 14, 22, 0.55); overflow: auto;
|
||||
.ms-menu-icon.icon-containers { mask-image: url("/play/static/images/iconLobbyMenuContainers.f960290f.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuContainers.f960290f.svg"); }
|
||||
.ms-menu-icon.icon-quests { mask-image: url("/play/static/images/iconLobbyMenuQuests.54801b8b.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuQuests.54801b8b.svg"); }
|
||||
.ms-menu-icon.icon-garage { mask-image: url("/play/static/images/iconLobbyMenuGarage.0b623165.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuGarage.0b623165.svg"); }
|
||||
.ms-menu-icon.icon-shop { mask-image: url("/play/static/images/iconLobbyMenuStore.e755fe14.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuStore.e755fe14.svg"); }
|
||||
.ms-menu-icon.icon-settings { mask-image: url("/play/static/images/iconLobbyMenuSettings.cc00d08a.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuSettings.cc00d08a.svg"); }
|
||||
.ms-menu-item span {
|
||||
margin-left: 0.5625em; font: 500 2em/1 BaseFontMedium, sans-serif;
|
||||
text-transform: uppercase; text-shadow: 0 0.25em 0.25em rgba(0, 0, 0, 0.25);
|
||||
transition: color 0.15s;
|
||||
}
|
||||
|
||||
/* ---------------- 通用块 ---------------- */
|
||||
.section-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
||||
.section-head h2 { margin: 0; font-size: 22px; letter-spacing: 0.02em; }
|
||||
.section-head h2 small { margin-left: 10px; color: var(--muted); font-size: 13px; font-weight: 500; }
|
||||
.search {
|
||||
width: 240px; padding: 8px 12px; border-radius: 8px;
|
||||
border: 1px solid var(--line); background: rgba(4, 8, 14, 0.8); color: var(--text); outline: none;
|
||||
.ms-footer { flex: 1; display: flex; align-items: flex-end; width: 100%; }
|
||||
.ms-footer ul {
|
||||
display: flex; align-items: center; list-style: none;
|
||||
margin: 0 0 0.8em 5em; padding: 0; height: 5.0625em;
|
||||
}
|
||||
.search:focus { border-color: var(--blue); }
|
||||
.hint { color: var(--muted); font-size: 12px; }
|
||||
.muted { color: var(--muted); }
|
||||
.small { font-size: 12px; }
|
||||
.warn { color: var(--accent); font-size: 13px; }
|
||||
.empty {
|
||||
padding: 38px 20px; text-align: center; color: var(--muted);
|
||||
border: 1px dashed var(--line); border-radius: var(--radius); background: rgba(8, 14, 22, 0.4);
|
||||
.ms-footer-item {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 5em; height: 5em; cursor: pointer;
|
||||
}
|
||||
.ms-footer-item:hover { border-radius: 6.25em; box-shadow: 0 0 0 0.125em #fff; }
|
||||
.ms-footer-icon {
|
||||
width: 2em; height: 2em; background: #bfd5ff;
|
||||
mask-position: center; mask-repeat: no-repeat; mask-size: contain;
|
||||
-webkit-mask-position: center; -webkit-mask-repeat: no-repeat; -webkit-mask-size: contain;
|
||||
}
|
||||
.ms-footer-item:hover .ms-footer-icon { background: #fff; }
|
||||
.ms-footer-icon.icon-news { mask-image: url("/play/static/images/iconLobbyMenuNews.6efe9ae3.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuNews.6efe9ae3.svg"); }
|
||||
.ms-footer-icon.icon-chat { mask-image: url("/play/static/images/iconLobbyMenuChat.e48a39a4.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuChat.e48a39a4.svg"); }
|
||||
.ms-footer-icon.icon-friends { mask-image: url("/play/static/images/iconLobbyMenuFriends.28305317.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuFriends.28305317.svg"); }
|
||||
.ms-footer-icon.icon-clans { mask-image: url("/play/static/images/iconLobbyMenuClan.f01ae646.svg"); -webkit-mask-image: url("/play/static/images/iconLobbyMenuClan.f01ae646.svg"); }
|
||||
|
||||
.primary, .ghost {
|
||||
padding: 9px 16px; border-radius: 8px; font-weight: 700;
|
||||
/* ---------------- 右侧 ---------------- */
|
||||
.ms-right-gradient {
|
||||
position: absolute; right: 0; top: 0; bottom: 0; width: 27.75em;
|
||||
background: linear-gradient(90deg, #000 0%, rgba(0, 0, 0, 0) 100%);
|
||||
opacity: 0.75; transform: rotate(180deg); pointer-events: none;
|
||||
}
|
||||
.primary { background: linear-gradient(180deg, #ffc93d, #f29500); border: 1px solid #8a5d00; color: #241800; }
|
||||
.ghost { background: rgba(63, 159, 224, 0.10); border: 1px solid rgba(63, 159, 224, 0.4); color: #cfe6ff; }
|
||||
.big { padding: 13px 26px; font-size: 15px; }
|
||||
.primary:hover, .ghost:hover { filter: brightness(1.08); }
|
||||
|
||||
/* ---------------- 首页 ---------------- */
|
||||
.hero {
|
||||
position: relative; min-height: 240px; display: flex; align-items: center;
|
||||
padding: 28px 30px; border-radius: 14px; overflow: hidden;
|
||||
border: 1px solid #2a4258;
|
||||
.ms-battle-pass {
|
||||
position: absolute; right: 2.5em; top: 8.5em;
|
||||
width: 22.5em; height: 10em; padding: 0.5em; box-sizing: border-box;
|
||||
background: linear-gradient(0deg, rgba(0, 25, 38, 0.25) 0%, #001926 100%);
|
||||
border-radius: 0.5rem; cursor: pointer; z-index: 1;
|
||||
filter: drop-shadow(0 0.25rem 0.25rem rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
.ms-battle-pass:hover { box-shadow: 0 0 0 0.125em #bfd5ff; }
|
||||
.ms-bp-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
height: 2.75em; font-size: 0.9375em;
|
||||
}
|
||||
.ms-bp-head span { display: flex; align-items: center; gap: 0.3em; }
|
||||
.ms-bp-head img { width: 1em; height: 1em; }
|
||||
.ms-bp-row {
|
||||
display: flex; align-items: center; height: 3.125em; margin: 0 0.3em 0.3em;
|
||||
border: 0.063em solid rgba(255, 255, 255, 0.1); border-radius: 0.3em;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.ms-bp-row i {
|
||||
width: 4em; height: 2.4em; margin: 0 0.5em;
|
||||
background: url("/play/static/images/StarMission.d11624d7.svg") center / contain no-repeat;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.ms-bp-row div { display: flex; flex-direction: column; font-size: 0.9375em; }
|
||||
.ms-bp-row b { font: 500 1em/1.2 BaseFontBold, sans-serif; }
|
||||
.ms-bp-row span { font-size: 0.8em; }
|
||||
.ms-bp-row.locked { opacity: 0.55; }
|
||||
.ms-bp-row.locked i { filter: grayscale(1); }
|
||||
.ms-announce {
|
||||
position: absolute; right: 2.5em; bottom: 2.5em;
|
||||
display: flex; flex-direction: column; width: 22.5em; min-height: 12.5em;
|
||||
background: rgba(0, 25, 38, 0.35); border: 0.063em solid rgba(255, 255, 255, 0.25);
|
||||
border-radius: 0.5rem; overflow: hidden; backdrop-filter: blur(0.78em);
|
||||
}
|
||||
.ms-announce:hover { border-color: #bfd5ff; }
|
||||
.ms-announce-art {
|
||||
height: 9.5em;
|
||||
background:
|
||||
radial-gradient(700px 300px at 85% 40%, rgba(255, 179, 0, 0.18), transparent 60%),
|
||||
linear-gradient(120deg, rgba(16, 38, 60, 0.95), rgba(8, 14, 22, 0.9));
|
||||
radial-gradient(60% 70% at 50% 40%, rgba(255, 179, 0, 0.18), transparent 70%),
|
||||
url("/play/static/images/background.fc2779c1.webp") center / cover no-repeat;
|
||||
}
|
||||
.hero-copy { max-width: 560px; position: relative; z-index: 1; }
|
||||
.eyebrow { color: var(--accent); letter-spacing: 0.25em; font-size: 11px; font-weight: 700; }
|
||||
.hero h1 { margin: 6px 0 8px; font-size: 42px; line-height: 1.05; letter-spacing: 0.02em; }
|
||||
.hero p { margin: 0 0 16px; color: #b9c9da; }
|
||||
.hero-actions { display: flex; gap: 10px; }
|
||||
.hero-art {
|
||||
position: absolute; right: -30px; bottom: -40px; width: 360px; height: 260px;
|
||||
background: radial-gradient(closest-side, rgba(63, 159, 224, 0.28), transparent 70%);
|
||||
filter: blur(2px);
|
||||
.ms-announce-title {
|
||||
padding: 0.7em 0.9em; font: 500 1.125em/1.4 BaseFontMedium, sans-serif;
|
||||
background: linear-gradient(rgba(0, 25, 38, 0), #001926 60%);
|
||||
}
|
||||
|
||||
.cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 12px; margin-top: 14px; }
|
||||
.card {
|
||||
display: flex; align-items: center; gap: 14px; padding: 14px;
|
||||
border-radius: 12px; border: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, rgba(19, 34, 50, 0.9), rgba(9, 16, 25, 0.9));
|
||||
color: var(--text); text-align: left;
|
||||
/* ---------------- 屏幕 / 返回 / 底部信息 ---------------- */
|
||||
.ms-content {
|
||||
position: absolute; inset: 0; z-index: 6;
|
||||
padding: 7em 3em 2.5em; box-sizing: border-box; overflow: auto;
|
||||
background: linear-gradient(180deg, rgba(0, 12, 20, 0.86), rgba(0, 8, 14, 0.94));
|
||||
}
|
||||
.card:hover { border-color: rgba(255, 179, 0, 0.45); transform: translateY(-1px); }
|
||||
.card img { width: 42px; height: 42px; object-fit: contain; }
|
||||
.card b { display: block; font-size: 15px; }
|
||||
.card span { color: var(--muted); font-size: 12px; }
|
||||
.card.static { cursor: default; }
|
||||
.hidden { display: none !important; }
|
||||
.ms-back {
|
||||
position: absolute; top: 6.8em; left: 1.6em; z-index: 8;
|
||||
padding: 0.55em 1.1em; border-radius: 0.4em; cursor: pointer;
|
||||
border: 1px solid rgba(255, 255, 255, 0.25); background: rgba(4, 12, 20, 0.7);
|
||||
color: #cfe0f0; font: 500 0.95em/1 BaseFontMedium, sans-serif;
|
||||
}
|
||||
.ms-back:hover { border-color: #fff; color: #fff; }
|
||||
.ms-screen { max-width: 1120px; margin: 0 auto; }
|
||||
.ms-screen-title { margin: 0 0 1em; font: 600 2em/1.2 BaseFontMedium, sans-serif; }
|
||||
.ms-screen-title small { margin-left: 0.6em; color: rgba(255, 255, 255, 0.5); font-size: 0.55em; font-weight: 400; }
|
||||
.ms-empty {
|
||||
padding: 3em 1.5em; text-align: center; color: rgba(255, 255, 255, 0.55);
|
||||
border: 1px dashed rgba(255, 255, 255, 0.18); border-radius: 0.5em;
|
||||
}
|
||||
.ms-chat-box {
|
||||
height: 24em; overflow: auto; padding: 0.7em; border-radius: 0.5em;
|
||||
background: rgba(0, 8, 14, 0.7); font: 12px/1.55 Consolas, monospace;
|
||||
}
|
||||
.ms-log-line { padding: 0.1em 0; border-bottom: 1px solid rgba(255, 255, 255, 0.06); color: #b8c9da; }
|
||||
.ms-news-list { display: flex; flex-direction: column; gap: 0.8em; }
|
||||
.ms-news {
|
||||
padding: 1em; border-radius: 0.5em; border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(4, 14, 22, 0.6);
|
||||
}
|
||||
.ms-news b { display: block; margin-bottom: 0.4em; }
|
||||
.ms-news p { margin: 0; color: rgba(255, 255, 255, 0.6); font-size: 0.85em; white-space: pre-wrap; }
|
||||
.ms-client-info {
|
||||
position: absolute; right: 0; bottom: 0; height: 1.3rem; z-index: 101;
|
||||
display: flex; align-items: center; justify-content: flex-end;
|
||||
padding-right: 1.8rem; pointer-events: none;
|
||||
font-size: 0.9375em; letter-spacing: 0.0375em; color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.ms-client-info i { margin: 0 0.4rem; font-style: normal; color: rgba(255, 255, 255, 0.6); }
|
||||
|
||||
/* ---------------- 车库 / 商店 ---------------- */
|
||||
.tabs { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; }
|
||||
.tab {
|
||||
padding: 7px 12px; border-radius: 999px; font-size: 13px;
|
||||
border: 1px solid var(--line); background: rgba(8, 14, 22, 0.6); color: #b9c9da;
|
||||
}
|
||||
.tab i { margin-left: 6px; color: var(--muted); font-style: normal; font-size: 11px; }
|
||||
.tab.active { border-color: var(--accent); color: #241800; background: linear-gradient(180deg, #ffc93d, #f29500); }
|
||||
.tab.active i { color: #4a3200; }
|
||||
|
||||
.item-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px;
|
||||
}
|
||||
.item {
|
||||
position: relative; display: flex; flex-direction: column; overflow: hidden;
|
||||
border-radius: 10px; border: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, rgba(18, 31, 45, 0.95), rgba(8, 13, 20, 0.95));
|
||||
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--rarity) 35%, transparent);
|
||||
}
|
||||
.item:hover { transform: translateY(-1px); border-color: var(--rarity); }
|
||||
.item-art {
|
||||
height: 70px; display: flex; align-items: center; justify-content: center;
|
||||
background:
|
||||
radial-gradient(closest-side, color-mix(in srgb, var(--rarity) 45%, transparent), transparent 70%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.06), transparent);
|
||||
}
|
||||
.item-art span { font-size: 28px; font-weight: 800; color: var(--rarity); opacity: 0.9; }
|
||||
.item-body { padding: 8px 10px 10px; display: flex; flex-direction: column; gap: 2px; }
|
||||
.item-body b {
|
||||
font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.badge {
|
||||
position: absolute; top: 6px; right: 6px; padding: 1px 6px; border-radius: 999px;
|
||||
font-size: 10px; font-weight: 800; background: var(--accent); color: #241800;
|
||||
}
|
||||
|
||||
/* ---------------- 战斗 ---------------- */
|
||||
.mode-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); gap: 10px; }
|
||||
.mode-card {
|
||||
text-align: left; padding: 16px; border-radius: 12px;
|
||||
border: 1px solid var(--line); background: linear-gradient(180deg, rgba(18, 31, 45, 0.92), rgba(8, 13, 20, 0.92));
|
||||
}
|
||||
.mode-card:hover { border-color: rgba(63, 159, 224, 0.6); }
|
||||
.mode-card b { display: block; font-size: 16px; margin-bottom: 6px; color: #fff; }
|
||||
.mode-card span { color: var(--muted); font-size: 12px; }
|
||||
|
||||
/* ---------------- 侧栏 ---------------- */
|
||||
.panel {
|
||||
padding: 12px; border-radius: 12px; border: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
}
|
||||
.panel h3 { margin: 0 0 10px; font-size: 14px; color: #dce8f4; letter-spacing: 0.04em; }
|
||||
.chat-box {
|
||||
height: 240px; overflow: auto; padding: 8px; border-radius: 8px;
|
||||
background: rgba(3, 7, 12, 0.75); font: 12px/1.55 Consolas, "Cascadia Mono", monospace;
|
||||
}
|
||||
.log-line { color: #b8c9da; padding: 1px 0; border-bottom: 1px solid rgba(34, 57, 79, 0.35); }
|
||||
.chat-input { display: flex; gap: 6px; margin-top: 8px; }
|
||||
.chat-input input { flex: 1; padding: 6px 8px; border-radius: 6px; border: 1px solid var(--line); background: #0a1119; }
|
||||
.chat-input button { padding: 6px 10px; border-radius: 6px; border: 1px solid var(--line); background: #142434; color: var(--muted); }
|
||||
|
||||
.news-list { display: flex; flex-direction: column; gap: 8px; }
|
||||
.news { padding: 12px; border-radius: 10px; border: 1px solid var(--line); background: rgba(8, 14, 22, 0.55); }
|
||||
.news b { display: block; margin-bottom: 4px; }
|
||||
.news p { margin: 0; color: var(--muted); font-size: 12px; white-space: pre-wrap; }
|
||||
|
||||
/* ---------------- 底部 / 连接面板 ---------------- */
|
||||
.statusbar {
|
||||
height: 28px; display: flex; align-items: center; padding: 0 14px;
|
||||
border-top: 1px solid var(--line); background: rgba(5, 9, 14, 0.9);
|
||||
color: var(--muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.connect-panel {
|
||||
position: fixed; top: 76px; right: 18px; width: 360px; z-index: 30;
|
||||
padding: 16px; border-radius: 12px; border: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, #16283a, #0a121c);
|
||||
box-shadow: 0 18px 50px rgba(0, 0, 0, 0.55);
|
||||
}
|
||||
.connect-panel.hidden { display: none; }
|
||||
.connect-panel h3 { margin: 0 0 12px; }
|
||||
.connect-panel label { display: block; margin-bottom: 10px; color: var(--muted); font-size: 12px; }
|
||||
.connect-panel input {
|
||||
display: block; width: 100%; margin-top: 4px; padding: 8px 10px; border-radius: 7px;
|
||||
border: 1px solid var(--line); background: #070d14; color: var(--text);
|
||||
}
|
||||
.connect-actions { display: flex; gap: 8px; }
|
||||
.connect-actions button {
|
||||
padding: 8px 14px; border-radius: 7px; border: 1px solid var(--line);
|
||||
background: #142434; color: #cfe0f0;
|
||||
}
|
||||
.connect-actions .primary { background: linear-gradient(180deg, #ffc93d, #f29500); border-color: #8a5d00; color: #241800; }
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.side { display: none; }
|
||||
.menu { width: 64px; }
|
||||
.nav span { display: none; }
|
||||
.nav { justify-content: center; }
|
||||
}
|
||||
|
||||
/* ---------------- React 车库 ---------------- */
|
||||
.garage-host { height: 100%; min-height: 0; }
|
||||
/* ---------------- React 车库(尺寸随根字号) ---------------- */
|
||||
.ms-garage-screen { padding-top: 0; }
|
||||
#garage-root { height: calc(100vh - 9.5em); min-height: 28em; }
|
||||
.garage {
|
||||
display: flex; flex-direction: column; gap: 12px; height: 100%; min-height: 0;
|
||||
display: flex; flex-direction: column; gap: 0.9em; height: 100%; min-height: 0;
|
||||
}
|
||||
.garage-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
||||
.garage-head h2 { margin: 0; font: 600 22px/1.2 BaseFontMedium, sans-serif; }
|
||||
.garage-head h2 small { margin-left: 8px; color: var(--muted); font-size: 12px; font-weight: 400; }
|
||||
.garage-head { display: flex; align-items: center; justify-content: space-between; gap: 0.9em; }
|
||||
.garage-head h2 { margin: 0; font: 600 1.7em/1.2 BaseFontMedium, sans-serif; color: #fff; }
|
||||
.garage-head h2 small { margin-left: 0.5em; color: rgba(255, 255, 255, 0.45); font-size: 0.5em; font-weight: 400; }
|
||||
.garage-search {
|
||||
width: 240px; padding: 8px 12px; border-radius: 6px;
|
||||
border: 1px solid rgba(255,255,255,.14); background: rgba(3,10,16,.72); color: #fff; outline: none;
|
||||
width: 18em; padding: 0.55em 0.9em; border-radius: 0.4em;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16); background: rgba(3, 10, 16, 0.72);
|
||||
color: #fff; outline: none; font-size: 0.9em;
|
||||
}
|
||||
.garage-search:focus { border-color: rgba(255,179,0,.6); }
|
||||
.garage-tabs { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.garage-search:focus { border-color: rgba(255, 179, 0, 0.6); }
|
||||
.garage-tabs { display: flex; flex-wrap: wrap; gap: 0.45em; }
|
||||
.garage-tab {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
padding: 6px 12px; border-radius: 5px; font-size: 12px;
|
||||
border: 1px solid rgba(255,255,255,.12); background: rgba(4,12,20,.62); color: #c7d7e6;
|
||||
display: inline-flex; align-items: center; gap: 0.5em;
|
||||
padding: 0.45em 0.9em; border-radius: 0.35em; font-size: 0.85em;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(4, 12, 20, 0.62); color: #c7d7e6;
|
||||
}
|
||||
.garage-tab span { color: var(--muted); font-size: 10px; }
|
||||
.garage-tab:hover { border-color: rgba(255,255,255,.3); }
|
||||
.garage-tab span { color: rgba(255, 255, 255, 0.45); font-size: 0.8em; }
|
||||
.garage-tab:hover { border-color: rgba(255, 255, 255, 0.3); }
|
||||
.garage-tab.active {
|
||||
border-color: #8a5d00; color: #241800; font-weight: 700;
|
||||
background: linear-gradient(180deg, #ffc93d, #f29500);
|
||||
}
|
||||
.garage-tab.active span { color: #4a3200; }
|
||||
.garage-main { flex: 1; min-height: 0; display: flex; gap: 14px; }
|
||||
.garage-main { flex: 1; min-height: 0; display: flex; gap: 1em; }
|
||||
.garage-grid {
|
||||
flex: 1; min-width: 0; overflow: auto; align-content: start;
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(132px, 1fr)); gap: 8px; padding-right: 4px;
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5em, 1fr)); gap: 0.55em; padding-right: 0.3em;
|
||||
}
|
||||
.garage-card {
|
||||
position: relative; display: flex; flex-direction: column; align-items: stretch;
|
||||
padding: 0; border-radius: 7px; overflow: hidden; text-align: left;
|
||||
border: 1px solid rgba(255,255,255,.10);
|
||||
padding: 0; border-radius: 0.4em; overflow: hidden; text-align: left;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-top: 2px solid var(--rarity, #9aa3b2);
|
||||
background: linear-gradient(180deg, rgba(20,34,48,.92), rgba(6,12,18,.94));
|
||||
color: var(--text);
|
||||
background: linear-gradient(180deg, rgba(20, 34, 48, 0.92), rgba(6, 12, 18, 0.94));
|
||||
color: #fff; cursor: pointer;
|
||||
}
|
||||
.garage-card:hover { border-color: var(--rarity, #9aa3b2); transform: translateY(-1px); }
|
||||
.garage-card.selected { box-shadow: 0 0 0 1px var(--rarity, #ffb300), 0 0 18px rgba(255,179,0,.18); }
|
||||
.garage-card.selected { box-shadow: 0 0 0 1px var(--rarity, #ffb300), 0 0 1.2em rgba(255, 179, 0, 0.18); }
|
||||
.garage-card-art {
|
||||
height: 64px; display: flex; align-items: center; justify-content: center;
|
||||
font: 800 26px/1 BaseFontBold, sans-serif; color: var(--rarity, #9aa3b2);
|
||||
height: 4.6em; display: flex; align-items: center; justify-content: center;
|
||||
font: 800 1.9em/1 BaseFontBold, sans-serif; color: var(--rarity, #9aa3b2);
|
||||
background: radial-gradient(closest-side, color-mix(in srgb, var(--rarity, #9aa3b2) 38%, transparent), transparent 72%);
|
||||
}
|
||||
.garage-card-name {
|
||||
padding: 0 8px; font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.garage-card-name { padding: 0 0.6em; font-size: 0.85em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.garage-card-meta {
|
||||
padding: 2px 8px 8px; display: flex; align-items: center; justify-content: space-between; gap: 6px;
|
||||
color: var(--muted); font-size: 10px;
|
||||
padding: 0.15em 0.6em 0.55em; display: flex; align-items: center; justify-content: space-between; gap: 0.4em;
|
||||
color: rgba(255, 255, 255, 0.5); font-size: 0.72em;
|
||||
}
|
||||
.garage-price { color: #ffd76a; }
|
||||
.garage-premium {
|
||||
position: absolute; top: 5px; right: 5px; padding: 0 5px; border-radius: 3px;
|
||||
background: var(--accent); color: #241800; font: 800 9px/14px BaseFontBold, sans-serif;
|
||||
position: absolute; top: 0.35em; right: 0.35em; padding: 0 0.35em; border-radius: 0.2em;
|
||||
background: #ffb300; color: #241800; font: 800 0.65em/1.1em BaseFontBold, sans-serif;
|
||||
}
|
||||
.garage-empty { grid-column: 1 / -1; padding: 40px; text-align: center; color: var(--muted); }
|
||||
.garage-empty { grid-column: 1 / -1; padding: 3em; text-align: center; color: rgba(255, 255, 255, 0.55); }
|
||||
.garage-detail {
|
||||
width: 300px; flex: 0 0 300px; display: flex; flex-direction: column; gap: 8px;
|
||||
padding: 12px; border-radius: 8px; border: 1px solid rgba(255,255,255,.10);
|
||||
background: rgba(6,14,22,.78);
|
||||
width: 22em; flex: 0 0 22em; display: flex; flex-direction: column; gap: 0.55em;
|
||||
padding: 0.8em; border-radius: 0.5em; border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
background: rgba(6, 14, 22, 0.78);
|
||||
}
|
||||
.garage-detail h3 { margin: 0 0 4px; font: 600 17px/1.3 BaseFontMedium, sans-serif; }
|
||||
.garage-detail h3 { margin: 0 0 0.2em; font: 600 1.25em/1.3 BaseFontMedium, sans-serif; }
|
||||
.garage-preview {
|
||||
height: 190px; border-radius: 6px; border: 1px solid rgba(255,255,255,.08);
|
||||
height: 14em; border-radius: 0.4em; border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background:
|
||||
radial-gradient(60% 60% at 50% 45%, rgba(63,159,224,.16), transparent 70%),
|
||||
linear-gradient(180deg, rgba(255,255,255,.04), transparent);
|
||||
radial-gradient(60% 60% at 50% 45%, rgba(63, 159, 224, 0.16), transparent 70%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent);
|
||||
}
|
||||
.garage-preview-label { color: var(--muted); font-size: 11px; text-align: center; margin-top: -4px; }
|
||||
.garage-props { display: flex; flex-direction: column; gap: 5px; margin-top: 8px; }
|
||||
.garage-preview-label { color: rgba(255, 255, 255, 0.45); font-size: 0.72em; text-align: center; margin-top: -0.2em; }
|
||||
.garage-props { display: flex; flex-direction: column; gap: 0.3em; margin-top: 0.5em; }
|
||||
.garage-prop {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 5px 8px; border-radius: 4px; background: rgba(255,255,255,.03); font-size: 12px;
|
||||
padding: 0.35em 0.55em; border-radius: 0.25em; background: rgba(255, 255, 255, 0.03); font-size: 0.82em;
|
||||
}
|
||||
.garage-prop span { color: var(--muted); }
|
||||
.garage-prop span { color: rgba(255, 255, 255, 0.5); }
|
||||
.garage-prop b { font-weight: 600; }
|
||||
|
||||
/* ---------------- 大厅浮动元素 ---------------- */
|
||||
.lobby-chat {
|
||||
position: absolute; left: 118px; bottom: 34px; width: 370px; z-index: 5;
|
||||
padding: 9px 10px; border-radius: 8px;
|
||||
border: 1px solid rgba(255,255,255,.08);
|
||||
background: rgba(3,9,14,.72); backdrop-filter: blur(4px);
|
||||
}
|
||||
.chat-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; font-size: 12px; }
|
||||
.lobby-chat .chat-box { height: 148px; }
|
||||
.lobby-chat .chat-input { margin-top: 6px; }
|
||||
|
||||
.play-fab {
|
||||
position: absolute; right: 42px; bottom: 46px; z-index: 6;
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 14px 34px 14px 24px; border-radius: 4px;
|
||||
border: 1px solid #8a5d00;
|
||||
background: linear-gradient(180deg, #ffc93d, #ef9100);
|
||||
color: #241800; font: 800 22px/1 BaseFontBold, sans-serif;
|
||||
box-shadow: 0 10px 28px rgba(0,0,0,.45), 0 0 24px rgba(255,179,0,.25);
|
||||
letter-spacing: .04em;
|
||||
}
|
||||
.play-fab img { width: 34px; height: 34px; }
|
||||
.play-fab:hover { filter: brightness(1.06); }
|
||||
|
||||
/* 稀有度颜色(React 车库卡片) */
|
||||
.rarity-0 { --rarity: #9aa3b2; }
|
||||
.rarity-1 { --rarity: #5fbf6a; }
|
||||
.rarity-2 { --rarity: #3f8fe0; }
|
||||
@@ -403,6 +368,134 @@ img { display: block; }
|
||||
.rarity-5 { --rarity: #e05f5f; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.lobby-chat { display: none; }
|
||||
.play-fab { right: 18px; bottom: 24px; }
|
||||
.ms-top-logo { display: none; }
|
||||
.ms-battle-pass, .ms-announce { display: none; }
|
||||
.garage-detail { display: none; }
|
||||
.ms-content { padding: 7em 1em 2em; }
|
||||
}
|
||||
|
||||
/* ---------------- 车库(原版布局) ---------------- */
|
||||
.ms-content { padding: 0; }
|
||||
.ms-screen { padding: 7em 3em 2.5em; box-sizing: border-box; }
|
||||
.ms-garage-screen { padding: 0; height: 100%; }
|
||||
#garage-root { height: 100%; min-height: 0; }
|
||||
.g2 {
|
||||
display: flex; flex-direction: column; height: 100%; min-height: 0;
|
||||
background: linear-gradient(180deg, rgba(0, 12, 20, 0.35), rgba(0, 8, 14, 0.75));
|
||||
}
|
||||
.g2-head {
|
||||
display: flex; align-items: center; height: 6em; flex: 0 0 6em;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
|
||||
background: linear-gradient(180deg, rgba(0, 12, 20, 0.6), rgba(0, 12, 20, 0.12));
|
||||
}
|
||||
.g2-back {
|
||||
width: 6em; height: 6em; display: flex; align-items: center; justify-content: center;
|
||||
border: 0; background: transparent; cursor: pointer;
|
||||
}
|
||||
.g2-back:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
.g2-back-icon {
|
||||
width: 1.5em; height: 1.2em; background: #bfd5ff;
|
||||
mask: url("/play/static/images/iconBackArrow.9f950ca9.svg") center / contain no-repeat;
|
||||
-webkit-mask: url("/play/static/images/iconBackArrow.9f950ca9.svg") center / contain no-repeat;
|
||||
}
|
||||
.g2-title {
|
||||
position: absolute; left: 0; right: 0; margin: auto; width: 12em; text-align: center;
|
||||
font: 500 1.4em/1 BaseFontMedium, sans-serif; text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
}
|
||||
.g2-currencies { margin-left: auto; display: flex; align-items: center; gap: 1.2em; padding-right: 1.5em; font: 500 1em/1 BaseFontMedium, sans-serif; }
|
||||
.g2-currencies span { display: inline-flex; align-items: center; gap: 0.35em; }
|
||||
.g2-currencies .coin { width: 1.1em; height: 1.1em; }
|
||||
|
||||
.g2-body { flex: 1; min-height: 0; display: flex; gap: 1em; padding: 4em 6em; box-sizing: border-box; }
|
||||
.g2-left { width: 22em; flex: 0 0 22em; display: flex; flex-direction: column; gap: 0.35em; }
|
||||
.g2-gs {
|
||||
display: flex; align-items: center; gap: 0.5em; margin-bottom: 0.5em;
|
||||
padding: 0.6em 0.8em; border-radius: 0.4em; background: rgba(0, 20, 32, 0.55);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
.g2-gs img { width: 2.2em; height: 1.1em; }
|
||||
.g2-gs b { font: 700 1.8em/1 BaseFontBold, sans-serif; color: #fff; }
|
||||
.g2-param {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0.45em 0.7em; border-radius: 0.3em; background: rgba(3, 12, 20, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06); font-size: 0.85em;
|
||||
}
|
||||
.g2-param span { color: rgba(255, 255, 255, 0.55); }
|
||||
.g2-resists { display: flex; gap: 0.5em; margin-top: auto; }
|
||||
.g2-resist {
|
||||
width: 4em; height: 5em; display: flex; align-items: center; justify-content: center;
|
||||
border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 0.4em;
|
||||
background: rgba(0, 18, 30, 0.5); color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.g2-scene {
|
||||
flex: 1; min-width: 0; position: relative; display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center; border-radius: 0.5em;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
background:
|
||||
radial-gradient(60% 55% at 50% 45%, rgba(63, 159, 224, 0.14), transparent 70%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent);
|
||||
}
|
||||
.g2-scene-art {
|
||||
font: 800 16em/1 BaseFontBold, sans-serif; color: rgba(63, 159, 224, 0.18);
|
||||
text-shadow: 0 0 0.2em rgba(0, 0, 0, 0.4); user-select: none;
|
||||
}
|
||||
.g2-scene-caption {
|
||||
position: absolute; bottom: 1.2em; left: 0; right: 0; text-align: center;
|
||||
font: 500 1em/1.4 BaseFontMedium, sans-serif; color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
.g2-scene-caption span { display: block; font-size: 0.65em; color: rgba(255, 255, 255, 0.35); }
|
||||
.g2-drawer-toggle {
|
||||
position: absolute; left: 1em; bottom: 1em; padding: 0.6em 1.4em;
|
||||
border-radius: 0.4em; border: 1px solid #8a5d00; cursor: pointer;
|
||||
background: linear-gradient(180deg, #ffc93d, #f29500); color: #241800;
|
||||
font: 700 0.9em/1 BaseFontBold, sans-serif;
|
||||
}
|
||||
|
||||
.g2-right { width: 23em; flex: 0 0 23em; display: grid; grid-template-columns: 1fr 1fr; gap: 0.6em; align-content: start; }
|
||||
.g2-mounted {
|
||||
display: flex; flex-direction: column; padding: 0.5em; border-radius: 0.4em;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(3, 12, 20, 0.55);
|
||||
}
|
||||
.g2-mounted-title { font-size: 0.72em; color: rgba(255, 255, 255, 0.5); text-transform: uppercase; }
|
||||
.g2-mounted-art {
|
||||
height: 4.6em; display: flex; align-items: center; justify-content: center;
|
||||
font: 800 1.8em/1 BaseFontBold, sans-serif; color: var(--rarity, #9aa3b2);
|
||||
background: radial-gradient(closest-side, color-mix(in srgb, var(--rarity, #9aa3b2) 30%, transparent), transparent 72%);
|
||||
}
|
||||
.g2-mounted-name { font-size: 0.8em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
|
||||
.g2-supplies {
|
||||
flex: 0 0 9.6em; display: flex; align-items: center; justify-content: center; gap: 1.2em;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
||||
background: rgba(0, 10, 18, 0.55);
|
||||
}
|
||||
.g2-supply {
|
||||
width: 7.5em; height: 4em; display: flex; align-items: center; gap: 0.5em;
|
||||
justify-content: center; border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 0.35em; background: rgba(3, 12, 20, 0.6); font-size: 0.72em;
|
||||
}
|
||||
.g2-supply img { width: 1.8em; height: 1.8em; object-fit: contain; }
|
||||
.g2-supply span { color: rgba(255, 255, 255, 0.6); }
|
||||
.g2-supply b { color: #fff; }
|
||||
|
||||
.g2-drawer {
|
||||
position: absolute; inset: 6em 0 0 0; z-index: 12; display: flex; flex-direction: column;
|
||||
padding: 1em 2em 1.5em; background: rgba(2, 8, 14, 0.94); backdrop-filter: blur(6px);
|
||||
}
|
||||
.g2-drawer-head { display: flex; align-items: center; gap: 1em; margin-bottom: 0.8em; }
|
||||
.g2-drawer-head b { font: 600 1.3em/1 BaseFontMedium, sans-serif; }
|
||||
.g2-drawer-head .garage-search { margin-left: auto; }
|
||||
.g2-drawer-close {
|
||||
padding: 0.5em 1.2em; border-radius: 0.35em; border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
background: rgba(255, 255, 255, 0.05); color: #fff; cursor: pointer;
|
||||
}
|
||||
.g2-drawer-grid { flex: 1; min-height: 0; overflow: auto; margin-top: 0.8em; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.g2-left, .g2-right { display: none; }
|
||||
.g2-supplies { gap: 0.4em; }
|
||||
.g2-supply { width: auto; padding: 0 0.5em; }
|
||||
.g2-supply span { display: none; }
|
||||
}
|
||||
Reference in new issue
Block a user