Merge remote-tracking branch 'origin/master' into fix/mobile-style
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m55s
Project CI / Backend tests (pull_request) Successful in 4m9s
Project CI / Frontend tests (pull_request) Successful in 2m15s
Project CI / Native shell tests (pull_request) Successful in 6m6s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m53s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m38s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m44s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 1m29s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m55s
Project CI / Backend tests (pull_request) Successful in 4m9s
Project CI / Frontend tests (pull_request) Successful in 2m15s
Project CI / Native shell tests (pull_request) Successful in 6m6s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 8m53s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 9m38s
Project CI / AI game creator shell web tests (pull_request) Successful in 1m45s
Project CI / Repository checks (pull_request) Successful in 2m44s
# Conflicts: # docs/project-memory/shared-memory/decision-log.md # docs/project-memory/shared-memory/pitfalls.md
This commit is contained in:
@@ -208,9 +208,6 @@ describe('CreationLandingView', () => {
|
||||
expect(subtitle.textContent).toContain('美术 Agent');
|
||||
expect(subtitle.textContent).toContain('无限画布');
|
||||
expect(subtitle.textContent).toContain('角色、场景、UI 与宣发素材');
|
||||
expect(
|
||||
screen.getByText('登录即送 100 泥点,可以免费制作 50 个素材'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
level: 2,
|
||||
|
||||
@@ -1418,9 +1418,6 @@ export function CreationLandingView({
|
||||
<br />
|
||||
用美术 Agent 与无限画布,快速制作角色、场景、UI 与宣发素材。
|
||||
</p>
|
||||
<p className="creation-landing__hero-benefit">
|
||||
登录即送 100 泥点,可以免费制作 50 个素材
|
||||
</p>
|
||||
</div>
|
||||
<div className="creation-landing__hero-actions">
|
||||
<PlatformActionButton
|
||||
|
||||
@@ -146,7 +146,13 @@ export function GameGalleryPage({
|
||||
const requestSerialRef = useRef(0);
|
||||
const scrollContainerRef = useRef<HTMLElement | null>(null);
|
||||
const hasRestoredScrollRef = useRef(false);
|
||||
const didSyncPropKeywordRef = useRef(false);
|
||||
/**
|
||||
* 顶栏搜索词只在「真的变了」时覆盖目录筛选。
|
||||
*
|
||||
* 不能用一次性的 did-初始化标记:开发态 StrictMode 会把挂载 effect 跑两遍,
|
||||
* 第二遍就会拿空 prop 把刚从会话存储恢复出来的关键词清掉(返回目录上下文丢失)。
|
||||
*/
|
||||
const lastPropKeywordRef = useRef(searchKeyword);
|
||||
|
||||
const filters = useMemo<GameGalleryFilters>(
|
||||
() => ({ keyword: activeKeyword, category, device }),
|
||||
@@ -182,10 +188,8 @@ export function GameGalleryPage({
|
||||
}, [activeKeyword, category]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!didSyncPropKeywordRef.current) {
|
||||
didSyncPropKeywordRef.current = true;
|
||||
return;
|
||||
}
|
||||
if (lastPropKeywordRef.current === searchKeyword) return;
|
||||
lastPropKeywordRef.current = searchKeyword;
|
||||
setKeywordInput(searchKeyword);
|
||||
setActiveKeyword(searchKeyword);
|
||||
}, [searchKeyword]);
|
||||
|
||||
@@ -196,6 +196,19 @@ export function GamePublishPage({
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [publishedGameId, setPublishedGameId] = useState('');
|
||||
/**
|
||||
* 同步的在途提交标记与稳定的幂等键。
|
||||
*
|
||||
* `isSubmitting` 是 React 状态,双击发生在同一次渲染窗口里时还没生效;再叠上
|
||||
* `prepareGamePackage` 的压缩耗时,两次点击会各建一份游戏。这里用 ref 同步拦截,
|
||||
* 并让同一次发布(含失败后重试)复用同一组 Idempotency-Key,服务端按幂等重放。
|
||||
*/
|
||||
const submitInFlightRef = useRef(false);
|
||||
const publishKeyRef = useRef<string | null>(null);
|
||||
const resolvePublishKey = () => {
|
||||
if (!publishKeyRef.current) publishKeyRef.current = createPublishKey();
|
||||
return publishKeyRef.current;
|
||||
};
|
||||
const [draft, setDraft] = useState<GamePublishDraft | null>(null);
|
||||
const [draftDetail, setDraftDetail] =
|
||||
useState<GameDistributionVersionDetail | null>(null);
|
||||
@@ -420,10 +433,12 @@ export function GamePublishPage({
|
||||
setError('请选择上次那个发行包 ZIP 继续上传');
|
||||
return;
|
||||
}
|
||||
if (submitInFlightRef.current) return;
|
||||
submitInFlightRef.current = true;
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
try {
|
||||
const resumeKey = createPublishKey();
|
||||
const resumeKey = resolvePublishKey();
|
||||
if (draftDetail.version.recoveryAction !== 'submit') {
|
||||
const prepared = await prepareGamePackage(packageFile as File);
|
||||
await uploadGamePackage(
|
||||
@@ -444,11 +459,13 @@ export function GamePublishPage({
|
||||
setDraftDetail(null);
|
||||
setIsResumingDraft(false);
|
||||
setPublishedGameId(draftDetail.game.id);
|
||||
publishKeyRef.current = null;
|
||||
} catch (resumeError: unknown) {
|
||||
setError(
|
||||
resumeError instanceof Error ? resumeError.message : '继续发布失败',
|
||||
);
|
||||
} finally {
|
||||
submitInFlightRef.current = false;
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
@@ -482,11 +499,13 @@ export function GamePublishPage({
|
||||
setError('请选择发行包 ZIP');
|
||||
return;
|
||||
}
|
||||
if (submitInFlightRef.current) return;
|
||||
submitInFlightRef.current = true;
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
try {
|
||||
const prepared = await prepareGamePackage(packageFile);
|
||||
const updateKey = createPublishKey();
|
||||
const updateKey = resolvePublishKey();
|
||||
const inputModes: GameDistributionInputMode[] = ['keyboard', 'mouse'];
|
||||
if (deviceSupport.touch) inputModes.push('touch');
|
||||
const tags = tagsInput
|
||||
@@ -535,11 +554,13 @@ export function GamePublishPage({
|
||||
? { ...current, versionNumber: version.versionNumber }
|
||||
: current,
|
||||
);
|
||||
publishKeyRef.current = null;
|
||||
} catch (updateError: unknown) {
|
||||
setError(
|
||||
updateError instanceof Error ? updateError.message : '发布新版本失败',
|
||||
);
|
||||
} finally {
|
||||
submitInFlightRef.current = false;
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
@@ -573,6 +594,8 @@ export function GamePublishPage({
|
||||
setError('请选择发行包 ZIP');
|
||||
return;
|
||||
}
|
||||
if (submitInFlightRef.current) return;
|
||||
submitInFlightRef.current = true;
|
||||
setIsSubmitting(true);
|
||||
setError('');
|
||||
try {
|
||||
@@ -596,7 +619,7 @@ export function GamePublishPage({
|
||||
inputModes,
|
||||
orientation,
|
||||
};
|
||||
const publishKey = createPublishKey();
|
||||
const publishKey = resolvePublishKey();
|
||||
const game = await createGame(gameMetadata, `${publishKey}:game`);
|
||||
const version = await createGameVersion(
|
||||
game.id,
|
||||
@@ -631,11 +654,13 @@ export function GamePublishPage({
|
||||
);
|
||||
clearPublishDraft();
|
||||
setPublishedGameId(game.id);
|
||||
publishKeyRef.current = null;
|
||||
} catch (submitError: unknown) {
|
||||
setError(
|
||||
submitError instanceof Error ? submitError.message : '发布游戏失败',
|
||||
);
|
||||
} finally {
|
||||
submitInFlightRef.current = false;
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-15
@@ -2945,8 +2945,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
gap: 0.42rem;
|
||||
}
|
||||
|
||||
.creation-landing__hero-subtitle,
|
||||
.creation-landing__hero-benefit {
|
||||
.creation-landing__hero-subtitle {
|
||||
margin: 0;
|
||||
color: #8a7466;
|
||||
text-indent: 0;
|
||||
@@ -2959,14 +2958,6 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.creation-landing__hero-benefit {
|
||||
color: #9a6f56;
|
||||
font-size: 16px;
|
||||
font-weight: 680;
|
||||
letter-spacing: 0.08em;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.creation-landing__hero-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -3934,11 +3925,6 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
line-height: 1.62;
|
||||
}
|
||||
|
||||
.creation-landing__hero-benefit {
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.creation-landing__hero-actions {
|
||||
gap: 0.72rem;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ describe('frontendRuntimeConfigService', () => {
|
||||
'读取前端运行时配置失败',
|
||||
{
|
||||
authImpact: 'local',
|
||||
skipAuth: true,
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
|
||||
@@ -19,7 +19,8 @@ export async function loadFrontendRuntimeConfig() {
|
||||
'读取前端运行时配置失败',
|
||||
{
|
||||
authImpact: 'local',
|
||||
skipAuth: true,
|
||||
// 必须带上登录态:发布灰度按作者判定,匿名请求恒为 false,
|
||||
// 否则一旦显式配置过 `game-distribution:publish`,网页端就再也拿不到发布入口。
|
||||
skipRefresh: true,
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
|
||||
Reference in New Issue
Block a user