Merge remote-tracking branch 'origin/master' into codex/editor-asset-library

# Conflicts:
#	docs/project-memory/shared-memory/decision-log.md
This commit is contained in:
2026-06-17 20:52:41 +08:00
68 changed files with 2279 additions and 2676 deletions
@@ -200,6 +200,7 @@ const authServiceMocks = vi.hoisted(() => ({
token: 'runtime-guest-token',
expiresAt: '2099-01-01T00:00:00.000Z',
})),
isWechatMiniProgramWebViewRuntime: vi.fn(() => false),
getPublicAuthUserByCode: vi.fn(
async (publicUserCode: string): Promise<PublicUserSummary> => ({
id: `public-user-${publicUserCode}`,
@@ -222,6 +223,8 @@ const authServiceMocks = vi.hoisted(() => ({
vi.mock('../../services/authService', () => ({
ensureRuntimeGuestToken: authServiceMocks.ensureRuntimeGuestToken,
isWechatMiniProgramWebViewRuntime:
authServiceMocks.isWechatMiniProgramWebViewRuntime,
getPublicAuthUserByCode: authServiceMocks.getPublicAuthUserByCode,
getPublicAuthUserById: authServiceMocks.getPublicAuthUserById,
}));
@@ -779,6 +779,7 @@ function ProfileHomeViewHarness({
userOverrides = {},
activeTab = 'profile',
profileTaskRefreshKey = 0,
profileGenerationQueueStatus = null,
profilePlayStats = null,
isProfilePlayStatsOpen = false,
}: {
@@ -789,6 +790,7 @@ function ProfileHomeViewHarness({
userOverrides?: Partial<AuthUser>;
activeTab?: RpgEntryHomeViewProps['activeTab'];
profileTaskRefreshKey?: number;
profileGenerationQueueStatus?: RpgEntryHomeViewProps['profileGenerationQueueStatus'];
profilePlayStats?: ProfilePlayStatsResponse | null;
isProfilePlayStatsOpen?: boolean;
}) {
@@ -856,6 +858,7 @@ function ProfileHomeViewHarness({
onSearchPublicCode={vi.fn()}
onRechargeSuccess={onRechargeSuccess}
profileTaskRefreshKey={profileTaskRefreshKey}
profileGenerationQueueStatus={profileGenerationQueueStatus}
/>
</AuthUiContext.Provider>
);
@@ -871,6 +874,7 @@ function renderProfileView(
profileStatsOptions: {
profilePlayStats?: ProfilePlayStatsResponse | null;
isProfilePlayStatsOpen?: boolean;
profileGenerationQueueStatus?: RpgEntryHomeViewProps['profileGenerationQueueStatus'];
} = {},
) {
return render(
@@ -879,6 +883,9 @@ function renderProfileView(
profileDashboardOverrides={profileDashboardOverrides}
userOverrides={userOverrides}
profileTaskRefreshKey={profileTaskRefreshKey}
profileGenerationQueueStatus={
profileStatsOptions.profileGenerationQueueStatus
}
profilePlayStats={profileStatsOptions.profilePlayStats}
isProfilePlayStatsOpen={profileStatsOptions.isProfilePlayStatsOpen}
/>,
@@ -2596,7 +2603,11 @@ test('profile daily task shortcut reflects task progress and claim updates', asy
await user.click(screen.getByRole('button', { name: /每日任务/u }));
const taskTitle = await screen.findByText('每日登录');
const taskPanel = taskTitle.closest('.platform-subpanel') as HTMLElement;
const taskPanel = screen
.getByRole('button', { name: '领取' })
.closest('.rounded-\\[1rem\\]') as HTMLElement;
expect(taskTitle).toBeTruthy();
expect(taskPanel).toBeTruthy();
expect(taskPanel.className).toContain('rounded-[1rem]');
expect(taskPanel.className).toContain('p-4');
expect(mockGetRpgProfileTasks).toHaveBeenCalledTimes(1);
@@ -2892,6 +2903,46 @@ test('profile stats cards are centered without update timestamp', async () => {
await screen.findByText('1 / 1');
});
test('profile page shows external generation queue status', async () => {
renderProfileView(
vi.fn(),
{},
{},
0,
{
profileGenerationQueueStatus: {
currentStatus: 'queued',
currentProgress: 18,
pendingCount: 6,
runningCount: 2,
},
},
);
await screen.findByText('1 / 1');
const queueRegion = screen.getByRole('region', { name: '生成队列' });
expect(queueRegion.className).toContain(
'platform-profile-generation-queue-card',
);
expect(within(queueRegion).getByText('排队中')).toBeTruthy();
expect(within(queueRegion).getByText('排队中 18%')).toBeTruthy();
expect(within(queueRegion).getByText('排队')).toBeTruthy();
expect(within(queueRegion).getByText('6')).toBeTruthy();
expect(within(queueRegion).getByText('生成')).toBeTruthy();
expect(within(queueRegion).getByText('2')).toBeTruthy();
const progressbar = within(queueRegion).getByRole('progressbar', {
name: '生成队列进度',
});
expect(progressbar.getAttribute('aria-valuenow')).toBe('18');
});
test('profile page hides external generation queue card without queue state', async () => {
renderProfileView();
await screen.findByText('1 / 1');
expect(screen.queryByRole('region', { name: '生成队列' })).toBeNull();
});
test('mobile profile page matches the reference layout sections', async () => {
mockNarrowMobileLayout();
@@ -126,6 +126,7 @@ import {
ProfileStatCardSkeleton,
} from '../platform-entry/PlatformProfilePrimitives';
import { PlatformProfileModalShell } from '../platform-entry/PlatformProfileModalShell';
import { PlatformProfileGenerationQueueCard } from '../platform-entry/PlatformProfileGenerationQueueCard';
import { PlatformProfilePlayedWorksModal } from '../platform-entry/PlatformProfilePlayedWorksModal';
import { PlatformProfileQrScannerModal } from '../platform-entry/PlatformProfileQrScannerModal';
import { PlatformProfileRechargeModal } from '../platform-entry/PlatformProfileRechargeModal';
@@ -133,6 +134,7 @@ import { PlatformProfileReferralModal } from '../platform-entry/PlatformProfileR
import { PlatformProfileRewardCodeRedeemModal } from '../platform-entry/PlatformProfileRewardCodeRedeemModal';
import { PlatformProfileTaskCenterModal } from '../platform-entry/PlatformProfileTaskCenterModal';
import { PlatformProfileWalletLedgerModal } from '../platform-entry/PlatformProfileWalletLedgerModal';
import type { ExternalGenerationQueueStatus } from '../platform-entry/platformExternalGenerationQueueStatusModel';
import { getInitialPlatformDesktopLayout } from '../platform-entry/platformEntryResponsive';
import {
type RechargePaymentResult,
@@ -276,6 +278,7 @@ export interface RpgEntryHomeViewProps {
onOpenProjects?: () => void;
onRechargeSuccess?: () => void | Promise<void>;
profileTaskRefreshKey?: number;
profileGenerationQueueStatus?: ExternalGenerationQueueStatus | null;
createTabContent?: ReactNode;
draftTabContent?: ReactNode;
hasUnreadDraftUpdate?: boolean;
@@ -2571,6 +2574,7 @@ export function RpgEntryHomeView({
onOpenProjects,
onRechargeSuccess,
profileTaskRefreshKey = 0,
profileGenerationQueueStatus = null,
createTabContent,
draftTabContent,
hasUnreadDraftUpdate = false,
@@ -3424,6 +3428,8 @@ export function RpgEntryHomeView({
const [recommendDragOffsetY, setRecommendDragOffsetY] = useState(0);
const [recommendDragCommitDirection, setRecommendDragCommitDirection] =
useState<RecommendSwipeDirection | null>(null);
const [isRecommendDragResetting, setIsRecommendDragResetting] =
useState(false);
const activeRecommendEntryKeyForSelection =
recommendFeedWindow.activeEntryKey;
const recommendCardStageRef = useRef<HTMLDivElement | null>(null);
@@ -3438,6 +3444,7 @@ export function RpgEntryHomeView({
return;
}
setIsRecommendDragResetting(false);
setRecommendDragCommitDirection(direction);
const panelHeight =
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
@@ -3454,8 +3461,12 @@ export function RpgEntryHomeView({
} else {
onSelectPreviousRecommendEntry?.(activeRecommendEntryKeyForSelection);
}
setIsRecommendDragResetting(true);
setRecommendDragOffsetY(0);
setRecommendDragCommitDirection(null);
window.requestAnimationFrame(() => {
setIsRecommendDragResetting(false);
});
}, RECOMMEND_ENTRY_COMMIT_ANIMATION_MS);
},
[
@@ -3517,6 +3528,7 @@ export function RpgEntryHomeView({
const deltaY = event.clientY - drag.startY;
const commitDirection = resolveRecommendDragCommitDirection(deltaY);
if (!commitDirection) {
setIsRecommendDragResetting(false);
setRecommendDragOffsetY(0);
return;
}
@@ -3532,6 +3544,7 @@ export function RpgEntryHomeView({
event.currentTarget.releasePointerCapture?.(drag.pointerId);
}
recommendDragStartRef.current = null;
setIsRecommendDragResetting(false);
setRecommendDragOffsetY(0);
},
[],
@@ -3542,6 +3555,7 @@ export function RpgEntryHomeView({
const recommendRailClassName = buildRecommendSwipeRailClassName({
offsetY: recommendDragOffsetY,
commitDirection: recommendDragCommitDirection,
isResetting: isRecommendDragResetting,
});
const selectNextRecommendEntry = useCallback(() => {
if (
@@ -4338,6 +4352,10 @@ export function RpgEntryHomeView({
/>
</button>
<PlatformProfileGenerationQueueCard
queueStatus={profileGenerationQueueStatus}
/>
<section
className="platform-profile-shortcut-panel"
aria-label="常用功能"
@@ -38,6 +38,13 @@ describe('rpgEntryRecommendSwipeDeckModel', () => {
expect(
buildRecommendSwipeRailClassName({ offsetY: -320, commitDirection: 1 }),
).toBe('platform-recommend-swipe-rail--committing');
expect(
buildRecommendSwipeRailClassName({
offsetY: 0,
commitDirection: null,
isResetting: true,
}),
).toBe('platform-recommend-swipe-rail--resetting');
expect(
shouldAnimateRecommendSwipe({
@@ -9,6 +9,7 @@ export type RecommendSwipeDirection = 1 | -1;
export type RecommendSwipeRailState = {
offsetY: number;
commitDirection: RecommendSwipeDirection | null;
isResetting?: boolean;
};
/** 收口推荐卡纵向滑动的纯判定,页面只保留 pointer 与动画副作用。 */
@@ -47,6 +48,10 @@ export function resolveRecommendCommitOffset(
export function buildRecommendSwipeRailClassName(
state: RecommendSwipeRailState,
) {
if (state.isResetting) {
return 'platform-recommend-swipe-rail--resetting';
}
if (state.commitDirection) {
return 'platform-recommend-swipe-rail--committing';
}