拆分大文件
This commit is contained in:
@@ -16,6 +16,7 @@ const baseUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
username: 'tester',
|
||||
displayName: '138****8000',
|
||||
publicUserCode: 'user-tester',
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'phone',
|
||||
bindingStatus: 'active',
|
||||
|
||||
@@ -70,6 +70,7 @@ const mockUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
publicUserCode: 'user-tester',
|
||||
phoneNumberMasked: '138****8000',
|
||||
loginMethod: 'phone',
|
||||
bindingStatus: 'active',
|
||||
|
||||
@@ -27,8 +27,16 @@ import type {
|
||||
} from '../../../packages/shared/src/contracts/puzzleAgentSession';
|
||||
import type { PuzzleRunSnapshot } from '../../../packages/shared/src/contracts/puzzleRuntimeSession';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type {
|
||||
CustomWorldGalleryCard,
|
||||
CustomWorldLibraryEntry,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { PublicUserSummary } from '../../../packages/shared/src/contracts/auth';
|
||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||
import {
|
||||
getPublicAuthUserByCode,
|
||||
getPublicAuthUserById,
|
||||
} from '../../services/authService';
|
||||
import {
|
||||
createBigFishCreationSession,
|
||||
executeBigFishCreationAction,
|
||||
@@ -57,6 +65,7 @@ import {
|
||||
} from '../../services/puzzle-runtime';
|
||||
import { listPuzzleWorks } from '../../services/puzzle-works';
|
||||
import { deleteRpgEntryWorldProfile } from '../../services/rpg-entry';
|
||||
import { getRpgEntryWorldGalleryDetailByCode } from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
@@ -173,6 +182,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
const [puzzleError, setPuzzleError] = useState<string | null>(null);
|
||||
const [isPuzzleBusy, setIsPuzzleBusy] = useState(false);
|
||||
const [isPuzzleLoadingLibrary, setIsPuzzleLoadingLibrary] = useState(false);
|
||||
const [isSearchingPublicCode, setIsSearchingPublicCode] = useState(false);
|
||||
const [publicSearchError, setPublicSearchError] = useState<string | null>(null);
|
||||
const [searchedPublicUser, setSearchedPublicUser] =
|
||||
useState<PublicUserSummary | null>(null);
|
||||
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
@@ -377,6 +390,86 @@ export function PlatformEntryFlowShellImpl({
|
||||
[authUi],
|
||||
);
|
||||
|
||||
const handlePublicCodeSearch = useCallback(
|
||||
async (keyword: string) => {
|
||||
const normalizedKeyword = keyword.trim();
|
||||
if (!normalizedKeyword) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSearchingPublicCode(true);
|
||||
setPublicSearchError(null);
|
||||
setSearchedPublicUser(null);
|
||||
|
||||
const upperKeyword = normalizedKeyword.toUpperCase();
|
||||
const shouldSearchUserIdFirst = /^user[_-][a-z0-9_-]+$/iu.test(normalizedKeyword);
|
||||
const shouldSearchWorkFirst =
|
||||
!shouldSearchUserIdFirst &&
|
||||
(upperKeyword.startsWith('CW') || /^\d{1,8}$/u.test(normalizedKeyword));
|
||||
const shouldSearchUserFirst =
|
||||
shouldSearchUserIdFirst || upperKeyword.startsWith('SY') || !shouldSearchWorkFirst;
|
||||
|
||||
const tryOpenGalleryEntry = async () => {
|
||||
const entry = await getRpgEntryWorldGalleryDetailByCode(normalizedKeyword);
|
||||
await detailNavigation.openGalleryDetail({
|
||||
ownerUserId: entry.ownerUserId,
|
||||
profileId: entry.profileId,
|
||||
publicWorkCode: entry.publicWorkCode,
|
||||
authorPublicUserCode: entry.authorPublicUserCode,
|
||||
visibility: 'published',
|
||||
publishedAt: entry.publishedAt,
|
||||
updatedAt: entry.updatedAt,
|
||||
authorDisplayName: entry.authorDisplayName,
|
||||
worldName: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summaryText: entry.summaryText,
|
||||
coverImageSrc: entry.coverImageSrc,
|
||||
themeMode: entry.themeMode,
|
||||
playableNpcCount: entry.playableNpcCount,
|
||||
landmarkCount: entry.landmarkCount,
|
||||
} satisfies CustomWorldGalleryCard);
|
||||
};
|
||||
|
||||
try {
|
||||
if (shouldSearchUserIdFirst) {
|
||||
const user = await getPublicAuthUserById(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldSearchWorkFirst) {
|
||||
try {
|
||||
await tryOpenGalleryEntry();
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (shouldSearchUserFirst) {
|
||||
try {
|
||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
return;
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (!shouldSearchWorkFirst) {
|
||||
await tryOpenGalleryEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
const user = await getPublicAuthUserByCode(normalizedKeyword);
|
||||
setSearchedPublicUser(user);
|
||||
} catch (error) {
|
||||
setPublicSearchError(
|
||||
resolveRpgCreationErrorMessage(error, '未找到对应的叙世号或作品号。'),
|
||||
);
|
||||
} finally {
|
||||
setIsSearchingPublicCode(false);
|
||||
}
|
||||
},
|
||||
[detailNavigation],
|
||||
);
|
||||
|
||||
const prepareCreationLaunch = useCallback(() => {
|
||||
if (sessionController.isCreatingAgentSession) {
|
||||
return false;
|
||||
@@ -1309,6 +1402,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
detailNavigation.openLibraryDetail(entry);
|
||||
});
|
||||
}}
|
||||
onSearchPublicCode={(keyword) => {
|
||||
void handlePublicCodeSearch(keyword);
|
||||
}}
|
||||
isSearchingPublicCode={isSearchingPublicCode}
|
||||
onOpenProfileDashboardCard={() => {
|
||||
if (platformBootstrap.dashboardError) {
|
||||
void platformBootstrap.refreshProfileDashboard();
|
||||
@@ -1795,6 +1892,54 @@ export function PlatformEntryFlowShellImpl({
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{(searchedPublicUser || publicSearchError) && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/45 p-4"
|
||||
>
|
||||
<div className="platform-surface w-full max-w-md rounded-[1.6rem] p-5">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-[var(--platform-text-soft)]">
|
||||
公开编号搜索
|
||||
</div>
|
||||
<div className="mt-1 text-xl font-black text-[var(--platform-text-strong)]">
|
||||
{publicSearchError ? '未找到结果' : '命中用户'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSearchedPublicUser(null);
|
||||
setPublicSearchError(null);
|
||||
}}
|
||||
className="platform-icon-button"
|
||||
aria-label="关闭搜索结果"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
{publicSearchError ? (
|
||||
<div className="mt-4 text-sm leading-6 text-[var(--platform-text-soft)]">
|
||||
{publicSearchError}
|
||||
</div>
|
||||
) : searchedPublicUser ? (
|
||||
<div className="mt-4 rounded-[1.2rem] border border-[var(--platform-line-soft)] p-4">
|
||||
<div className="text-lg font-bold text-[var(--platform-text-strong)]">
|
||||
{searchedPublicUser.displayName}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-[var(--platform-text-soft)]">
|
||||
叙世号 {searchedPublicUser.publicUserCode}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,6 +287,7 @@ const mockAuthUser: AuthUser = {
|
||||
id: 'user-1',
|
||||
username: 'tester',
|
||||
displayName: '测试玩家',
|
||||
publicUserCode: 'user-tester',
|
||||
phoneNumberMasked: null,
|
||||
loginMethod: 'password',
|
||||
bindingStatus: 'active',
|
||||
@@ -569,6 +570,8 @@ beforeEach(() => {
|
||||
entry: {
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'agent-draft-custom-world-agent-session-1',
|
||||
publicWorkCode: null,
|
||||
authorPublicUserCode: null,
|
||||
profile: {
|
||||
id: 'agent-draft-custom-world-agent-session-1',
|
||||
name: '潮雾列岛',
|
||||
@@ -1204,6 +1207,8 @@ test('clicking a public work while logged out routes through requireAuth', async
|
||||
{
|
||||
ownerUserId: 'author-1',
|
||||
profileId: 'world-public-1',
|
||||
publicWorkCode: 'work-public-1',
|
||||
authorPublicUserCode: 'author-1',
|
||||
visibility: 'published',
|
||||
publishedAt: '2026-04-16T12:00:00.000Z',
|
||||
updatedAt: '2026-04-16T12:00:00.000Z',
|
||||
@@ -2378,6 +2383,8 @@ test('creation hub published work can open detail view before deleting from deta
|
||||
const publishedLibraryEntry = {
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'world-delete-1',
|
||||
publicWorkCode: 'work-delete-1',
|
||||
authorPublicUserCode: 'user-1',
|
||||
profile: {
|
||||
id: 'world-delete-1',
|
||||
name: '潮雾列岛',
|
||||
@@ -2463,6 +2470,8 @@ test('creation hub published work enters existing detail view', async () => {
|
||||
{
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'world-public-1',
|
||||
publicWorkCode: 'work-public-1',
|
||||
authorPublicUserCode: 'user-1',
|
||||
profile: {
|
||||
id: 'world-public-1',
|
||||
name: '潮雾列岛',
|
||||
@@ -2534,6 +2543,8 @@ test('creation hub published work experience button enters world directly', asyn
|
||||
{
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'world-experience-1',
|
||||
publicWorkCode: 'work-experience-1',
|
||||
authorPublicUserCode: 'user-1',
|
||||
profile: {
|
||||
id: 'world-experience-1',
|
||||
name: '潮雾列岛',
|
||||
@@ -2609,7 +2620,9 @@ test('creation hub published work delete button removes the work directly from c
|
||||
};
|
||||
const publishedLibraryEntry = {
|
||||
ownerUserId: 'user-1',
|
||||
profileId: 'world-card-delete-1',,
|
||||
profileId: 'world-card-delete-1',
|
||||
publicWorkCode: 'work-card-delete-1',
|
||||
authorPublicUserCode: 'user-1',
|
||||
profile: {
|
||||
id: 'world-card-delete-1',
|
||||
name: '潮雾列岛',
|
||||
|
||||
@@ -76,6 +76,8 @@ export interface RpgEntryHomeViewProps {
|
||||
onOpenLibraryDetail: (
|
||||
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
|
||||
) => void;
|
||||
onSearchPublicCode?: (keyword: string) => void | Promise<void>;
|
||||
isSearchingPublicCode?: boolean;
|
||||
onOpenProfileDashboardCard?: (cardKey: ProfileDashboardCardKey) => void;
|
||||
createTabContent?: ReactNode;
|
||||
}
|
||||
@@ -665,6 +667,10 @@ function formatDashboardUpdatedAt(value: string | null | undefined) {
|
||||
}
|
||||
|
||||
function buildPublicUserCode(user: AuthUser | null | undefined) {
|
||||
if (user?.publicUserCode?.trim()) {
|
||||
return user.publicUserCode.trim();
|
||||
}
|
||||
|
||||
const raw =
|
||||
user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() ||
|
||||
user?.username.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() ||
|
||||
@@ -778,10 +784,13 @@ export function RpgEntryHomeView({
|
||||
onOpenCreateTypePicker,
|
||||
onOpenGalleryDetail,
|
||||
onOpenLibraryDetail,
|
||||
onSearchPublicCode,
|
||||
isSearchingPublicCode = false,
|
||||
onOpenProfileDashboardCard,
|
||||
createTabContent,
|
||||
}: RpgEntryHomeViewProps) {
|
||||
const authUi = useAuthUi();
|
||||
const [desktopSearchKeyword, setDesktopSearchKeyword] = useState('');
|
||||
const isAuthenticated = Boolean(authUi?.user);
|
||||
const isDesktopLayout = usePlatformDesktopLayout();
|
||||
const featuredShelf = useMemo(
|
||||
@@ -821,6 +830,14 @@ export function RpgEntryHomeView({
|
||||
}
|
||||
authUi?.openLoginModal();
|
||||
};
|
||||
const submitDesktopSearch = () => {
|
||||
const keyword = desktopSearchKeyword.trim();
|
||||
if (!keyword || !onSearchPublicCode || isSearchingPublicCode) {
|
||||
return;
|
||||
}
|
||||
|
||||
void onSearchPublicCode(keyword);
|
||||
};
|
||||
const desktopHeroEntry =
|
||||
featuredShelf[0] ?? latestEntries[0] ?? myEntries[0] ?? null;
|
||||
const desktopHeroCover = desktopHeroEntry
|
||||
@@ -1440,6 +1457,8 @@ export function RpgEntryHomeView({
|
||||
onOpenGalleryDetail({
|
||||
ownerUserId: entry.ownerUserId,
|
||||
profileId: entry.profileId,
|
||||
publicWorkCode: null,
|
||||
authorPublicUserCode: null,
|
||||
visibility: 'published',
|
||||
publishedAt: entry.visitedAt,
|
||||
updatedAt: entry.visitedAt,
|
||||
@@ -1572,9 +1591,30 @@ export function RpgEntryHomeView({
|
||||
<RpgEntryBrandLogo className="shrink-0" decorative />
|
||||
<div className="platform-desktop-search flex min-w-0 max-w-[34rem] flex-1 items-center gap-3 px-4 py-3 text-[var(--platform-text-soft)]">
|
||||
<Search className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate text-sm">
|
||||
搜索世界、角色、主题或灵感
|
||||
</span>
|
||||
<input
|
||||
value={desktopSearchKeyword}
|
||||
onChange={(event) => setDesktopSearchKeyword(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
submitDesktopSearch();
|
||||
}
|
||||
}}
|
||||
placeholder="输入 SY 或 CW 编号"
|
||||
className="w-full min-w-0 bg-transparent text-sm text-[var(--platform-text-strong)] outline-none placeholder:text-[var(--platform-text-soft)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={submitDesktopSearch}
|
||||
disabled={
|
||||
!desktopSearchKeyword.trim() ||
|
||||
!onSearchPublicCode ||
|
||||
isSearchingPublicCode
|
||||
}
|
||||
className="shrink-0 text-xs font-semibold text-[var(--platform-text-soft)] disabled:opacity-50"
|
||||
>
|
||||
{isSearchingPublicCode ? '搜索中' : '搜索'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user