From 7923c60f9819da24662a86f169f007bb0367617f Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 25 Oct 2026 19:45:13 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=8C=89=E7=BA=BF=E4=B8=8A=20DOM?= =?UTF-8?q?=20=E9=87=8D=E5=BB=BA=E5=A4=A7=E5=8E=85=E9=A6=96=E9=A1=B5?= =?UTF-8?q?=E4=B8=8E=E8=BD=A6=E5=BA=93=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/UI_Rebuild_Plan.md | 58 +- .../jsMain/kotlin/App/Browser/LobbyView.kt | 517 +++++-------- .../kotlin/App/Browser/Ui/GarageScreen.kt | 236 ++++-- .../app/src/jsMain/resources/lobby.css | 727 ++++++++++-------- 4 files changed, 834 insertions(+), 704 deletions(-) diff --git a/3DTank/docs/UI_Rebuild_Plan.md b/3DTank/docs/UI_Rebuild_Plan.md index 9e28fac..3a6b6e5 100644 --- a/3DTank/docs/UI_Rebuild_Plan.md +++ b/3DTank/docs/UI_Rebuild_Plan.md @@ -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/..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..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。 diff --git a/TankiOffline/app/src/jsMain/kotlin/App/Browser/LobbyView.kt b/TankiOffline/app/src/jsMain/kotlin/App/Browser/LobbyView.kt index 62bba1f..daec6b3 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/Browser/LobbyView.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/Browser/LobbyView.kt @@ -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> = 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", """ -
-
${U.Rank}
-
- ${Esc(U.Uid.ifEmpty { "未登录" })} - GS ${U.GearScore} · Rank ${U.Rank} +
+
+ ${LEsc(U.Uid.ifEmpty { "未登录" })} +
+
${U.Score} / ${if (Bounds.TopBound > 0) Top else "—"}
+
+ """ + ) + SetHtml( + "ms-top-right", + """ +
+
+ + ${U.Rubies} + +
+
+ + ${U.Crystals} +
-
- ${U.Crystals} - ${U.Score} - - ${if (U.HasPremium) "VIP" else "普通"} - -
+
F11
+ """ ) - 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 { "-" })}|TYPE: web|VER: Kotlin/JS|" + + "SRV: offline|STATE: ${LEsc(Client.Status)}" ) - BindDynamic() } // ------------------------------------------------------------------ 骨架 private fun Shell(): String = """ -
-
-
-
- +
+ +
+ +
+
+ +
-
- +
+
+ +
    + ${MenuItem("containers", "Containers", "幸运礼盒")} + ${MenuItem("quests", "Quests", "任务")} + ${MenuItem("garage", "Garage", "车库")} + ${MenuItem("shop", "Shop", "商店")} + ${MenuItem("settings", "Option", "设置")} +
+
+
    + ${FooterItem("news", "News")} + ${FooterItem("chat", "Chat")} + ${FooterItem("friends", "Friends")} + ${FooterItem("clans", "Clan")} +
