Fix reward code modal and mobile viewport dock
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -207,4 +207,12 @@
|
||||
|
||||
---
|
||||
|
||||
## 18. 2026-04-30 资料兑换码弹窗响应式修正
|
||||
|
||||
- `RpgEntryHomeView.tsx` 的兑换码弹窗现在抽成同一份 `rewardCodeModal`,桌面与移动端分支都挂载,避免竖屏点击头像右侧“兑换码”后只更新状态但不显示窗口。
|
||||
- `src/index.css` 已补齐 `platform-modal-backdrop`、`platform-recharge-modal`、`platform-profile-input`、`platform-primary-button`、`platform-modal-close` 与兑换结果提示样式;后续资料类轻量弹窗可以复用这组类接入平台主题背景。
|
||||
- 兑换码窗口仍只保留输入框、兑换按钮和后端返回提示,不新增规则说明文案。
|
||||
|
||||
---
|
||||
|
||||
_文档目的:交接给下一个 Agent 时,优先读本文件 + `UI_CODING_STANDARD.md`,再改 `uiAssets.ts` / `App.tsx` / `index.css`。_
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# 平台首页移动端底部 Dock 可见视口修复
|
||||
|
||||
## 背景
|
||||
|
||||
手机浏览器会把顶部地址栏纳入传统 `100vh` 的计算,导致平台首页根容器高于真实可见区域。底部 dock 虽然在 flex 布局末尾,但会被推到浏览器可见区域之外,用户需要滚动或收起地址栏后才能看到。
|
||||
|
||||
## 落地口径
|
||||
|
||||
- 平台入口根壳统一使用 `.platform-viewport-shell`,优先按 `100dvh` 约束高度,旧浏览器回退到 `100vh`。
|
||||
- 移动端首页底部 dock 使用 `.platform-mobile-bottom-dock` 固定在可见视口底部,并叠加 `safe-area-inset-bottom`。
|
||||
- 移动端首页内容壳通过 `--platform-bottom-dock-outer-height` 预留底部空间,避免滚动内容被固定 dock 遮挡。
|
||||
- 不新增 UI 说明文案,不改变底部导航业务语义。
|
||||
|
||||
## 验收
|
||||
|
||||
- 手机竖屏打开平台首页时,底部 dock 始终贴住浏览器可见区域底部。
|
||||
- 浏览器地址栏展开时,dock 不应被挤到屏幕外。
|
||||
- 主页、分类、创作、存档、我的五个 tab 均保持原有点击行为。
|
||||
@@ -5,6 +5,7 @@
|
||||
## 文档列表
|
||||
|
||||
- [SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md](./SPACETIMEDB_SCHEMA_CHANGE_CONSTRAINTS.md):冻结 SpacetimeDB 表结构变更约束、自动迁移可接受范围、冲突后的系统行为,以及保留旧数据的增量迁移流程;凡涉及 `spacetime publish`、表字段调整或 `migration.rs` 对齐时优先参考。
|
||||
- [PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md](./PLATFORM_MOBILE_BOTTOM_DOCK_VIEWPORT_FIX_2026-04-30.md):记录平台首页底部 dock 在手机浏览器地址栏展开时脱离可见区域的根因,以及 `100dvh`、固定底部锚点和安全区占位的修复口径。
|
||||
- [SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md](./SPACETIMEDB_JSON_STRING_MIGRATION_PROCEDURE_2026-04-27.md):记录 SpacetimeDB private 表迁移 JSON 导出/导入 procedure、迁移操作员授权、HTTP 413 分片导入、Jenkins 自动迁移回灌和导入脚本参数。
|
||||
- [JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md](./JENKINS_SPACETIMEDB_DATABASE_MIGRATION_PIPELINES_2026-04-29.md):记录 `Genarrative-Database-Export` / `Genarrative-Database-Import` 两条 SCM-backed 数据库迁移流水线参数、默认 dry-run、token 边界和 `CHUNK_SIZE` 413 规避参数。
|
||||
- [RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md](./RPG_PROMPT_FRONTEND_REMOVAL_AND_SERVER_RS_MIGRATION_2026-04-28.md):冻结 RPG 提示词禁止存在前端的边界,明确前端只保留 API client,角色私聊/NPC 对话/剧情续写等 prompt 统一收口到 `server-rs`。
|
||||
|
||||
BIN
logs/codex-mobile-dock-fix.png
Normal file
BIN
logs/codex-mobile-dock-fix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 992 KiB |
11
src/App.tsx
11
src/App.tsx
@@ -1,4 +1,11 @@
|
||||
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
lazy,
|
||||
Suspense,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { useAuthUi } from './components/auth/AuthUiContext';
|
||||
import { PlatformEntryFlowShell } from './components/platform-entry/PlatformEntryFlowShell';
|
||||
@@ -133,7 +140,7 @@ export default function App() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`platform-ui-shell platform-theme ${platformThemeClass} flex h-screen max-h-screen flex-col overflow-hidden bg-[image:var(--platform-body-fill)] p-2 font-sans text-[var(--platform-text-strong)] sm:p-4`}
|
||||
className={`platform-ui-shell platform-viewport-shell platform-theme ${platformThemeClass} flex flex-col overflow-hidden bg-[image:var(--platform-body-fill)] p-2 font-sans text-[var(--platform-text-strong)] sm:p-4`}
|
||||
>
|
||||
<PlatformEntryFlowShell
|
||||
selectionStage={selectionStage}
|
||||
|
||||
@@ -464,6 +464,18 @@ test('wallet ledger modal shows empty and error states', async () => {
|
||||
expect(screen.getByText('重新加载')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('opens reward code modal from profile action on mobile', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
renderProfileView();
|
||||
await user.click(screen.getByRole('button', { name: /兑换码/u }));
|
||||
|
||||
const modal = await screen.findByPlaceholderText('输入兑换码');
|
||||
expect(modal).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '兑换' })).toBeTruthy();
|
||||
expect(screen.getByLabelText('关闭兑换码')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('shows a reachable login entry in logged out mobile shell', async () => {
|
||||
const user = userEvent.setup();
|
||||
const openLoginModal = vi.fn();
|
||||
|
||||
@@ -1788,7 +1788,7 @@ function RewardCodeRedeemModal({
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="platform-modal-backdrop fixed inset-0 z-50 flex items-center justify-center px-4 py-6">
|
||||
<div className="platform-modal-backdrop fixed inset-0 z-[80] flex items-center justify-center px-4 py-6">
|
||||
<div className="platform-recharge-modal w-full max-w-sm overflow-hidden rounded-[1.4rem]">
|
||||
<div className="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
||||
<div className="text-base font-black">兑换码</div>
|
||||
@@ -3448,6 +3448,17 @@ export function RpgEntryHomeView({
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
const rewardCodeModal: ReactNode = isRewardCodeOpen ? (
|
||||
<RewardCodeRedeemModal
|
||||
value={rewardCodeInput}
|
||||
isSubmitting={isSubmittingRewardCode}
|
||||
error={rewardCodeError}
|
||||
success={rewardCodeSuccess}
|
||||
onChange={setRewardCodeInput}
|
||||
onSubmit={submitRewardCode}
|
||||
onClose={() => setIsRewardCodeOpen(false)}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
if (!isDesktopLayout) {
|
||||
return (
|
||||
@@ -3471,10 +3482,9 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-3 min-w-0 shrink-0 border-t pt-2"
|
||||
className="platform-mobile-bottom-dock mt-3 min-w-0 shrink-0 border-t pt-2"
|
||||
style={{
|
||||
borderColor: 'var(--platform-line-soft)',
|
||||
paddingBottom: 'calc(env(safe-area-inset-bottom) + 0.2rem)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
@@ -3507,6 +3517,7 @@ export function RpgEntryHomeView({
|
||||
onSubmitRedeem={submitReferralInviteCode}
|
||||
/>
|
||||
) : null}
|
||||
{rewardCodeModal}
|
||||
{isProfilePlayStatsOpen ? (
|
||||
<ProfilePlayedWorksModal
|
||||
stats={profilePlayStats}
|
||||
@@ -3604,17 +3615,7 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{isRewardCodeOpen ? (
|
||||
<RewardCodeRedeemModal
|
||||
value={rewardCodeInput}
|
||||
isSubmitting={isSubmittingRewardCode}
|
||||
error={rewardCodeError}
|
||||
success={rewardCodeSuccess}
|
||||
onChange={setRewardCodeInput}
|
||||
onSubmit={submitRewardCode}
|
||||
onClose={() => setIsRewardCodeOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
{rewardCodeModal}
|
||||
{profilePopupPanel ? (
|
||||
<ProfileReferralModal
|
||||
panel={profilePopupPanel}
|
||||
|
||||
114
src/index.css
114
src/index.css
@@ -21,6 +21,10 @@
|
||||
:root {
|
||||
--ui-scale: clamp(0.78, 0.72 + 0.45vw, 1.06);
|
||||
--platform-bottom-nav-height: 3.5rem;
|
||||
--platform-bottom-dock-outer-height: calc(
|
||||
var(--platform-bottom-nav-height) + env(safe-area-inset-bottom, 0px) +
|
||||
1.15rem
|
||||
);
|
||||
--platform-bottom-nav-padding: 0.25rem;
|
||||
--platform-bottom-nav-gap: 0.25rem;
|
||||
--platform-bottom-nav-radius: 1.2rem;
|
||||
@@ -50,6 +54,20 @@ body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.platform-viewport-shell {
|
||||
height: 100vh;
|
||||
max-height: 100vh;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@supports (height: 100dvh) {
|
||||
.platform-viewport-shell {
|
||||
height: 100dvh;
|
||||
max-height: 100dvh;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes character-animator-portrait-death-fall {
|
||||
0% {
|
||||
transform: translateY(0) rotate(0deg) scaleX(1) scale(1);
|
||||
@@ -754,6 +772,10 @@ body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.platform-mobile-bottom-dock {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.platform-tab-panel {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@@ -1707,6 +1729,20 @@ body {
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
|
||||
.platform-modal-backdrop {
|
||||
background: var(--platform-overlay-fill);
|
||||
color: var(--platform-text-strong);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.platform-recharge-modal {
|
||||
border: 1px solid var(--platform-modal-border);
|
||||
background: var(--platform-modal-fill);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1),
|
||||
0 24px 80px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.platform-overlay {
|
||||
background: var(--platform-overlay-fill);
|
||||
}
|
||||
@@ -1790,6 +1826,20 @@ body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.platform-mobile-entry-shell {
|
||||
box-sizing: border-box;
|
||||
padding-bottom: var(--platform-bottom-dock-outer-height);
|
||||
}
|
||||
|
||||
.platform-mobile-bottom-dock {
|
||||
position: fixed;
|
||||
right: max(0.75rem, env(safe-area-inset-right, 0px));
|
||||
bottom: 0;
|
||||
left: max(0.75rem, env(safe-area-inset-left, 0px));
|
||||
z-index: 60;
|
||||
padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 0.5rem);
|
||||
}
|
||||
|
||||
.platform-mobile-home-stage {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
@@ -2191,6 +2241,70 @@ body {
|
||||
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
|
||||
}
|
||||
|
||||
.platform-profile-input {
|
||||
border: 1px solid var(--platform-subpanel-border);
|
||||
background: var(--platform-input-fill);
|
||||
color: var(--platform-text-strong);
|
||||
outline: none;
|
||||
box-shadow: inset 0 1px 0 var(--platform-input-highlight);
|
||||
transition:
|
||||
border-color 180ms ease,
|
||||
background-color 180ms ease,
|
||||
box-shadow 180ms ease;
|
||||
}
|
||||
|
||||
.platform-profile-input::placeholder {
|
||||
color: var(--platform-text-soft);
|
||||
}
|
||||
|
||||
.platform-profile-input:focus {
|
||||
border-color: var(--platform-nav-active-border);
|
||||
background: var(--platform-input-fill-focus);
|
||||
box-shadow: 0 0 0 3px var(--platform-input-focus-ring);
|
||||
}
|
||||
|
||||
.platform-primary-button {
|
||||
border: 1px solid var(--platform-button-primary-border);
|
||||
background: var(--platform-button-primary-fill);
|
||||
color: var(--platform-button-primary-text);
|
||||
box-shadow: var(--platform-profile-action-shadow);
|
||||
transition:
|
||||
transform 180ms ease,
|
||||
filter 180ms ease,
|
||||
opacity 180ms ease;
|
||||
}
|
||||
|
||||
.platform-primary-button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
filter: brightness(1.02);
|
||||
}
|
||||
|
||||
.platform-modal-close {
|
||||
background: var(--platform-profile-chip-fill);
|
||||
color: var(--platform-profile-chip-text);
|
||||
transition:
|
||||
background-color 180ms ease,
|
||||
color 180ms ease,
|
||||
transform 180ms ease;
|
||||
}
|
||||
|
||||
.platform-modal-close:hover {
|
||||
background: var(--platform-profile-chip-hover-fill);
|
||||
color: var(--platform-text-strong);
|
||||
}
|
||||
|
||||
.platform-profile-error {
|
||||
border: 1px solid var(--platform-button-danger-border);
|
||||
background: var(--platform-button-danger-fill);
|
||||
color: var(--platform-button-danger-text);
|
||||
}
|
||||
|
||||
.platform-profile-success {
|
||||
border: 1px solid var(--platform-success-border);
|
||||
background: var(--platform-success-bg);
|
||||
color: var(--platform-success-text);
|
||||
}
|
||||
|
||||
.platform-profile-hero {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--platform-profile-hero-border);
|
||||
|
||||
Reference in New Issue
Block a user