美化买断制定价字段:分组卡片、胶囊付费方式、带单位的价格输入
Project CI / Backend tests (pull_request) Failing after 15s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m12s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m18s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m6s
Project CI / Repository checks (pull_request) Failing after 34s
Project CI / Frontend tests (pull_request) Successful in 3m26s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m34s
Project CI / Native shell tests (pull_request) Successful in 8m59s

- packages/shared:`PlatformGamePricingField` 改用 `PlatformSubpanel` 承载分组外壳(描边卡片 + 14px 黑体标题 `titleVariant="strong"`),与网页 `game-publish-fieldset`、AGC `__publication` 同族观感。
- packages/shared:付费方式从裸原生 radio 改为「原生 radio(屏外裁切,保留单选组语义/方向键/.checked/label 关联)+ 共享 chip 外观 `getPlatformCategoryChipClassName`」(未选=浅底描边,选中=实心品牌填充反白)。
- packages/shared:买断价改用共享 `PlatformTextField`(size=md / density=compact),标签与输入分离(label htmlFor),输入右对齐等宽数字、右侧留出「泥点」单位位;错误态红边红底(对网页宿主 `platform-subpanel` 输入框 remap 的 `!important` 同权重例外)、禁用态降透明度。
- packages/shared:CSS 补齐分组间距与层次、44px 触控胶囊、窄屏(≤480px)两枚等宽胶囊、`focus-visible` 焦点环;文案、props、校验与提交逻辑一律未改。
- 未触碰 AGC 发布面板「项目版本」一组及其包装/样式,也未改 `GameDistributionPublishPanel.tsx`。
This commit is contained in:
2026-10-06 11:09:19 +08:00
parent dcef9b62a8
commit 7223c05e28
2 changed files with 196 additions and 58 deletions
@@ -1,82 +1,168 @@
/*
* 买断制定价字段:网页发布页与 AGC 发布面板共用同一份表现与文案。
*
* 这里只补平台原语没覆盖的部分,不另造一套控件观感:
* - 外壳:`PlatformSubpanel`(`platform` 面 + `xs` 圆角 ≈ 网页 `.game-publish-fieldset` 的
* 0.7rem 描边卡片,AGC 侧同族容器是 `.game-distribution-publish-panel__publication`)。
* - 付费方式:原生 radio(屏外裁切保留语义 / 键盘 / `.checked`)+ 共享 chip 外观
* (`getPlatformCategoryChipClassName`:未选=浅底描边,选中=实心品牌填充反白)。
* - 买断价:`PlatformTextField`,本文件只补右对齐等宽数字、单位留位、错误态与层次。
*
* 选择器统一写成「根类 + 子类」而不是裸类:AGC 发布面板的
* `.game-distribution-publish-panel__fields label/input` 是后代选择器,裸类会被它
* 覆盖。提高一档特异性后,无论宿主在前后引入样式,组件外观都由这里唯一决定。
* `.game-distribution-publish-panel__fields label/input` 是后代选择器(0,1,1),会盖掉
* 原语里的 Tailwind 工具类(0,1,0)。提高一档特异性后,组件外观在两端都由这里唯一决定。
*/
.platform-game-pricing-field {
display: grid;
gap: 0.55rem;
gap: 0.6rem;
min-width: 0;
margin: 0;
border: 0;
padding: 0;
font-size: 0.76rem;
color: var(--platform-text-strong, #3d1f10);
}
.platform-game-pricing-field > legend {
padding: 0 0.3rem;
font-size: 0.72rem;
font-weight: 700;
}
.platform-game-pricing-field .platform-game-pricing-field__modes {
display: flex;
flex-wrap: wrap;
gap: 0.9rem;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.platform-game-pricing-field .platform-game-pricing-field__option {
position: relative;
display: inline-flex;
align-items: center;
gap: 0.35rem;
font-size: 0.76rem;
font-weight: 500;
}
.platform-game-pricing-field .platform-game-pricing-field__option input {
width: auto;
gap: 0;
min-width: 0;
margin: 0;
cursor: pointer;
}
/* 原生 radio 只做语义与键盘载体:`display: none` / `visibility: hidden` 会把可聚焦性和
读屏可见性一起拿掉,所以用「屏外裁切」隐藏,焦点仍落在它身上。 */
.platform-game-pricing-field .platform-game-pricing-field__option-input {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
width: 1px;
height: 1px;
min-width: 0;
margin: 0;
border: 0;
padding: 0;
background: transparent;
overflow: hidden;
white-space: nowrap;
clip-path: inset(50%);
}
/* chip 自带高度 36px(窄屏还会降到 33px),这里统一抬到 44px 触控下限;
「免费 / 买断制」字数不同,按内容宽度会一宽一窄,给个共同下限让两枚等宽。 */
.platform-game-pricing-field .platform-game-pricing-field__option-chip {
min-width: 5.5rem;
min-height: 2.75rem;
}
.platform-game-pricing-field
.platform-game-pricing-field__option-input:focus-visible
+ .platform-game-pricing-field__option-chip {
outline: 2px solid var(--platform-input-focus-ring, #b6623f);
outline-offset: 2px;
}
.platform-game-pricing-field
.platform-game-pricing-field__option-input:disabled
+ .platform-game-pricing-field__option-chip {
cursor: not-allowed;
opacity: 0.55;
}
.platform-game-pricing-field .platform-game-pricing-field__price {
display: grid;
gap: 0.3rem;
font-weight: 500;
gap: 0.35rem;
min-width: 0;
}
.platform-game-pricing-field .platform-game-pricing-field__price input {
box-sizing: border-box;
width: 100%;
min-width: 0;
border: 1px solid var(--platform-surface-border, rgba(226, 203, 184, 0.72));
border-radius: 0.7rem;
padding: 0.55rem 0.7rem;
background: var(--platform-panel-fill, #fffdfa);
.platform-game-pricing-field .platform-game-pricing-field__price-label {
display: block;
color: var(--platform-text-strong, #3d1f10);
font: inherit;
font-weight: 500;
font-size: 0.75rem;
font-weight: 700;
line-height: 1.35;
cursor: pointer;
}
.platform-game-pricing-field .platform-game-pricing-field__price-control {
position: relative;
display: flex;
align-items: center;
min-width: 0;
}
/* 边框 / 圆角 / 底色 / 焦点环都来自 `PlatformTextField`(AGC 宿主还会用面板自己的输入框
token 覆盖,与同面板其它输入框一致),这里只补三件事:数字右对齐且等宽、给单位留位、错误态。 */
.platform-game-pricing-field .platform-game-pricing-field__price-input {
padding-right: 2.9rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
letter-spacing: 0.01em;
text-align: right;
}
/* 错误态。网页宿主对子面板内的输入框做了统一 remap,且带 `!important`
(`src/index.css` 的 `.platform-theme :where(.platform-subpanel …) :where(input…)` 规则,
用来把弹窗/子面板里的输入框remap成平台输入框底色)。错误态是必须压过它的例外,所以同权重声明。
不改 `box-shadow`:那是宿主的输入高光与焦点环,压掉会让非法字段失去键盘焦点提示。 */
.platform-game-pricing-field
.platform-game-pricing-field__price-input[aria-invalid='true'] {
border-color: var(--platform-button-danger-text, #a6402f) !important;
background: var(
--platform-button-danger-fill,
rgba(255, 237, 229, 0.94)
) !important;
}
.platform-game-pricing-field .platform-game-pricing-field__price-unit {
position: absolute;
top: 50%;
right: 0.75rem;
transform: translateY(-50%);
color: var(--platform-text-soft, #988476);
font-size: 0.72rem;
font-weight: 700;
line-height: 1;
pointer-events: none;
}
.platform-game-pricing-field
.platform-game-pricing-field__price
input:focus-visible {
border-color: var(--platform-accent, #c7653d);
outline: 2px solid rgba(199, 101, 61, 0.2);
outline-offset: 1px;
.platform-game-pricing-field__price-input[aria-invalid='true']
+ .platform-game-pricing-field__price-unit {
color: var(--platform-button-danger-text, #a6402f);
}
.platform-game-pricing-field .platform-game-pricing-field__hint {
color: var(--platform-text-soft, #988476);
font-size: 0.7rem;
font-weight: 500;
line-height: 1.5;
}
.platform-game-pricing-field .platform-game-pricing-field__error {
color: var(--platform-button-danger-text, #a6402f);
font-size: 0.7rem;
font-size: 0.72rem;
font-weight: 600;
line-height: 1.45;
}
/* 窄屏(≈390px):两枚胶囊等分整行,不靠换行兜底,也不出现横向溢出。 */
@media (max-width: 480px) {
.platform-game-pricing-field .platform-game-pricing-field__modes {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.platform-game-pricing-field .platform-game-pricing-field__option-chip {
width: 100%;
min-width: 0;
}
}
@@ -2,10 +2,13 @@ import './PlatformGamePricingField.css';
import { useId } from 'react';
import { getPlatformCategoryChipClassName } from './platformCategoryChipModel';
import {
PLATFORM_GAME_MAX_PRICE_MUD_POINTS,
type PlatformGamePricingMode,
} from './platformGamePricingModel';
import { PlatformSubpanel } from './PlatformSubpanel';
import { PlatformTextField } from './PlatformTextField';
export type PlatformGamePricingFieldProps = {
/** 付费方式;`free` 等价于 0 泥点。 */
@@ -26,6 +29,11 @@ export type PlatformGamePricingFieldProps = {
*
* 网页发布页与 AGC 发布面板共用同一份表现和文案;组件只表达输入 UI,价格校验、提交
* 与业务状态由使用方持有。上限取自共享常量,两端不各写一份。
*
* 组成全部复用平台原语:外壳是 `PlatformSubpanel`(与网页发布页 `game-publish-fieldset`
* 同族的描边卡片),付费方式是「原生 radio + 标签 chip」,价格是 `PlatformTextField`。
* 两个选项是互斥单选,所以语义仍由原生 radio 承载(读屏的单选组、方向键切换、
* `.checked` / `.disabled`、label 关联全都照旧),chip 只负责外观。
*/
export function PlatformGamePricingField({
mode,
@@ -40,48 +48,92 @@ export function PlatformGamePricingField({
const errorId = `${useId()}-price-error`;
const priceInputId = `${useId()}-price-input`;
return (
<fieldset className="platform-game-pricing-field" disabled={disabled}>
<legend>付费方式</legend>
<div className="platform-game-pricing-field__modes">
<PlatformSubpanel
as="div"
className="platform-game-pricing-field"
radius="xs"
padding="xs"
surface="platform"
title="付费方式"
titleVariant="strong"
>
<div
className="platform-game-pricing-field__modes"
role="radiogroup"
aria-label="付费方式"
>
<label className="platform-game-pricing-field__option">
<input
className="platform-game-pricing-field__option-input"
type="radio"
name={radioName}
checked={mode === 'free'}
disabled={disabled}
onChange={() => onModeChange('free')}
/>
免费
<span
className={[
'platform-game-pricing-field__option-chip',
getPlatformCategoryChipClassName(mode === 'free'),
].join(' ')}
>
免费
</span>
</label>
<label className="platform-game-pricing-field__option">
<input
className="platform-game-pricing-field__option-input"
type="radio"
name={radioName}
checked={mode === 'paid'}
disabled={disabled}
onChange={() => onModeChange('paid')}
/>
买断制
<span
className={[
'platform-game-pricing-field__option-chip',
getPlatformCategoryChipClassName(mode === 'paid'),
].join(' ')}
>
买断制
</span>
</label>
</div>
{mode === 'paid' ? (
<label className="platform-game-pricing-field__price">
买断价(泥点)
<input
id={priceInputId}
type="number"
inputMode="numeric"
value={priceInput}
disabled={disabled}
aria-invalid={error ? true : undefined}
aria-describedby={error ? errorId : undefined}
onChange={(event) => onPriceInputChange(event.target.value)}
/>
<div className="platform-game-pricing-field__price">
<label
className="platform-game-pricing-field__price-label"
htmlFor={priceInputId}
>
买断价(泥点)
</label>
<div className="platform-game-pricing-field__price-control">
<PlatformTextField
id={priceInputId}
className="platform-game-pricing-field__price-input"
type="number"
inputMode="numeric"
size="md"
density="compact"
value={priceInput}
disabled={disabled}
aria-invalid={error ? true : undefined}
aria-describedby={error ? errorId : undefined}
onChange={(event) => onPriceInputChange(event.target.value)}
/>
{/* 单位是价格的视觉附属:label 里已经写了「(泥点)」,这里对读屏重复一次没有意义。 */}
<span
className="platform-game-pricing-field__price-unit"
aria-hidden="true"
>
泥点
</span>
</div>
<span className="platform-game-pricing-field__hint">
买断一次永久拥有;价格随本次版本审核通过后生效,最多{' '}
{PLATFORM_GAME_MAX_PRICE_MUD_POINTS} 泥点
</span>
</label>
</div>
) : null}
{error ? (
<span
@@ -92,6 +144,6 @@ export function PlatformGamePricingField({
{error}
</span>
) : null}
</fieldset>
</PlatformSubpanel>
);
}