From c23690669db2942e8a3e5a04ecdc6cef2636eeea Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 27 Oct 2026 00:12:42 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E6=8C=89=E5=8E=9F=E7=89=88?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E9=87=8D=E5=BB=BA=E5=95=86=E5=BA=97=E9=A1=B5?= =?UTF-8?q?=E5=B8=83=E5=B1=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 3DTank/docs/ObfuscatedSymbols.md | 130 ++++ 3DTank/docs/ShopStyleTable.md | 224 ++++++ 3DTank/docs/UiTranslationStatus.md | 7 +- .../kotlin/App/Browser/Ui/LobbyScreens.kt | 663 ++++++++++++++---- 4 files changed, 877 insertions(+), 147 deletions(-) create mode 100644 3DTank/docs/ObfuscatedSymbols.md create mode 100644 3DTank/docs/ShopStyleTable.md diff --git a/3DTank/docs/ObfuscatedSymbols.md b/3DTank/docs/ObfuscatedSymbols.md new file mode 100644 index 0000000..0a38040 --- /dev/null +++ b/3DTank/docs/ObfuscatedSymbols.md @@ -0,0 +1,130 @@ +# TankIC 混淆符号字典(原版 Kotlin/JS UI) + +> 范围:`3DTank/src/module_82347.js`(UI 组件/样式/资源)、`3DTank/src/module_63383.js`(CSS DSL)、`3DTank/src/module_37036.js`(Kotlin 运行时)、`3DTank/src/index.json`(资源模块索引)。`3DTank/src` 为原版拆包,`3dtank.com/play/static/js/unpacked` 为同构微修改版;本表行号优先引用前者。 +> 方法:静态解析 `Sq` / `Lq` 注册、样式类构造器、`Eq("...")` 字段、`jq(...)` 访问器、IIFE 参数表;并用 `acorn` 作用域分析确认资源参数的真实引用。 +> 证据格式:`文件:行号`。资源模块优先引用 `3DTank/src/index.json` 的 `id` / `path` 行;压缩版 `main.6469b966.js` 仅作同构对照,不再作为主证据。 +> 同构核对:`3DTank/src/module_82347.js` 与 `3dtank.com/play/static/js/unpacked/module_82347.js` 仅 `137040-137047` 内容不同(本地 SRV 显示补丁),行号一致;`module_63383/37036/59131/24923` 两版哈希一致。 + +## 0. 读法 + +- `Sq = To.$_$.ld`(`3DTank/src/module_82347.js:1326`)→ `3DTank/src/module_37036.js:10177` → `ie()`(`3DTank/src/module_37036.js:1276-1277`,内部调用 `ne("object", ...)`;`ne` 定义在 `:1271`)。它是 Kotlin 类元数据注册,不是 CSS 注册。 +- 真正的样式字段在样式类构造器内:`this. = this.aux([], <函数>).suv(this, Eq("<语义名>", ...))`。 +- 访问器形如 `jq(<类>).<方法> = function () { ... this..dk(this, Eq("<语义名>"...)) }`;因此 `字段=访问器` 可一一对应。 +- 带参数、且不绑定单一字段的方法单列为“参数化/共享样式方法”,不强行归入字段。 + +## 1. 样式类注册表 + +| 类名 | 语义名 | 文件:行号 | 已确认的字段/访问器 | +|---|---|---|---| +| `d7n` | `MainScreenComponentStyle` | `3DTank/src/module_82347.js:107879`(def `67488`;acc `173741-173921`) | `containerPanel=k1pz`; `containerForMenuGradient=l1pz`; `gradientBlock=m1pz`; `gradientBlockRight=n1pz`; `buttonPlay=e1o4`; `blockMainMenu=o1pz`; `noNew=p1pz`; `new=q1pz`; `playButtonContainer=r1pz`; `disabled=w1lw`; `disabledButtonPlay=s1pz`; `activeItem=t1pz`; `commonBlock=u1ju` | +| `fni` | `UserInfoContainerStyle` | `3DTank/src/module_82347.js:107924`(def `71520`;acc `177288-177638`) | `rankIconContainer=o1qe`; `containerRightBorder=p1qe`; `rankIconContainerClickable=q1qe`; `userNameRank=r1qe`; `textDecoration=s1qe`; `containerUserNameRank=t1qe`; `blockLeftPanel=y1qd`; `userTitleContainer=u1qe`; `titleRankIcon=v1qe`; `containerProgressMainScreen=w1qe`; `containerParameterProgress=x1qe`; `xpIcon=y1qe`; `progressValue=y1iq`; `tooltipContainer=u1je`; `premiumBenefitsContainer=u1qa`; `tooltipTimer=z1q9`; `grayColor=m1qa`; `regularUser=r1ih`; `friendUser=t1ih`; `clanMateUser=s1ih`; `blockForIconTankiOnline=n1qe`; `accountRemovalTimer=a1qe`;参数化:`u1iz(t)` @`177438` | +| `Yni` | `UserScoreComponentStyle` | `3DTank/src/module_82347.js:107927`(def `72033`;acc `177929-178089`) | `coinsContainer=b1ni`; `temporaryCashContainer=j1qd`; `temporaryCashButton=k1qd`; `blockRightPanel=z1qd`; `iconCrystal=o1k9`; `addRubyCrystal=m1qd`; `addCoins=i1qd`; `coinBlock=g1qd`; `coinIcon=h1qd`;参数化:`n1qd(t)` @`177974`、`l1qd(t)` @`178000`、`z1qc(t)` @`178059` | +| `d9` | `UserGroupTitleButtonComponentStyle` | `3DTank/src/module_82347.js:106987`(def `5061`;acc `110368-110398`) | `userGroupTitle=u1ii`; `commonBlockGroup=w1ii`; `commonUserIconsBlockGroup=v1ii` | +| `d1n` | `BreadcrumbsComponentStyle` | `3DTank/src/module_82347.js:107855`(def `66052`;acc `172496-172601`) | `headerContainer=h1pt`; `backButton=y1ie`; `rightButtonsContainer=d1pt`; `exitGameButton=e1pt`; `logout=i1pt`; `iconLogout=j1pt`; `rootTitle=o1om`; `breadcrumbs=g1pt` | +| `kNi` | `SettingsComponentStyle` | `3DTank/src/module_82347.js:108144`(def `89525`;acc `193943-194138`) | `blockContentOptions=n1s2`; `scroll=r1k3`; `scrollCreateBattle=p1jd`; `settingsBlockOptions=u1rv`; `containerBlock=n1n3`; `borderLineOption=d1rn`; `commonBlock=u1ju`; `rowBlock=q1s2`; `textHeadlineOptions=f1rn`; `scrollingMenu=o1jn`; `dropDownTitle=e1rt`; `dropDown=x1nc`; `dropdownPlaceholder=z1mg`; `slider=e1nj` | +| `EMi` | `SettingsMenuComponentStyle` | `3DTank/src/module_82347.js:108168`(def `90329`;acc `197540-197588`) | `slideMenuOptions=g1s7`; `yellowLineMenuOptions=h1s7`; `menuItemOptions=a1s7`; `activeItemOptions=b1s7`;共享样式:`f1s7()` @`197540` 返回 `zMi`(见 1.1) | +| `UPi` | `SettingsButtonsComponentStyle` | `3DTank/src/module_82347.js:108141`(def `89326`;acc `193821-193851`) | `containerGroupButton=h1s2`; `buttonsWidthBackReset=g1s2`; `buttonSaveLeave=i1s2` | +| `J7t` | `DropDownStyle` | `3DTank/src/module_82347.js:107449`(def `34073`;acc `141248-141385`) | `dropdownRoot=h1mh`; `dropdownControl=i1mh`; `dropdownPlaceholder=z1mg`; `dropdownMenu=k1mh`; `outerContainerStyle=a1md`; `innerContainerStyle=r1mc`; `dataListControl=a1mh`; `arrowDropDown=j1mh`;参数化:`f1mh(t)` @`141263`、`c1mh(t)` @`141338` | +| `u7t` | `ContextMenuStyle` | `3DTank/src/module_82347.js:107442`(def `33638`;acc `140826-140934`) | `menu=y1mf`; `menuItem=z1mf`; `colorText=z1li`; `disabledMenuItem=a1mg`; `menuItemRank=b1mg`; `decoration=c1mg`;参数化:`d1mg(t)` @`140856`、`e1mg(t,n,i,r)` @`140934` | +| `QBi` | `CheckBoxComponentStyle` | `3DTank/src/module_82347.js:108203`(def `92742`;acc `200405-200446`) | `oneSetting=w1md`;参数化:`d1sh(t)` @`200420`、`x1md(t,n)` @`200446` | +| `Z0t` | `CheckBoxStyle` | `3DTank/src/module_82347.js:107431`(def `33272`;acc `140535-140563`) | `checkbox=k1md`;参数化:`l1md(t,n)` @`140550`、`n1md(t,n,i)` @`140553`、`m1md(t,n,i)` @`140563` | +| `EUi` | `InputRangeComponentStyle` | `3DTank/src/module_82347.js:108205`(def `92965`;acc `200584-200614`) | `blockSoundVolume=r1sh`; `soundVolume=s1sh`; `range=hd` | +| `o2n` | `HeaderCommonStyle` | `3DTank/src/module_82347.js:107868`(def `67109`;acc `173096-173111`) | `icons=sdy`; `iconFont=e1ka` | + +表中 `acc` 是 `jq(...)` 方法范围,可能包含 1.1 节的参数化方法;`字段=访问器` 只列直接读取 `this..dk(...)` 的方法。 + +### 1.1 参数化/共享样式方法(已核对,不是字段) + +| 方法 | 参数/返回 | 调用点 | 已核对语义 | 证据文件:行号 | +|---|---|---|---|---| +| `fni.u1iz(t)` | 百分比数值 | `3DTank/src/module_82347.js:177904` | XP 进度条容器;子节点宽度按 `t%` | `3DTank/src/module_82347.js:177438-177460` | +| `Yni.n1qd(t)` | 布尔 cursor 开关 | `3DTank/src/module_82347.js:176977` | 水晶货币行样式 | `3DTank/src/module_82347.js:177974-177980` | +| `Yni.l1qd(t)` | 布尔 cursor 开关 | `3DTank/src/module_82347.js:176951` | 红宝石货币行样式(带底边框) | `3DTank/src/module_82347.js:178000-178009` | +| `Yni.z1qc(t)` | 布尔 cursor 开关 | `3DTank/src/module_82347.js:176918` | 金币按钮行样式 | `3DTank/src/module_82347.js:178059-178069` | +| `EMi.f1s7()` | 返回共享函数 `zMi` | `3DTank/src/module_82347.js:197505` | 设置菜单项共享布局样式 | `3DTank/src/module_82347.js:197540`; `zMi` 定义 `3DTank/src/module_82347.js:90245-90258` | +| `J7t.f1mh(t)` | 布尔 `isWhiteTheme` | `3DTank/src/module_82347.js:141083` | 下拉容器主题样式 | `3DTank/src/module_82347.js:141263-141271` | +| `J7t.c1mh(t)` | 布尔 `isWhiteTheme` | `3DTank/src/module_82347.js:33852` | 下拉滚动条主题样式 | `3DTank/src/module_82347.js:141338-141348` | +| `u7t.d1mg(t)` | 布尔分支 | `3DTank/src/module_82347.js:141061` | 菜单项样式(普通/选中分支) | `3DTank/src/module_82347.js:140856-140868` | +| `u7t.e1mg(t,n,i,r)` | 4 个参数 | `3DTank/src/module_82347.js:143351` | 菜单项复合样式 | `3DTank/src/module_82347.js:140934-140946` | +| `QBi.d1sh(t)` | 布尔 disabled | `3DTank/src/module_82347.js:93016` | 设置行/复选框行样式 | `3DTank/src/module_82347.js:200420-200439` | +| `QBi.x1md(t,n)` | 元素 + 布尔 | `3DTank/src/module_82347.js:33330`, `93020` | 设置输入/开关行样式 | `3DTank/src/module_82347.js:200446-200463` | +| `Z0t.l1md(t,n)` | 元素 + 布尔 | `3DTank/src/module_82347.js:33315`, `140502` | 开关容器基础样式 | `3DTank/src/module_82347.js:140550-140551` | +| `Z0t.n1md(t,n,i)` | 元素 + 两个布尔 | `3DTank/src/module_82347.js:33321`, `140530` | 开关轨道样式 | `3DTank/src/module_82347.js:140553-140561` | +| `Z0t.m1md(t,n,i)` | 元素 + 两个布尔 | `3DTank/src/module_82347.js:140530` | 圆形开关/勾选样式 | `3DTank/src/module_82347.js:140563-140575` | + +## 2. CSS DSL 映射表 + +仅列已由 `module_63383.js` 函数体 `$e("")` 直接确认的项。 + +| 压缩名 | 导出键 | 实际函数名 | CSS 属性 | 证据文件:行号 | +|---|---|---|---|---| +| `Wy` | `Ho.$_$.sb` | `Pc` | `width` | `3DTank/src/module_82347.js:1398`; `3DTank/src/module_63383.js:8082`, `5120`, `5126` | +| `Vy` | `Ho.$_$.b9` | `ua` | `height` | `3DTank/src/module_82347.js:1399`; `3DTank/src/module_63383.js:7803`, `3566`, `3571` | +| `R$` | `Ho.$_$.ka` | `rc` | `position` | `3DTank/src/module_82347.js:1440`; `3DTank/src/module_63383.js:7931`, `4528`, `4533` | +| `H$` | `Ho.$_$.b8` | `$h` | `cursor` | `3DTank/src/module_82347.js:1451`; `3DTank/src/module_63383.js:7670`, `2937`, `2942` | +| `o$` | `Ho.$_$.p9` | `za` | `marginRight` | `3DTank/src/module_82347.js:1413`; `3DTank/src/module_63383.js:7885`, `3862`, `3867` | +| `jj` | `Ho.$_$.x6` | `Uu` | `borderBottom` | `3DTank/src/module_82347.js:1541`; `3DTank/src/module_63383.js:7610`, `512`, `517` | +| `fS` | `Ho.$_$.i8` | `Ah` | `flexDirection` | `3DTank/src/module_82347.js:1581`; `3DTank/src/module_63383.js:7694`, `3085`, `3090` | +| `pS` | `Ho.$_$.h6` | `h_` | `alignItems` | `3DTank/src/module_82347.js:1583`; `3DTank/src/module_63383.js:7577`, `1642`, `1647` | +| `cx` | `Ho.$_$.g9` | `oa` | `justifyContent` | `3DTank/src/module_82347.js:1471`; `3DTank/src/module_63383.js:7853`, `3603`, `3608` | +| `_x` | `Ho.$_$.i6` | `c_` | `alignSelf` | `3DTank/src/module_82347.js:1468`; `3DTank/src/module_63383.js:7578`, `1679`, `1684` | +| `Jy` | `Ho.$_$.c8` | `jh` | `display` | `3DTank/src/module_82347.js:1403`; `3DTank/src/module_63383.js:7671`, `2974`, `2979` | +| `LS` | `Ho.$_$.fa` | `Ya` | `paddingLeft` | `3DTank/src/module_82347.js:1608`; `3DTank/src/module_63383.js:7924`, `4380`, `4385` | +| `BS` | `Ho.$_$.ga` | `Ja` | `paddingRight` | `3DTank/src/module_82347.js:1609`; `3DTank/src/module_63383.js:7925`, `4417`, `4422` | + +> `Ho` 即模块 `63383`:`3DTank/src/module_82347.js:212990` 的 `i(63383)` 对应参数 `Ho`。`3DTank/src/module_24923.js:1313` 又依赖 `63383` 做样式注入/类名生成;本表只确认 `63383` 中的属性名字面量,不把 `24923` 的生成器内部实现当作语义。13 项导出键→函数→`$e` 属性已逐项机器校验一致。 + +## 3. 资源模块表 + +`3DTank/src/module_82347.js` 的资源参数名 → 调用参数 `i()` → `3DTank/src/index.json` 中同 id 的 `path` 字段(原版拆包索引;压缩版 `main.6469b966.js` 为同构对照)。前 9 行为任务指定资源,后 8 行为同组件/核对项直接引用的补充项。 + +| 模块 id | static/images/... 文件名 | 用途 | 证据文件:行号 | +|---|---|---|---| +| `32475` | `ruby.ce62efbc.svg` | `ruby` 红宝石图标 | 参数 `cn` `3DTank/src/module_82347.js:123`;`i(32475)` `:212634`;`3DTank/src/index.json:2375-2377`;使用 `3DTank/src/module_82347.js:176956` | +| `91844` | `crystalSmall.242e6c15.svg` | `crystalSmall` 小水晶图标 | 参数 `fo` `3DTank/src/module_82347.js:448`、`I` `:37`;`i(91844)` `:212548`;`3DTank/src/index.json:6245-6247`;使用 `3DTank/src/module_82347.js:96447`, `176985` | +| `39856` | `addRuby.ff47c8be.svg` | `addRuby` 红宝石加号 | 参数 `Ku` `3DTank/src/module_82347.js:375`;`i(39856)` `:212886`;`3DTank/src/index.json:2845-2847`;使用 `3DTank/src/module_82347.js:176972` | +| `33190` | `addCrystal.af9a397c.svg` | `addCrystal` 水晶加号 | 参数 `Yu` `3DTank/src/module_82347.js:376`;`i(33190)` `:212887`;`3DTank/src/index.json:2430-2432`;使用 `3DTank/src/module_82347.js:176996` | +| `17881` | `groupUser.67c9ec61.svg` | `groupUser` 组队成员剪影 | 参数 `m` `3DTank/src/module_82347.js:24`;`i(17881)` `:212535`;`3DTank/src/index.json:1320-1322`;使用 `3DTank/src/module_82347.js:4860` | +| `51963` | `iconButtonPremium.bc17a592.svg` | `iconButtonPremium` 高级账号图标 | 参数 `H` `3DTank/src/module_82347.js:50`;`i(51963)` `:212561`;`3DTank/src/index.json:3610-3612`;使用 `3DTank/src/module_82347.js:176468` | +| `39688` | `iconTankiOnline.2a9c236f.svg` | `iconTankiOnline` Tanki Online 标志 | 参数 `Ju` `3DTank/src/module_82347.js:378`;`i(39688)` `:212889`;`3DTank/src/index.json:2825-2827`;使用 `3DTank/src/module_82347.js:177224` | +| `70556` | `fullscreen.0b4547f5.svg` | `fullscreen` 全屏图标 | 参数 `zn` `3DTank/src/module_82347.js:132`;`i(70556)` `:212643`;`3DTank/src/index.json:4830-4832`;使用 `3DTank/src/module_82347.js:137380` | +| `88309` | `iconBack.62869951.svg` | `iconBack` 返回/退出箭头 | 参数 `g` `3DTank/src/module_82347.js:23`;`i(88309)` `:212534`;`3DTank/src/index.json:5960-5962`;使用 `3DTank/src/module_82347.js:66013`, `4762` | +| `98628` | `iconBackArrow.9f950ca9.svg` | Breadcrumbs 子页返回箭头 | 参数 `kr` `3DTank/src/module_82347.js:239`;`i(98628)` `:212750`;`3DTank/src/index.json:6635-6637`;`IconStyle.iconBackArrow` @`3DTank/src/module_82347.js:211486`,使用 `:172388` | +| `82126` | `back.72d79619.svg` | 触摸控制编辑器返回键 | 参数 `uo` `3DTank/src/module_82347.js:440`;`i(82126)` `:212951`;`3DTank/src/index.json:5555-5557`;使用 `3DTank/src/module_82347.js:92464`(`TouchControlsEditorComponentStyle.backButton` @`:108202`) | +| `91451` | `fullscreenOff.5691cd5d.svg` | 退出全屏图标(与 `fullscreen` 成对) | 参数 `bn` `3DTank/src/module_82347.js:133`;`i(91451)` `:212644`;`3DTank/src/index.json:6200-6202`;使用 `3DTank/src/module_82347.js:137380` | +| `86552` | `coin.3a16e831.svg` | 金币图标 | 参数 `Vi` `3DTank/src/module_82347.js:212`;`i(86552)` `:212640`;`3DTank/src/index.json:5855-5857`;使用 `3DTank/src/module_82347.js:176926` | +| `22511` | `addCoin.cc7a25d3.svg` | 金币加号 | 参数 `Wu` `3DTank/src/module_82347.js:373`;`i(22511)` `:212884`;`3DTank/src/index.json:1695-1697`;使用 `3DTank/src/module_82347.js:176941` | +| `22198` | `caution.b9d12319.svg` | 临时货币按钮图标 | 参数 `Vu` `3DTank/src/module_82347.js:374`;`i(22198)` `:212885`;`3DTank/src/index.json:1665-1667`;使用 `3DTank/src/module_82347.js:176904`(`UserScoreComponentStyle.temporaryCashButton` @`:176901`) | +| `16849` | `empty.c3b3f789.svg` | Tanki Online 标志占位图(`mr4_1` 为真时替代 logo) | 参数 `Xu` `3DTank/src/module_82347.js:377`;`i(16849)` `:212888`;`3DTank/src/index.json:1240-1242`;使用 `3DTank/src/module_82347.js:177224` | +| `30786` | `ic_delete_from_group.bdff6efa.svg` | 组队成员行删除/移出图标 | 参数 `w` `3DTank/src/module_82347.js:21`;`i(30786)` `:212532`;`3DTank/src/index.json:2240-2242`;使用 `3DTank/src/module_82347.js:4765`、`:110194`(`v4.tap` 组队成员行 @`:110022`) | + +## 4. 组件渲染表 + +只记录 `tap` 内可直接读出的层级;节点标签由 `Ry`=`div`(`3DTank/src/module_59131.js:746`)、`jx`=`span`(`3DTank/src/module_59131.js:797`)、`Iq`=`h3`(`3DTank/src/module_59131.js:758`)、`tj`=`img`(`3DTank/src/module_59131.js:770`)确认。 + +| 组件类/函数 | 渲染函数 tap 行号 | 生成的 DOM 结构(父 → 子) | 关键样式字段 | +|---|---|---|---| +| `x6n`(Premium 购买/入口按钮) | `3DTank/src/module_82347.js:176431` | `div(g6n)` → 可选 `div(k6n)` 文本、`div(z6n)` 高级图标、`span(b6n)` 计时器 | 根:`box-shadow`、`cursor`、`96×96`、未开通 VIP 时金红径向渐变(`176436-176441`);图标使用 `iconButtonPremium`(`176468`);state `p6n` 含 `isPremiumBuyButton` / `hasPremium` / `premiumRemainingTime`(`176382-176393`) | +| `L4`(组队标题按钮) | `3DTank/src/module_82347.js:110273` | `div($4)` → 可选 `div(j4)` 金币文本、`div(E4)` 组队槽容器 → `q4/b4` 成员槽、可选 `Z5` tooltip | `Vy/Wy 96/95`、`border-right`、背景(`110277-110283`);`UserGroupTitleButtonComponentStyle.userGroupTitle/commonBlockGroup/commonUserIconsBlockGroup`(`110307`, `110327`, `110299`);state `d4` 含 `isCanInvite` / `membersGroup` / `onlineStatus` / `maxUsersInGroup`(`110214-110225`) | +| `Eni`(用户信息子组件) | `3DTank/src/module_82347.js:177748` | `div(dni)` → `div(mni)` 等级/头像 → `img` 头像;`div(gni)` → 昵称/称号/XP 区 | `UserInfoContainerStyle`:`rankIconContainerClickable`、`containerUserNameRank`、`containerProgressMainScreen`、`xpIcon`、`progressValue`(`177761`, `177781`, `177789`, `177909-177915`);state `pni` 含 `userId` / `uid` / `userInGroup` / `userScore` / `rank` / `clanTag` / `hasPremium`(`177677-177704`) | +| `rii`(用户信息容器/左侧面板) | `3DTank/src/module_82347.js:178113` | `div(Qni)` → `x6n`、可选 `L4`、`Eni` | `UserInfoContainerStyle.blockLeftPanel`(`178116`);三个子组件均在 `isLobbyLayout` 下装配(`178117-178124`);state `Jni` 仅 `groupButtonActive`(`178104-178106`) | +| `lti`(用户货币/全屏条) | `3DTank/src/module_82347.js:176892` | `div(nti)` → 可选 `div(ati)` 临时货币、`div(sti)` 金币块、`div(iti)` 红宝石行、`div(eti)` 水晶行 | `UserScoreComponentStyle`:`temporaryCashContainer`、`coinsContainer`、`addCoins`、`blockRightPanel`、`addRubyCrystal`、`iconCrystal`、`addRubyCrystal`(`176897-176977`);state `fti` 含 `user` / `isMobileDevice` / `isFullScreen` / `showExitButton` / `removalTimer`(`177000-177014`) | +| `eKt`(全屏按钮) | `3DTank/src/module_82347.js:137348` | `div(nKt)` → `div(iKt)` 全屏图标、可选 `h3(rKt)` 文本 | `FullScreenButtonStyle.b1m0/c1m0`(`107372`, `137351-137363`);图标在 `fullscreen` / `fullscreenOff` 间切换(`137380`);state `JVt` 含 `isFullScreen` / `inputMode`(`137312-137314`) | +| `d9`(样式类,无 `tap`) | 无 `tap`(已核对为样式类) | 渲染在 `L4.tap`(`3DTank/src/module_82347.js:110273`) | `userGroupTitle`/`commonBlockGroup`/`commonUserIconsBlockGroup`(`110368-110398`) | + +## 5. 未确认清单 + +- `x6n` / `L4` / `Eni` / `rii` / `lti` / `eKt` 的官方语义类名:未找到;`Lq` 注册对这些类均传空名(`3DTank/src/module_82347.js:107914`, `106986`, `107926`, `107929`, `107920`, `107371`),本表只用样式归属和渲染内容标注。 +- `x6n` 的官方组件名:未找到;state `p6n` 已确认它是 Premium 购买/入口按钮(`isPremiumBuyButton`、`hasPremium`、`premiumRemainingTime`,`3DTank/src/module_82347.js:176382-176393`),本表按 HEAD premium 按钮记录。 +- 资源模块的行号以 `3DTank/src/index.json` 的 `id` / `path` 行为准;压缩版 `main.6469b966.js` 为单行,仅用于同构对照。 + +已核对后移出未确认: + +- `d9`:确认是 `UserGroupTitleButtonComponentStyle` 样式类,没有 `tap`;渲染在 `L4.tap`(`3DTank/src/module_82347.js:106987`, `110273`)。 +- 14 个参数化/共享方法:确认不是字段访问器,已列入 1.1 节;其中 `EMi.f1s7()` 返回共享函数 `zMi`(`3DTank/src/module_82347.js:197540`, `3DTank/src/module_82347.js:90245-90258`)。 +- `iconBack` / `iconBackArrow` / `back` 分工:`iconBack` 用于 Logout(`:66013`),`iconBackArrow` 用于 Breadcrumbs 子页返回(`:172388`),`back` 用于触摸控制编辑器返回(`:92464`)。 + +## 6. 待核对(仅记录,不改原文档) + +- `3DTank/docs/HeadStyleTable.md:64` 的 `head.exit` 写 `back.72d79619.svg`;源码中 Breadcrumbs 子页返回按钮使用 `IconStyle.iconBackArrow`(`3DTank/src/module_82347.js:172388`)→ 资源模块 `98628` `iconBackArrow.9f950ca9.svg`(`3DTank/src/index.json:6635-6637`)。`back.72d79619.svg` 在 `module_82347` 中只用于触摸控制编辑器返回键(`3DTank/src/module_82347.js:92464`)。同文档 `:88` 的 `head.logout.icon` 使用 `iconBack.62869951.svg`,与源码一致;`UI_Rebuild_Plan.md:475` 描述的是 logout,也与源码一致。 +- `3DTank/docs/SettingsStyleTable.md` 第 6 节存在行号漂移,语义均正确:`InputRangeComponentStyle` 的 `EUi` 构造器实际在 `3DTank/src/module_82347.js:92965`(表写 `92980`,位于首字段结束);`kUi` 实际在 `92902`(表写 `92930`);`hUi.tap` 实际在 `200482`(表写 `200507`);`SettingsComponentStyle-slider` 的 `z1jp_1 -> slider` 的 `Eq("slider")` 实际在 `89711`(表写 `89620`)。 diff --git a/3DTank/docs/ShopStyleTable.md b/3DTank/docs/ShopStyleTable.md new file mode 100644 index 0000000..90161cf --- /dev/null +++ b/3DTank/docs/ShopStyleTable.md @@ -0,0 +1,224 @@ +# Shop Style Table + +> 来源:`3DTank/src/module_82347.js`(与 `3dtank.com/play/static/js/unpacked/module_82347.js` 同构)。 +> 提取方法:`3DTank/tools/headless_probe.js` 加载 `module_37036` + `module_82347`,实例化样式类,执行 `dk(...)` 的 style 函数。 +> Kotlin 侧:`TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt` 的 `ShopScreen` 及其上方 `Shop*` 样式。 + +## 1. 样式类 + +| 类 | 语义名 | 定义 | 访问器 | +|---|---|---|---| +| `$Fi` | `NewShopCommonComponentStyle` | `93477` | `e1hz` container / `b1sk` commonBlockMenuShop / `b1k7` commonContainer / `c1sj` gradient / `l1sk` containerAnnouncement | +| `zHi` | `ShopSelectedSectionComponentStyle` | `94195` | `y1qq` commonCard / `a1sp` gradientCategory / `y1so` limitedOffersCommon / `x1so` mobileCardContainer / `c1sp` scrollContainerShop / `u1ju` commonBlock / `u1sj` activeCategory / `v1sj` notifier / `k1nd` timer;参数化 `t1sj(icon, active)` | +| `MVi` | `ShopCategoryComponentStyle` | `95731` | `e1hz` container / `ri5` header / `gax` description / `t1su` bonusContainer / `v1su` bonus / `w1su` marginItemsContainer;参数化 `u1su(rec, bonus)` | +| `YVi` | `ShopCategoryOfferSectionStyle` | `95908` | `x1su` promoCodesItemsContainer / `p1st` widthSearchInput / `b1m1` containerItem / `y1su` containerCoin / `z1su` containerLimitOffers / `a1sv` containerKitOffers | +| `RKi` | `ShopFooterComponentStyle` | `96243` | `u1hz` mainContainer / `n1sw` buttonAndTextContainer / `o1sw` exclamation / `k1sw` reportButton | +| `uXi` | `ShopItemComponentStyle` | `96891` | `k1sy` 基础卡片 / `h1sy` footerContent / `h1pt` headerContainer / `j1sy` headerScroll / `y1sx` headerContent / `i1sy` containerName / `n1sy` previewContainer / `e1mb` discount / `s1ma` discountContainer 等 | +| `PZt` | `ShopBuyButtonComponentStyle` | `31920` | `u11p` root / `vck` price | + +## 2. 关键 CSS + +### 2.1 左侧分类菜单 + +`commonBlockMenuShop`(`b1sk`): + +```css +display:flex; flex-direction:column; align-items:center; justify-content:flex-start; +cursor:default; background-color:rgba(3,8,13,.25); backdrop-filter:blur(.781em); +border-radius:1rem; border-right:1.25em solid transparent; +height:max-content; margin-left:2em; margin-top:2em; +max-height:calc(100% - 9.8em); z-index:2; +padding:1.25em; padding-right:0; width:25em; +``` + +分类项 `t1sj(icon, active)`: + +```css +display:flex; align-items:center; justify-content:flex-start; +width:23.75em; height:3.5em; min-height:3.5em; max-height:3.5em; +margin:0; position:relative; overflow:hidden; cursor:pointer; +/* active */ +background-color:rgba(255,204,0,.25); border-radius:.5rem; color:rgb(255,204,0); +``` + +图标(`div`): + +```css +width:1.5em; height:1.5em; margin-right:1.25em; +background-color:white; /* active: rgb(255,204,0) + margin-left:1.25em */ +mask-image:url(icon); mask-position:center; mask-repeat:no-repeat; mask-size:contain; +``` + +标签(`h4`): + +```css +font:500 1em/1 BaseFontMedium; text-transform:uppercase; +color:white; /* active: rgb(255,204,0) */ +margin:.2em 0 0; +``` + +### 2.2 主容器 + +`commonContainer`(`b1k7`): + +```css +background:radial-gradient(50% 100% at 50% 100%, #203040 0%, #03080D 100%); +display:flex; align-items:stretch; justify-content:space-between; +height:100%; width:100%; max-height:calc(100% - 6em); +``` + +原版 `pFi.tap` 的三个子节点:左菜单、分类内容、右侧选中区。 + +### 2.3 分类内容 + +`ShopCategoryComponentStyle-container`(`e1hz`)是 `display:contents`;标题/描述/物品网格直接进入 `commonContainer`。 + +标题(`ri5`): + +```css +font:500 1.875em/1.4em BaseFontBold; margin:1em 0 0; text-transform:uppercase; +``` + +描述(`gax`): + +```css +opacity:.6; line-height:1.25em; font-size:.938em; +``` + +物品容器 `u1su(rec, bonus)`: + +```css +display:flex; flex-wrap:wrap; align-items:flex-start; justify-content:flex-start; +width:37em; height:100%; gap:1em; margin-left:.25em; margin-top:.25em; /* bonus: .6em */ +``` + +`containerItem`(`b1m1`): + +```css +display:grid; grid-template-columns:50% 50%; grid-template-rows:31.35%; +gap:1em; width:37.25em; margin:2em .9em 1em .25em; padding:0 0 0 .25em; +justify-content:flex-start; align-content:flex-start; z-index:3; +``` + +### 2.4 物品卡片 + +`k1sy`: + +```css +display:flex; flex-direction:column; align-items:center; justify-content:center; +position:relative; user-select:none; box-sizing:border-box; cursor:pointer; +border-radius:1rem; background-color:rgba(255,255,255,.15); +width:17.5em; height:inherit; +box-shadow:0 0 0 1px rgba(255,255,255,.3); +``` + +标题容器 `h1pt`: + +```css +width:calc(100% - 1.5em); position:absolute; left:.75em; top:.75em; +display:flex; align-items:center; justify-content:flex-start; z-index:1; +``` + +标题文字 `y1sx`: + +```css +font:500 1em/1 BaseFontMedium; text-transform:uppercase; color:white; margin-top:.2em; +``` + +价格文字 `h1sy`: + +```css +font:500 1.125em/1.125 BaseFontMedium; text-transform:uppercase; margin-top:.125em; +``` + +预览 `n1sy`: + +```css +display:flex; align-items:center; justify-content:center; +height:auto; width:auto; max-width:80%; z-index:3; +``` + +折扣 `e1mb`: + +```css +position:absolute; right:.625em; bottom:1em; +line-height:1.333em; font-size:.938em; font-weight:bold; +``` + +### 2.5 底部栏 + +`ShopFooterComponentStyle-mainContainer`(`u1hz`): + +```css +display:flex; align-items:center; justify-content:flex-start; +width:100%; height:3.125em; position:absolute; bottom:0; +border-top:.063em solid rgba(255,255,255,.1); +backdrop-filter:blur(2.688em); z-index:3; +``` + +`buttonAndTextContainer`(`n1sw`): + +```css +width:auto; height:2.2em; margin-left:1.8em; +display:flex; align-items:center; justify-content:flex-start; +``` + +`exclamation`(`o1sw`): + +```css +margin-left:.7em; opacity:.5; +font:400 .875em/1 BaseFontRegular; +``` + +## 3. 资源 + +| 用途 | 资源 | +|---|---| +| 红宝石 | `ruby.ce62efbc.svg` | +| 水晶 | `crystalSmall.242e6c15.svg` | +| 金币 | `coin.3a16e831.svg` | +| 红宝石加号 | `addRuby.ff47c8be.svg` | +| 水晶加号 | `addCrystal.af9a397c.svg` | +| 金币加号 | `addCoin.cc7a25d3.svg` | + +## 4. 当前实现状态 + +已还原: + +- 左侧竖向分类菜单(25em 面板 + 23.75em × 3.5em 分类项 + 图标/激活态) +- 主容器径向渐变 +- 分类标题/描述 +- 2 列物品网格(37em,gap 1em) +- 物品卡片(17.5em,1rem 圆角,15% 白底,1px 内描边) +- 物品名称/价格/货币图标 +- 右侧选中卡片(22.5em) +- 底部 3.125em 栏 + 购买按钮 + 价格 + +未展开: + +- `containerItem` 的 `grid-template-rows:31.35%` 动态行高(当前固定 11.5em) +- `ShopBuyButtonComponentStyle` 的共享按钮外观(当前用金色按钮近似) +- `ShopSelectedSectionComponentStyle` 的 `commonCard` / `limitedOffersCommon` 轮播(当前是单个选中卡片) +- `ShopCategoryOfferSectionStyle` 的 `containerCoin` / `containerLimitOffers` / `containerKitOffers` 特殊分类 +- 折扣/加成/礼包/促销码/能源条等子组件(当前数据模型没有这些字段) +- 分类菜单的 `(any-hover:hover)` 悬停样式(当前未加) + +## 5. 验收 + +```powershell +node 3DTank/tools/headless_probe.js ` + --url "http://localhost:8080/?autoconnect=1" ` + --out "build/probe/shop" ` + --width 1296 --height 1006 --wait 14000 ` + --eval-file "C:\Users\TermiNexus\AppData\Local\Temp\opencode\goto_shop.js" +``` + +实测: + +```text +shop.menu x=36 y=104 w=328 h=452 +shop.category x=362 y=104 w=... h=... +shop.item 226.8 × 149.0 +shop.selected 22.5em 宽 +shop.footer y=966 h=40 +``` diff --git a/3DTank/docs/UiTranslationStatus.md b/3DTank/docs/UiTranslationStatus.md index 3400712..7821a90 100644 --- a/3DTank/docs/UiTranslationStatus.md +++ b/3DTank/docs/UiTranslationStatus.md @@ -11,7 +11,7 @@ | 子页面外壳 | `LobbyView.kt` + `lobby3.css` | `ms-top` 始终显示;`#lobby-content` 从 6em 开始;不再使用独立 Breadcrumbs 顶栏 | | 大厅首页 | `app/src/jsMain/kotlin/App/Browser/Ui/HomeScreen.kt` | Emotion;Play / 主菜单 / Footer / BattlePass / 公告 | | 车库 | `app/src/jsMain/kotlin/App/Browser/Ui/GarageScreen.kt` | React;默认「总览」+ 右侧 4 个已装配件;分类栏、底盘/炮塔选择条、迷彩网格、左右面板、底部选择条 | -| 商店 | `app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt` | 分类页签 + 物品卡片(分类/名称图标、稀有度、VIP、货币价格)+ 右侧详情/购买确认 | +| 商店 | `app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt` | 已按 `NewShopCommonComponentStyle` / `ShopSelectedSectionComponentStyle` / `ShopCategoryComponentStyle` / `ShopItemComponentStyle` / `ShopFooterComponentStyle` 重建:左侧 25em 分类菜单、径向渐变主容器、分类标题/描述、2 列物品网格、17.5em 物品卡、右侧 22.5em 选中卡、底部 3.125em 购买栏;详见 `ShopStyleTable.md` | | 幸运礼盒 | 同上 | 4 种礼盒美术、钥匙图标/数量、可能奖励稀有度、右侧详情/开启确认 | | 战斗选择 | 同上 | 模式筛选(DM/TDM/CTF/CP/AS/JGR/SGE/RUGBY 图标)+ 战场列表 + 右侧详情/加入/创建入口 | | 任务 | 同上 | 分类侧栏(主要/每日/每周/星级挑战/合约)+ 任务卡(进度、奖励图标、领取/去完成)+ 星级挑战横幅 + 离线示例 | @@ -51,6 +51,11 @@ | 真实迷彩预览 | `Item3DCC.Resource` | 迷彩资源 URL 未打通;车库用色块、商店用色块渐变占位 | | 军团/好友真实数据 | `RootState` | 已接状态与列表;真实服务器数据下会替换离线示例 | | HEAD 组队 3/4 人分支 | `module_82347` `UserGroupTitleButtonComponentStyle-commonBlockGroup` | 2 人组(默认上限)已按 `q4` + `groupUser.svg` 还原;3/4 人组的等级小图标分支未接组队数据 | +| 商店物品卡动态行高 | `module_82347` `ShopCategoryOfferSectionStyle-containerItem` | 原版 `grid-template-rows:31.35%`,当前固定 `11.5em`;等容器高度确定后替换 | +| 商店购买按钮共享外观 | `module_82347` `ShopBuyButtonComponentStyle` | 当前用金色按钮近似;共享 Button 组件的完整样式待展开 | +| 商店选中区轮播 | `module_82347` `ShopSelectedSectionComponentStyle` | 当前单个 22.5em 选中卡;`commonCard` / `limitedOffersCommon` 的横向轮播待展开 | +| 商店特殊分类 | `module_82347` `ShopCategoryOfferSectionStyle` | `containerCoin` / `containerLimitOffers` / `containerKitOffers` 的金币、限时、礼包分类待展开 | +| 商店折扣/加成/促销码 | `module_82347` `ShopItemComponentStyle` | 当前数据模型没有折扣、加成、促销码字段;原版徽章/能源条未渲染 | ## 说明 diff --git a/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt b/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt index 418b711..1d4c5d8 100644 --- a/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt +++ b/TankiOffline/app/src/jsMain/kotlin/App/Browser/Ui/LobbyScreens.kt @@ -659,6 +659,353 @@ private fun RarityColor(Rarity: Int): String = when (Rarity) { else -> "rgba(232, 242, 255, 0.78)" } +/** 原版 `commonBlockMenuShop`。 */ +private val ShopMenu: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("flex-direction", "column") + Style("align-items", "center") + Style("justify-content", "flex-start") + Style("cursor", "default") + Style("background-color", "rgba(3, 8, 13, 0.25)") + Style("backdrop-filter", "blur(0.781em)") + Style("border-radius", "1rem") + Style("border-right", "1.25em solid transparent") + Style("height", "max-content") + Style("margin-left", "2em") + Style("margin-top", "2em") + Style("max-height", "calc(100% - 5.8em)") + Style("overflow-y", "auto") + Style("z-index", "2") + Style("padding", "1.25em") + Style("padding-right", "0") + Style("width", "25em") + Style("box-sizing", "border-box") +} + +/** 原版 `ShopSelectedSectionComponentStyle-t1sj`(分类菜单项)。 */ +private fun ShopMenuItemStyle(Active: Boolean): csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("align-items", "center") + Style("justify-content", "flex-start") + Style("width", "23.75em") + Style("height", "3.5em") + Style("min-height", "3.5em") + Style("max-height", "3.5em") + Style("margin", "0") + Style("position", "relative") + Style("overflow", "hidden") + Style("border", "0") + Style("background", "transparent") + Style("cursor", if (Active) "default" else "pointer") + if (Active) { + Style("background-color", "rgba(255, 204, 0, 0.25)") + Style("border-radius", "0.5rem") + } +} + +private fun ShopMenuIconStyle(Active: Boolean, Icon: String): csstype.PropertiesBuilder.() -> Unit = { + val Url = "url(\"${Asset("static/images/$Icon")}\")" + Style("width", "1.5em") + Style("height", "1.5em") + Style("margin-right", "1.25em") + if (Active) Style("margin-left", "1.25em") + Style("background-color", if (Active) "rgb(255, 204, 0)" else "rgb(255, 255, 255)") + Style("mask-image", Url) + Style("mask-position", "center") + Style("mask-repeat", "no-repeat") + Style("mask-size", "contain") + Style("-webkit-mask-image", Url) + Style("-webkit-mask-position", "center") + Style("-webkit-mask-repeat", "no-repeat") + Style("-webkit-mask-size", "contain") +} + +private fun ShopMenuLabelStyle(Active: Boolean): csstype.PropertiesBuilder.() -> Unit = { + Style("font-family", "BaseFontMedium, FallbackFontMedium") + Style("font-style", "normal") + Style("font-weight", "500") + Style("font-size", "1em") + Style("text-transform", "uppercase") + Style("color", if (Active) "rgb(255, 204, 0)" else "rgb(255, 255, 255)") + Style("margin", "0.2em 0 0") +} + +/** 原版 `NewShopCommonComponentStyle-commonContainer`。 */ +private val ShopMain: csstype.PropertiesBuilder.() -> Unit = { + Style("background", "radial-gradient(50% 100% at 50% 100%, #203040 0%, #03080D 100%)") + Style("display", "flex") + Style("align-items", "stretch") + Style("justify-content", "space-between") + Style("height", "100%") + Style("width", "100%") + Style("max-height", "100%") + Style("box-sizing", "border-box") + Style("padding", "0 2em 3.125em 0") + Style("overflow", "hidden") +} + +private val ShopCategory: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("flex-direction", "column") + Style("align-items", "flex-start") + Style("flex", "1") + Style("min-width", "0") + Style("height", "100%") + Style("padding", "1em 0 0 1em") + Style("box-sizing", "border-box") + Style("overflow", "hidden") +} + +private val ShopCategoryHeader: csstype.PropertiesBuilder.() -> Unit = { + Style("font-family", "BaseFontBold, FallbackFontBold") + Style("font-style", "normal") + Style("font-weight", "500") + Style("font-size", "1.875em") + Style("line-height", "1.4em") + Style("margin", "0") + Style("text-transform", "uppercase") +} + +private val ShopCategoryDesc: csstype.PropertiesBuilder.() -> Unit = { + Style("opacity", "0.6") + Style("line-height", "1.25em") + Style("font-size", "0.938em") + Style("margin", "0.25em 0 0") +} + +private val ShopItemsScroll: csstype.PropertiesBuilder.() -> Unit = { + Style("flex", "1") + Style("min-height", "0") + Style("width", "100%") + Style("overflow-y", "auto") + Style("padding-right", "0.5em") + Style("box-sizing", "border-box") +} + +private val ShopItems: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("flex-wrap", "wrap") + Style("align-content", "flex-start") + Style("justify-content", "flex-start") + Style("gap", "1em") + Style("width", "37em") + Style("margin", "0.25em 0 0 0.25em") +} + +/** 原版 `ShopItemComponentStyle-k1sy`。 */ +private fun ShopItemStyle(Selected: Boolean): csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("flex-direction", "column") + Style("align-items", "center") + Style("justify-content", "center") + Style("position", "relative") + Style("user-select", "none") + Style("box-sizing", "border-box") + Style("cursor", "pointer") + Style("border", "0") + Style("border-radius", "1rem") + Style("background-color", "rgba(255, 255, 255, 0.15)") + Style("width", "17.5em") + Style("height", "11.5em") + Style( + "box-shadow", + if (Selected) "0em 0em 0em 1px rgb(255, 204, 0)" else "0em 0em 0em 1px rgba(255, 255, 255, 0.3)", + ) +} + +private val ShopItemHeader: csstype.PropertiesBuilder.() -> Unit = { + Style("width", "calc(100% - 1.5em)") + Style("height", "auto") + Style("position", "absolute") + Style("left", "0.75em") + Style("top", "0.75em") + Style("display", "flex") + Style("align-items", "center") + Style("justify-content", "flex-start") + Style("z-index", "1") +} + +private val ShopItemName: csstype.PropertiesBuilder.() -> Unit = { + Style("font-family", "BaseFontMedium, FallbackFontMedium") + Style("font-style", "normal") + Style("font-weight", "500") + Style("font-size", "1em") + Style("text-transform", "uppercase") + Style("color", "rgb(255, 255, 255)") + Style("margin", "0.2em 0 0") + Style("white-space", "nowrap") + Style("overflow", "hidden") + Style("text-overflow", "ellipsis") +} + +private val ShopItemPreview: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("align-items", "center") + Style("justify-content", "center") + Style("width", "6em") + Style("height", "6em") + Style("margin-top", "1.5em") +} + +private val ShopItemPreviewImage: csstype.PropertiesBuilder.() -> Unit = { + Style("width", "6em") + Style("height", "6em") + Style("object-fit", "contain") +} + +private val ShopItemFooter: csstype.PropertiesBuilder.() -> Unit = { + Style("position", "absolute") + Style("right", "0.75em") + Style("bottom", "0.75em") + Style("display", "flex") + Style("align-items", "center") + Style("gap", "0.25em") + Style("z-index", "2") +} + +private val ShopItemPrice: csstype.PropertiesBuilder.() -> Unit = { + Style("font-family", "BaseFontMedium, FallbackFontMedium") + Style("font-style", "normal") + Style("font-weight", "500") + Style("font-size", "1.125em") + Style("text-transform", "uppercase") + Style("margin-top", "0.125em") +} + +private val ShopItemCurrency: csstype.PropertiesBuilder.() -> Unit = { + Style("width", "1em") + Style("height", "1em") +} + +private val ShopSelected: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("flex-direction", "column") + Style("align-items", "stretch") + Style("width", "22.5em") + Style("flex", "0 0 22.5em") + Style("align-self", "flex-start") + Style("margin-top", "2em") + Style("padding", "0.4em") + Style("border-radius", "0.625em") + Style("background", "rgba(255, 255, 255, 0.08)") + Style("box-sizing", "border-box") + Style("overflow", "hidden") +} + +private val ShopSelectedPreview: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("align-items", "center") + Style("justify-content", "center") + Style("height", "12em") + Style("border-radius", "0.4em") + Style("background-position", "center") + Style("background-repeat", "no-repeat") + Style("background-size", "contain") + Style("background-color", "rgba(0, 12, 20, 0.35)") +} + +private val ShopSelectedBody: csstype.PropertiesBuilder.() -> Unit = { + Style("padding", "0.75em 0.6em 0.6em") +} + +private val ShopSelectedTitle: csstype.PropertiesBuilder.() -> Unit = { + Style("font-family", "BaseFontMedium, FallbackFontMedium") + Style("font-style", "normal") + Style("font-weight", "500") + Style("font-size", "1.125em") + Style("line-height", "1.2") + Style("text-transform", "uppercase") + Style("margin", "0") +} + +private val ShopSelectedMeta: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("align-items", "center") + Style("gap", "0.6em") + Style("margin-top", "0.35em") + Style("font-size", "0.875em") + Style("opacity", "0.7") +} + +private val ShopSelectedPrice: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("align-items", "center") + Style("gap", "0.4em") + Style("margin-top", "0.75em") +} + +/** 原版 `ShopFooterComponentStyle-mainContainer`。 */ +private val ShopFooter: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("align-items", "center") + Style("justify-content", "flex-start") + Style("width", "100%") + Style("height", "3.125em") + Style("position", "absolute") + Style("bottom", "0") + Style("left", "0") + Style("border-top", "0.063em solid rgba(255, 255, 255, 0.1)") + Style("backdrop-filter", "blur(2.688em)") + Style("background", "rgba(3, 8, 13, 0.25)") + Style("z-index", "3") +} + +private val ShopFooterInner: csstype.PropertiesBuilder.() -> Unit = { + Style("width", "auto") + Style("height", "2.2em") + Style("margin-left", "1.8em") + Style("display", "flex") + Style("align-items", "center") + Style("justify-content", "flex-start") +} + +private val ShopFooterHint: csstype.PropertiesBuilder.() -> Unit = { + Style("margin-left", "0.7em") + Style("opacity", "0.5") + Style("font-family", "BaseFontRegular, FallbackFontRegular") + Style("font-style", "normal") + Style("font-weight", "normal") + Style("font-size", "0.875em") +} + +/** 原版 `ShopBuyButtonComponentStyle`:深色文字 + 金色按钮。 */ +private val ShopBuyButton: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "inline-flex") + Style("align-items", "center") + Style("justify-content", "center") + Style("height", "1.5em") + Style("padding", "0 0.8em") + Style("border", "0") + Style("border-radius", "0.25em") + Style("background", "rgb(255, 204, 0)") + Style("color", "rgb(0, 25, 38)") + Style("font", "500 1em/1 BaseFontMedium, FallbackFontMedium") + Style("cursor", "pointer") +} + +private val ShopBuyPrice: csstype.PropertiesBuilder.() -> Unit = { + Style("display", "flex") + Style("align-items", "center") + Style("justify-content", "flex-end") + Style("min-width", "3em") + Style("min-height", "1.5em") + Style("margin-left", "0.5em") +} + +private fun ShopItemCurrencyAsset(Currency: Int): String = when (Currency) { + 0 -> "crystalSmall.242e6c15.svg" + 1 -> "coin.3a16e831.svg" + 2 -> "ruby.ce62efbc.svg" + else -> "coin.3a16e831.svg" +} + +private fun ShopItemCurrencyColor(Currency: Int): String = when (Currency) { + 0 -> Palette.Cyan + 1 -> Palette.Yellow + 2 -> Palette.Red + else -> Palette.White +} + private val ShopScreen = FC { var Category by useState("all") var SelectedId by useState(null) @@ -666,86 +1013,96 @@ private val ShopScreen = FC { val Shown = if (Category == "all") Items else Items.filter { it.Category.toString() == Category } val Categories = GarageCategories.filter { C -> Items.any { it.Category == C.Id } } val Selected = Shown.firstOrNull { it.Id == SelectedId } ?: Shown.firstOrNull() + val ActiveLabel = Categories.firstOrNull { it.Id.toString() == Category }?.Label ?: "全部" - ScreenShell("商店", "${Items.size} 件可购买") { - div { - css(TabBar) - button { - css(TabStyle(Category == "all")) - onClick = { Category = "all"; SelectedId = null } - +"全部" - } - for (C in Categories) { - button { - css(TabStyle(Category == C.Id.toString())) - onClick = { Category = C.Id.toString(); SelectedId = null } - +C.Label - } - } - } + div { + css(ScreenRoot) + Ui("screen.商店") + // 原版 commonContainer:左菜单 / 中间物品 / 右侧选中 div { - css(Columns) + css(ShopMain) + Ui("shop.main") + div { - css { - Style("flex", "1") - Style("min-width", "0") + css(ShopMenu) + Ui("shop.menu") + button { + css(ShopMenuItemStyle(Category == "all")) + Ui("shop.menu.all") + onClick = { Category = "all"; SelectedId = null } + div { css(ShopMenuIconStyle(Category == "all", "iconLobbyMenuContainers.f960290f.svg")) } + span { css(ShopMenuLabelStyle(Category == "all")); +"全部" } } - if (Shown.isEmpty()) { - EmptyState("离线目录里暂无可购买物品。") - } else { - div { - css(Grid) - for (Item in Shown.take(120)) { - button { - val CardBase = CardStyle(Item.Id == Selected?.Id) - css { - CardBase(this) - Style("padding", "0") - } - onClick = { SelectedId = Item.Id } - div { - css { - Style("height", "5.5em") - Style("border-radius", "0.45em 0.45em 0 0") - if (Item.Category == 2) { - Style("background", PaintGradient(Item.Name)) - } else { - Style("background", "url(\"${Asset("static/images/" + ItemIcon(Item))}\") center / contain no-repeat") - Style("background-color", "rgba(0, 12, 20, 0.5)") - } - Style("border-bottom", "1px solid ${Ui.DividerSoft}") - Style("opacity", "0.92") - } - } - div { - css { Style("padding", "0.65em 0.8em 0.8em") } + for (C in Categories) { + val Active = Category == C.Id.toString() + button { + css(ShopMenuItemStyle(Active)) + Ui("shop.menu.${C.Id}") + onClick = { Category = C.Id.toString(); SelectedId = null } + div { css(ShopMenuIconStyle(Active, CategoryIcon(C.Id))) } + span { css(ShopMenuLabelStyle(Active)); +C.Label } + } + } + } + + div { + css(ShopCategory) + Ui("shop.category") + h2 { css(ShopCategoryHeader); Ui("shop.category.header"); +ActiveLabel } + p { + css(ShopCategoryDesc) + Ui("shop.category.desc") + +"${Shown.size} 件物品 · 选择物品查看详情" + } + div { + css(ShopItemsScroll) + Ui("shop.items.scroll") + if (Shown.isEmpty()) { + EmptyState("离线目录里暂无可购买物品。") + } else { + div { + css(ShopItems) + Ui("shop.items") + for (Item in Shown.take(200)) { + button { + css(ShopItemStyle(Item.Id == Selected?.Id)) + Ui("shop.item.${Item.Id}") + onClick = { SelectedId = Item.Id } div { - css { - Style("display", "flex") - Style("align-items", "center") - Style("justify-content", "space-between") - Style("gap", "0.4em") - } - div { css(CardTitle); +Item.Name } - if (Item.Premium) Pill("VIP", "#ffcc00") + css(ShopItemHeader) + span { css(ShopItemName); +Item.Name } + } + if (Item.Category == 2) { + div { + css { + Apply(ShopItemPreview) + Style("background", PaintGradient(Item.Name)) + Style("border-radius", "0.5em") + } + } + } else { + div { + css(ShopItemPreview) + img { + src = Asset("static/images/" + ItemIcon(Item)) + css(ShopItemPreviewImage) + } + } } - div { css(CardText); +CategoryLabel(Item.Category) } div { - css { - Style("display", "flex") - Style("align-items", "center") - Style("justify-content", "space-between") - Style("margin-top", "0.55em") + css(ShopItemFooter) + img { + src = Asset("static/images/" + ShopItemCurrencyAsset(Item.Currency)) + css(ShopItemCurrency) } span { css { - Style("color", RarityColor(Item.Rarity)) - Style("font", "500 0.78em/1 ${Ui.FontMedium}") + Apply(ShopItemPrice) + Style("color", ShopItemCurrencyColor(Item.Currency)) } - +RarityName(Item.Rarity) + +"${Item.Price}" } - span { css { Style("color", Ui.Cyan); Style("font", "500 0.9em/1 ${Ui.FontMedium}") }; +PriceText(Item) } } } } @@ -755,112 +1112,126 @@ private val ShopScreen = FC { } div { - css { - Style("width", "22.5em") - Style("flex", "0 0 22.5em") - Style("align-self", "flex-start") - } - css { - Apply(Panel) - Style("overflow", "hidden") } + css(ShopSelected) + Ui("shop.selected") if (Selected == null) { div { css { Style("padding", "1.2em") - Style("color", Ui.Muted) + Style("color", Palette.TextMuted) } +"选择一件物品查看详情。" } } else { - div { - css { - Style("height", "11em") - if (Selected.Category == 2) { + if (Selected.Category == 2) { + div { + css { + Apply(ShopSelectedPreview) Style("background", PaintGradient(Selected.Name)) - } else { - Style("background", "url(\"${Asset("static/images/" + ItemIcon(Selected))}\") center / contain no-repeat") - Style("background-color", "rgba(0, 12, 20, 0.55)") } - Style("border-bottom", "1px solid ${Ui.DividerSoft}") + } + } else { + div { + css(ShopSelectedPreview) + img { + src = Asset("static/images/" + ItemIcon(Selected)) + css { + Style("width", "12em") + Style("height", "12em") + Style("object-fit", "contain") + } + } } } div { - css { Style("padding", "1em 1.1em 1.2em") } + css(ShopSelectedBody) + h3 { css(ShopSelectedTitle); +Selected.Name } div { - css { - Style("display", "flex") - Style("align-items", "center") - Style("justify-content", "space-between") - Style("gap", "0.5em") - } - h2 { css(CardTitle); +Selected.Name } - if (Selected.Premium) Pill("VIP", "#ffcc00") + css(ShopSelectedMeta) + span { +CategoryLabel(Selected.Category) } + span { +RarityName(Selected.Rarity) } } div { - css { - Style("display", "flex") - Style("align-items", "center") - Style("gap", "0.6em") - Style("margin-top", "0.25em") - } - span { css(CardText); +CategoryLabel(Selected.Category) } - span { css { Style("color", RarityColor(Selected.Rarity)); Style("font", "500 0.8em/1 ${Ui.FontMedium}") }; +RarityName(Selected.Rarity) } - } - div { - css { - Style("display", "flex") - Style("align-items", "center") - Style("gap", "0.5em") - Style("margin-top", "0.8em") - } + css(ShopSelectedPrice) img { - src = Asset( - "static/images/" + when (Selected.Currency) { - 0 -> "crystalSmall.242e6c15.svg" - 1 -> "coin.3a16e831.svg" - 2 -> "ruby.ce62efbc.svg" - else -> "coin.3a16e831.svg" - } - ) + src = Asset("static/images/" + ShopItemCurrencyAsset(Selected.Currency)) css { Style("width", "1.4em"); Style("height", "1.4em") } } - span { css { Style("color", Ui.Cyan); Style("font", "500 1.2em/1 ${Ui.FontMedium}") }; +PriceText(Selected) } - } - p { - css { - Style("margin-top", "0.8em") - Style("color", Ui.Muted) - Style("font", "400 0.88em/1.5 ${Ui.FontRegular}") - } - +"购买后可在车库中装备。离线模式下只演示购买确认流程。" - } - div { - css { Style("display", "flex"); Style("gap", "0.5em"); Style("margin-top", "0.8em") } - button { - css(PrimaryButton) - onClick = { - DialogContext.Show( - "购买确认", - "确定购买「${Selected.Name}」吗?\n价格:${PriceText(Selected)}", - "购买", - "取消", - OnConfirm = { - ToastContext.Show("已提交购买:${Selected.Name}(离线模式)") - }, - ) + span { + css { + Style("font", "500 1.2em/1 BaseFontMedium, FallbackFontMedium") + Style("color", ShopItemCurrencyColor(Selected.Currency)) } - +"购买" + +PriceText(Selected) } - button { - css(SecondaryButton) - onClick = { ScreenContext.OnNavigate("garage") } - +"去车库" + } + button { + css { + Apply(ShopBuyButton) + Style("margin-top", "0.9em") } + Ui("shop.selected.buy") + onClick = { + DialogContext.Show( + "购买确认", + "确定购买「${Selected.Name}」吗?\n价格:${PriceText(Selected)}", + "购买", + "取消", + OnConfirm = { + ToastContext.Show("已提交购买:${Selected.Name}(离线模式)") + }, + ) + } + +"购买" } } } } } + + // 原版 ShopFooterComponentStyle + div { + css(ShopFooter) + Ui("shop.footer") + div { + css(ShopFooterInner) + button { + css(ShopBuyButton) + Ui("shop.footer.buy") + onClick = { + Selected?.let { Item -> + DialogContext.Show( + "购买确认", + "确定购买「${Item.Name}」吗?\n价格:${PriceText(Item)}", + "购买", + "取消", + OnConfirm = { + ToastContext.Show("已提交购买:${Item.Name}(离线模式)") + }, + ) + } + } + +"购买" + } + Selected?.let { Item -> + div { + css(ShopBuyPrice) + img { + src = Asset("static/images/" + ShopItemCurrencyAsset(Item.Currency)) + css(ShopItemCurrency) + } + span { + css { + Style("font", "500 1em/1 BaseFontMedium, FallbackFontMedium") + Style("color", ShopItemCurrencyColor(Item.Currency)) + } + +"${Item.Price}" + } + } + } + span { css(ShopFooterHint); +"选择物品后可购买" } + } + } } }