51cb05f418
Project CI / AI game creator shell Rust crates (push) Successful in 5m56s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m36s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 7m37s
Project CI / Backend tests (push) Successful in 9m22s
Project CI / Frontend tests (push) Successful in 4m40s
Project CI / AI game creator shell web tests (push) Successful in 3m50s
Project CI / Repository checks (push) Successful in 6m30s
Project CI / Native shell tests (push) Successful in 8m32s
本 PR 补齐创作者主页与关注粉丝功能的产品合同和工程设计,供后续按里程碑实施;本次只修改文档。 桌面导航第四项进入自己的创作者主页,“我的”移到第五项。游戏详情可进入作者主页并关注/取消关注;自己或他人的关注、粉丝列表均公开可查看,点击用户头像或昵称可进入其主页。本人支持取消/重新关注、回关和移除粉丝,并明确两个关注方向独立。 工程设计包含关系表与 DTO、认证和事务边界、跨页状态同步、共享组件、深链及验收要求,拆分关系查询和页面交互两个里程碑。按用户要求,游戏列表沿用最多 48 项的现有限制,不增加游戏目录分页改造。 验证: - 已合入最新 origin/master(21c79f54a);共享决策记录冲突保留双方新增内容。 - npm run check:encoding 通过。 - npm run check:doc-index 通过。 - git diff --check 与暂存差异检查通过。 - 相对 master 仅六份文档发生变化;没有业务代码或 schema 修改,未执行业务测试或部署。 Reviewed-on: #638 Co-authored-by: Linghong <ink29535@proton.me> Co-committed-by: Linghong <ink29535@proton.me>
141 lines
4.3 KiB
TypeScript
141 lines
4.3 KiB
TypeScript
import { useCallback, useEffect, useState } from 'react';
|
|
|
|
import { useAuthUi } from './components/auth/AuthUiContext';
|
|
import type { SelectionStage } from './components/platform-entry/platformEntryActiveTypes';
|
|
import { PlatformEntryFlowShell } from './components/platform-entry/PlatformEntryFlowShell';
|
|
import { useHostNavigationCanGoBack } from './hooks/useHostNavigationCanGoBack';
|
|
import {
|
|
isAppHistoryState,
|
|
isKnownMainAppPagePath,
|
|
normalizeAppPath,
|
|
pushAppHistoryPath,
|
|
replaceAppHistoryPath,
|
|
resolveInitialSelectionStageFromPath,
|
|
resolvePathForSelectionStage,
|
|
shouldRedirectEditorCanvasWithoutProject,
|
|
} from './routing/activeAppPageRoutes';
|
|
import {
|
|
resolveAppTitleForSelectionStage,
|
|
syncAppTitle,
|
|
} from './services/activeAppTitle';
|
|
import {
|
|
refreshNativeAppHostRuntime,
|
|
subscribeHostRuntimeChange,
|
|
} from './services/host-bridge/hostBridge';
|
|
|
|
function resolveInitialAppSelectionStage() {
|
|
if (
|
|
shouldRedirectEditorCanvasWithoutProject(
|
|
window.location.pathname,
|
|
window.location.search,
|
|
)
|
|
) {
|
|
replaceAppHistoryPath('/project');
|
|
return 'project';
|
|
}
|
|
|
|
if (!isKnownMainAppPagePath(window.location.pathname)) {
|
|
replaceAppHistoryPath('/');
|
|
return 'platform';
|
|
}
|
|
|
|
return resolveInitialSelectionStageFromPath(window.location.pathname, false);
|
|
}
|
|
|
|
export default function App() {
|
|
const authUi = useAuthUi();
|
|
const hostNavigation = useHostNavigationCanGoBack();
|
|
const [, setLocationRevision] = useState(0);
|
|
const [, setHostRuntimeRevision] = useState(0);
|
|
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(
|
|
resolveInitialAppSelectionStage,
|
|
);
|
|
|
|
const setSelectionStage = useCallback(
|
|
(stage: SelectionStage, options?: { path?: string }) => {
|
|
setRawSelectionStage(stage);
|
|
setLocationRevision((revision) => revision + 1);
|
|
pushAppHistoryPath(options?.path ?? resolvePathForSelectionStage(stage));
|
|
},
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const unsubscribe = subscribeHostRuntimeChange(() => {
|
|
setHostRuntimeRevision((revision) => revision + 1);
|
|
});
|
|
|
|
void refreshNativeAppHostRuntime();
|
|
|
|
return unsubscribe;
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const syncStageFromHistory = () => {
|
|
// 同一舞台切换用户、列表方向或浏览器返回也要重读 URL。
|
|
setLocationRevision((revision) => revision + 1);
|
|
if (
|
|
shouldRedirectEditorCanvasWithoutProject(
|
|
window.location.pathname,
|
|
window.location.search,
|
|
)
|
|
) {
|
|
replaceAppHistoryPath('/project');
|
|
setRawSelectionStage('project');
|
|
return;
|
|
}
|
|
if (!isKnownMainAppPagePath(window.location.pathname)) {
|
|
replaceAppHistoryPath('/');
|
|
setRawSelectionStage('platform');
|
|
return;
|
|
}
|
|
setRawSelectionStage(
|
|
resolveInitialSelectionStageFromPath(window.location.pathname, false),
|
|
);
|
|
};
|
|
|
|
window.addEventListener('popstate', syncStageFromHistory);
|
|
return () => window.removeEventListener('popstate', syncStageFromHistory);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (
|
|
!hostNavigation.isSupported ||
|
|
hostNavigation.canGoBack ||
|
|
selectionStage === 'platform' ||
|
|
isAppHistoryState(window.history.state)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
const currentPath = normalizeAppPath(window.location.pathname);
|
|
const currentSearch = window.location.search;
|
|
|
|
replaceAppHistoryPath('/');
|
|
pushAppHistoryPath(`${currentPath}${currentSearch}`);
|
|
}, [hostNavigation.canGoBack, hostNavigation.isSupported, selectionStage]);
|
|
const platformThemeClass =
|
|
authUi?.platformTheme === 'dark'
|
|
? 'platform-theme--dark'
|
|
: 'platform-theme--light';
|
|
const isImageEditorStage = selectionStage === 'image-editor';
|
|
const platformShellSurfaceClass = isImageEditorStage
|
|
? 'bg-white p-0'
|
|
: 'bg-[image:var(--platform-body-fill)] p-2 sm:p-4';
|
|
|
|
useEffect(() => {
|
|
syncAppTitle(resolveAppTitleForSelectionStage(selectionStage));
|
|
}, [selectionStage]);
|
|
|
|
return (
|
|
<div
|
|
className={`platform-ui-shell platform-viewport-shell platform-theme ${platformThemeClass} flex flex-col overflow-hidden ${platformShellSurfaceClass} font-sans text-[var(--platform-text-strong)]`}
|
|
>
|
|
<PlatformEntryFlowShell
|
|
selectionStage={selectionStage}
|
|
setSelectionStage={setSelectionStage}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|