+
+
-
-
- -
- - - -
未连接
- - """.trimIndent() - private fun NavButton(Id: String, Icon: String, Label: String, Key: String): String = """ - + private fun MenuItem(Id: String, Key: String, Fallback: String): String = """ +
  • +
    + ${LEsc(Translations.T("lobby.menu.${Key.uppercase()}", Fallback))} +
  • + """.trimIndent() + + private fun FooterItem(Id: String, Key: String): String = """ + """.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 """ -
    -
    -
    ${Esc(Translations.T("lobby.menu.PLAY", "开始战斗"))}
    -

    ${Esc(Translations.T("lobby.menu.PLAY", "开始战斗"))}

    -

    快速匹配一场战斗,或从战斗列表中选择模式与地图。

    -
    - - -
    - ${if (Connected) "" else """ -

    当前未连接服务器:点右上角「连接」填入中继地址后即可接收真实大厅数据。

    - """} -
    -
    -
    - -
    - ${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", "车库"), "装备、皮肤与强化")} -
    - """.trimIndent() - } - - private fun HomeCard(Nav: String, Icon: String, Title: String, Text: String): String = """ - - """.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 """ -

    ${Esc(Translations.T("lobby.battle.list.TITLE", "战场列表"))}

    - -
    -
    - ${Modes.joinToString("") { (Key, Name, Desc) -> """ - - """ }} -
    -

    战斗创建 / 匹配的完整链路属于 S4;这里先接真实状态里的战斗列表与模式文案。

    - """.trimIndent() + else -> "" } private fun Garage(): String { if (!UiCatalog.Loaded) { UiCatalog.Ensure(Client) { Refresh() } - return """ -

    ${Esc(Translations.T("lobby.menu.GARAGE", "车库"))}

    -
    正在加载物品目录…
    - """.trimIndent() + return """
    正在加载物品目录…
    """ } - return """
    """ + return """
    """ } private fun Shop(): String { val All = Client.Router.Catalog.Items.values.filter { it.buyable } - val Shown = All.take(96) return """ -

    ${Esc(Translations.T("lobby.menu.SHOP", "商店"))} - ${All.size} 件可购买

    - ${if (All.isEmpty()) """ -
    离线目录里暂无可购买物品,连接服务器后会自动填充。
    - """ else """ -
    ${Shown.joinToString("") { ItemCard(it) }}
    - """} +
    +

    ${LEsc(Translations.T("lobby.menu.SHOP", "商店"))}${All.size} 件可购买

    +
    ${if (All.isEmpty()) "离线目录里暂无可购买物品。" else "商店卡片将在下一轮迁移。"}
    +
    """.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 """ -

    ${Esc(Translations.T("lobby.menu.CONTAINERS", "幸运礼盒"))} - ${All.size}

    - ${if (All.isEmpty()) "
    没有礼盒数据。
    " - else "
    " + All.take(60).joinToString("") { ItemCard(it) } + "
    "} +
    +

    ${LEsc(Translations.T("lobby.menu.CONTAINERS", "幸运礼盒"))}$N

    +
    礼盒开启动画与掉落表待接入。
    +
    """.trimIndent() } - private fun Quests(): String = """ -

    ${Esc(Translations.T("lobby.quests.navigation.HEADER", "任务"))}

    -
    - ${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 = """ +
    +

    ${LEsc(Translations.T("lobby.battle.list.TITLE", "战场列表"))}

    +
    战斗选择 / 匹配属于 S4;当前先接入真实大厅外壳。
    """.trimIndent() - private fun Friends(): String = """ -

    ${Esc(Translations.T("lobby.friends.TITLE", "好友"))}

    -
    好友列表将在社交链路补齐后接入。
    + private fun Quests(): String = """ +
    +

    ${LEsc(Translations.T("lobby.quests.navigation.HEADER", "任务"))}

    +
    任务列表将在下一轮迁移。
    +
    """.trimIndent() + private fun Chat(): String { + val Lines = Client.Logs.takeLast(20).joinToString("") { "
    ${LEsc(it)}
    " } + return """ +
    +

    ${LEsc(Translations.T("lobby.menu.CHAT", "聊天"))}

    +
    ${if (Lines.isEmpty()) "连接后显示服务器日志" else Lines}
    +
    + """.trimIndent() + } + private fun News(): String { val Items = Client.Router.Store.State.News.News return """ -

    ${Esc(Translations.T("lobby.menu.NEWS", "新闻"))}

    - ${if (Items.isEmpty()) "
    暂无新闻。
    " - else "
    " + Items.take(20).joinToString("") { N -> - "
    ${Esc(N.header)}

    ${Esc(N.description)}

    " +
    +

    ${LEsc(Translations.T("lobby.menu.NEWS", "新闻"))}

    + ${if (Items.isEmpty()) "
    暂无新闻。
    " + else "
    " + Items.take(20).joinToString("") { N -> + "
    ${LEsc(N.header)}

    ${LEsc(N.description)}

    " } + "
    "} +
    """.trimIndent() } private fun Settings(): String = """ -

    ${Esc(Translations.T("lobby.menu.OPTIONS", "设置"))}

    -
    -
    连接大厅 / 空间中继地址
    -
    语言简体中文(运行时读取官方 zh-CN)
    -
    诊断帧 ${Client.Frames},记录 ${Client.Records},未覆盖 ${Client.Router.Unmapped}
    +
    +

    ${LEsc(Translations.T("lobby.menu.OPTIONS", "设置"))}

    +
    设置页将在下一轮迁移(游戏 / 画面 / 声音 / 操控 / 账户 …)。
    """.trimIndent() private fun Placeholder(Title: String, Text: String): String = """ -

    ${Esc(Title)}

    -
    ${Esc(Text)}
    +
    +

    ${LEsc(Title)}

    +
    ${LEsc(Text)}
    +
    """.trimIndent() - private fun Chat(): String { - val Lines = Client.Logs.takeLast(12).joinToString("") { "
    ${Esc(it)}
    " } - return """ -
    - ${Esc(Translations.T("lobby.menu.CHAT", "聊天"))} - ${Client.Status} -
    -
    ${if (Lines.isEmpty()) "连接后显示服务器日志" else Lines}
    -
    - """.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 """ -
    -
    ${Esc(Name.take(1))}
    -
    - ${Esc(Name)} - ${Esc(Category)}${if (Count > 1) " · x$Count" else ""} - ${if (Premium) "VIP" else ""} -
    -
    - """.trimIndent() - } - private fun SetHtml(Id: String, Html: String) { - val El = Doc.getElementById(Id) ?: return + val El = LDoc.getElementById(Id) ?: return El.innerHTML = Html } } diff --git a/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt b/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt index b8cf238..ecca533 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt @@ -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 { var Category by useState(-1) var Query by useState("") var Selected by useState(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 { } 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 diff --git a/TankiOffline/app/src/jsMain/resources/lobby.css b/TankiOffline/app/src/jsMain/resources/lobby.css index a39edc4..9d2abff 100644 --- a/TankiOffline/app/src/jsMain/resources/lobby.css +++ b/TankiOffline/app/src/jsMain/resources/lobby.css @@ -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; } +}