打磨封面与截图的文件选择交互
- 网页发布页:封面与截图改用透明 input 覆盖自定义胶囊按钮,点击命中原生控件;上传后不再出现「未选择任何文件」与「已选择」互相矛盾的状态。 - AGC 发布面板:同一处理,按钮文案随状态在「选择封面图片 / 更换封面图片」「添加截图 / 截图已满 6 张」之间切换。 - 文档:实施计划补充真实浏览器发布链路证据(网页上传封面 → my-games 待审 → 版本 frozenMetadata 指向本次上传素材)与交互打磨记录。
This commit is contained in:
+39
-22
@@ -391,14 +391,24 @@ export function GameDistributionPublishPanel({
|
||||
</div>
|
||||
<label>
|
||||
游戏封面(必需)
|
||||
<input
|
||||
ref={coverInputRef}
|
||||
type="file"
|
||||
accept={PANEL_IMAGE_ACCEPT}
|
||||
onChange={(event) => {
|
||||
void handleCoverSelected(event.target.files?.[0] ?? null);
|
||||
}}
|
||||
/>
|
||||
{/*
|
||||
原生 file 控件在清空 value 后会显示“未选择任何文件”,与下方提示互相矛盾;
|
||||
上传后必须清空 value 才能重选同一张图,所以这里改用自定义触发按钮。
|
||||
*/}
|
||||
<span className="game-distribution-publish-panel__picker">
|
||||
<input
|
||||
ref={coverInputRef}
|
||||
type="file"
|
||||
accept={PANEL_IMAGE_ACCEPT}
|
||||
disabled={busy}
|
||||
onChange={(event) => {
|
||||
void handleCoverSelected(event.target.files?.[0] ?? null);
|
||||
}}
|
||||
/>
|
||||
<span className="game-distribution-publish-panel__pick">
|
||||
{cover ? '更换封面图片' : '选择封面图片'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="game-distribution-publish-panel__hint">
|
||||
{cover
|
||||
? `已选择「${cover.name}」;重新选择即可替换封面。`
|
||||
@@ -417,20 +427,27 @@ export function GameDistributionPublishPanel({
|
||||
</div>
|
||||
<label>
|
||||
{`游戏截图(可选,最多 ${MAX_AGC_GAME_SCREENSHOTS} 张)`}
|
||||
<input
|
||||
ref={screenshotInputRef}
|
||||
type="file"
|
||||
accept={PANEL_IMAGE_ACCEPT}
|
||||
multiple
|
||||
disabled={
|
||||
busy || screenshots.length >= MAX_AGC_GAME_SCREENSHOTS
|
||||
}
|
||||
onChange={(event) => {
|
||||
void handleScreenshotsSelected(
|
||||
Array.from(event.target.files ?? []),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className="game-distribution-publish-panel__picker">
|
||||
<input
|
||||
ref={screenshotInputRef}
|
||||
type="file"
|
||||
accept={PANEL_IMAGE_ACCEPT}
|
||||
multiple
|
||||
disabled={
|
||||
busy || screenshots.length >= MAX_AGC_GAME_SCREENSHOTS
|
||||
}
|
||||
onChange={(event) => {
|
||||
void handleScreenshotsSelected(
|
||||
Array.from(event.target.files ?? []),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className="game-distribution-publish-panel__pick">
|
||||
{screenshots.length >= MAX_AGC_GAME_SCREENSHOTS
|
||||
? '截图已满 6 张'
|
||||
: '添加截图'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="game-distribution-publish-panel__hint">
|
||||
截图会按选择顺序展示在游戏详情页;不需要可以留空。
|
||||
</span>
|
||||
|
||||
@@ -3907,6 +3907,59 @@ textarea {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__picker {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 透明 input 覆盖在触发按钮上:真实点击永远命中原生控件,自定义文案只负责显示。 */
|
||||
.game-distribution-publish-panel__picker input[type='file'] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__pick {
|
||||
display: inline-flex;
|
||||
pointer-events: none;
|
||||
align-items: center;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid rgb(104 77 57 / 18%);
|
||||
border-radius: 999px;
|
||||
background: rgb(255 255 255 / 76%);
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: 160ms ease;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__picker:hover
|
||||
.game-distribution-publish-panel__pick {
|
||||
border-color: #a8663d;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__picker
|
||||
input[type='file']:focus-visible
|
||||
+ .game-distribution-publish-panel__pick {
|
||||
outline: 2px solid rgb(168 102 61 / 24%);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__picker
|
||||
input[type='file']:disabled
|
||||
+ .game-distribution-publish-panel__pick {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.game-distribution-publish-panel__cover {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 168px) minmax(0, 1fr);
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
|
||||
- 真实本地栈媒体验证(2026-09-20,`npm run dev:api-server` + `dev:web`,api-server `127.0.0.1:12401`、SpacetimeDB `127.0.0.1:12402`、真实 dev OSS 桶):`E2E_ADMIN_USER=… E2E_ADMIN_PASSWORD=… npm run check:game-distribution-media-e2e`(`scripts/check-game-distribution-media-e2e.mjs`)27 项全部通过——真实直传封面与 2 张截图素材、缺封面 400、截图 7 张 400、不存在素材 400(创建游戏与创建版本两处)、他人素材 403、创建游戏/版本/上传/送审(202 + `pending_review`)、作者回读拿到 `frozenMetadata` 且素材 ID 与顺序一致、待审期间公开目录不含该游戏且封面匿名读返回 404、管理员审核通过后公开投影带封面与截图对象键且不含素材 ID、匿名 `read-url` 对封面与截图都返回签名地址、发行网关 `index.html` 与 `assets/app.js` 均 200 且带 nosniff。
|
||||
- 真实浏览器展示验证(同一栈,headless Chromium `390x844` 与 `1280x900`):`/games` 卡片渲染真实封面签名地址(无占位回退),`/games/detail?id=…` hero 显示封面、缩略图条渲染「封面 + 2 张截图」,点击第 3 张后 `aria-pressed` 与 hero 图片同步切换;移动端布局正常。截图存于 `~/data/tmp/gd3/`(不入库)。
|
||||
- 网页发布页真实上传验证(同一栈 + headless Chromium):在 `/games/publish` 里用作者会话填写资料、选择真实 PNG 封面与 ZIP 并提交,页面返回「已提交审核」;随后 `my-games` 显示该游戏 `pending_review`,版本回读的 `frozenMetadata.coverAssetId` / `coverObjectKey` 正是本次浏览器上传的素材,证明网页端封面直传(凭证 → OSS → confirm)与冻结链路真实可用。
|
||||
- 发布资料入口交互打磨:原生 file 控件在上传后清空 value 时会显示「未选择任何文件」,与「已选择」提示互相矛盾;网页发布页与 AGC 面板都改成透明 input 覆盖自定义胶囊按钮(点击命中原生控件,文案显示「选择/更换封面图片」「添加截图」),并在验证中用 `elementFromPoint` 确认点击命中的是 `input[type=file]`。
|
||||
- 创建游戏同步校验素材归属:此前 `POST /api/game-distribution/games` 只校验「封面必填 / 截图 ≤6 / ID 非空」,素材是否存在与是否属于当前作者要等到创建版本才失败,游戏行会先落一个无效素材 ID;现在创建游戏与创建版本都走 `resolve_owned_game_media`,不存在返回 400、他人素材返回 403。
|
||||
|
||||
## 尚未完成
|
||||
|
||||
@@ -851,15 +851,26 @@ export function GamePublishPage({
|
||||
<div className="game-publish-media__copy">
|
||||
<label className="game-publish-field">
|
||||
游戏封面(必需)
|
||||
<input
|
||||
ref={coverInputRef}
|
||||
type="file"
|
||||
accept={GAME_PUBLISH_IMAGE_ACCEPT}
|
||||
disabled={isResumingDraft || isSubmitting || isUploadingAsset}
|
||||
onChange={(event) => {
|
||||
void handleCoverSelected(event.target.files?.[0] ?? null);
|
||||
}}
|
||||
/>
|
||||
{/*
|
||||
原生 file 控件在清空 value 后会显示“未选择任何文件”,与下方的“已选择”提示互相
|
||||
矛盾;上传后必须清空 value 才能重选同一张图,所以这里改用自定义触发按钮。
|
||||
*/}
|
||||
<span className="game-publish-media__picker">
|
||||
<input
|
||||
ref={coverInputRef}
|
||||
type="file"
|
||||
accept={GAME_PUBLISH_IMAGE_ACCEPT}
|
||||
disabled={
|
||||
isResumingDraft || isSubmitting || isUploadingAsset
|
||||
}
|
||||
onChange={(event) => {
|
||||
void handleCoverSelected(event.target.files?.[0] ?? null);
|
||||
}}
|
||||
/>
|
||||
<span className="game-publish-media__pick">
|
||||
{coverAsset ? '更换封面图片' : '选择封面图片'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="game-publish-hint">
|
||||
{coverAsset?.assetObjectId
|
||||
? `已选择「${coverAsset.name}」;重新选择即可替换本次发布的封面。`
|
||||
@@ -883,23 +894,30 @@ export function GamePublishPage({
|
||||
<div className="game-publish-media__screenshots">
|
||||
<label className="game-publish-field">
|
||||
{`游戏截图(可选,最多 ${MAX_GAME_SCREENSHOTS} 张)`}
|
||||
<input
|
||||
ref={screenshotInputRef}
|
||||
type="file"
|
||||
accept={GAME_PUBLISH_IMAGE_ACCEPT}
|
||||
multiple
|
||||
disabled={
|
||||
isResumingDraft ||
|
||||
isSubmitting ||
|
||||
isUploadingAsset ||
|
||||
screenshotAssets.length >= MAX_GAME_SCREENSHOTS
|
||||
}
|
||||
onChange={(event) => {
|
||||
void handleScreenshotsSelected(
|
||||
Array.from(event.target.files ?? []),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className="game-publish-media__picker">
|
||||
<input
|
||||
ref={screenshotInputRef}
|
||||
type="file"
|
||||
accept={GAME_PUBLISH_IMAGE_ACCEPT}
|
||||
multiple
|
||||
disabled={
|
||||
isResumingDraft ||
|
||||
isSubmitting ||
|
||||
isUploadingAsset ||
|
||||
screenshotAssets.length >= MAX_GAME_SCREENSHOTS
|
||||
}
|
||||
onChange={(event) => {
|
||||
void handleScreenshotsSelected(
|
||||
Array.from(event.target.files ?? []),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className="game-publish-media__pick">
|
||||
{screenshotAssets.length >= MAX_GAME_SCREENSHOTS
|
||||
? '截图已满 6 张'
|
||||
: '添加截图'}
|
||||
</span>
|
||||
</span>
|
||||
<span className="game-publish-hint">
|
||||
截图会按选择顺序展示在详情页;不需要可以留空。
|
||||
</span>
|
||||
|
||||
@@ -924,6 +924,52 @@
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.game-publish-media__picker {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* 透明 input 覆盖在触发按钮上:真实点击永远命中原生控件,自定义文案只负责显示。 */
|
||||
.game-publish-media__picker input[type='file'] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
.game-publish-media__pick {
|
||||
display: inline-flex;
|
||||
pointer-events: none;
|
||||
align-items: center;
|
||||
padding: 0.35rem 0.85rem;
|
||||
border: 1px solid var(--platform-surface-border);
|
||||
border-radius: 999px;
|
||||
background: var(--platform-panel-fill);
|
||||
color: var(--platform-text-strong);
|
||||
font-size: 0.74rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: 160ms ease;
|
||||
}
|
||||
.game-publish-media__picker:hover .game-publish-media__pick {
|
||||
border-color: rgba(199, 101, 61, 0.45);
|
||||
}
|
||||
.game-publish-media__picker
|
||||
input[type='file']:focus-visible
|
||||
+ .game-publish-media__pick {
|
||||
outline: 2px solid rgba(199, 101, 61, 0.35);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.game-publish-media__picker
|
||||
input[type='file']:disabled
|
||||
+ .game-publish-media__pick {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.game-publish-media__screenshots {
|
||||
display: grid;
|
||||
gap: 0.6rem;
|
||||
|
||||
Reference in New Issue
Block a user