Update spacetime-client bindings and frontend

Large update across server and web clients: regenerated/added many spacetime-client module bindings and input types (including new delete/work_delete input types and numerous procedure/reducer files), updates to server-rs API modules (bark_battle, jump_hop, wooden_fish, auth, module-runtime and shared contracts), and fixes in module-runtime behavior and domain logic. Frontend changes include new/updated components and tests (creative audio helpers, bark-battle/jump-hop/wooden-fish clients and views, unified generation pages, RPG entry views, and runtime shells), plus CSS and service updates. Documentation and operational notes updated (.hermes pitfalls and multiple PRD/docs) to cover daily-task refresh, banner asset fallback, recommend-key bug, and other platform behaviors. Tests and verification steps added/updated alongside these changes.
This commit is contained in:
2026-06-04 22:44:19 +08:00
parent 2678954627
commit 27b30f974b
326 changed files with 4374 additions and 2539 deletions

View File

@@ -14,9 +14,9 @@ import {
Gamepad2,
GitFork,
Heart,
Loader2,
LogIn,
MessageCircle,
Loader2,
Palette,
Pencil,
Plus,
@@ -24,7 +24,6 @@ import {
Search,
Settings,
Share2,
ShieldCheck,
SlidersHorizontal,
Sparkles,
Star,
@@ -80,6 +79,7 @@ import type {
} from '../../../packages/shared/src/contracts/runtime';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import { buildPublicWorkDetailUrl } from '../../routing/appPageRoutes';
import { refreshStoredAccessToken } from '../../services/apiClient';
import type { AuthUser } from '../../services/authService';
import {
getPublicAuthUserByCode,
@@ -136,6 +136,7 @@ import { getInitialPlatformDesktopLayout } from '../platform-entry/platformEntry
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
import {
buildPlatformPublicGalleryCardKey,
buildPlatformWorldDisplayTags,
describePlatformThemeLabel,
formatPlatformWorkDisplayName,
@@ -152,8 +153,8 @@ import {
isWoodenFishGalleryEntry,
type PlatformPublicGalleryCard,
type PlatformWorldCardLike,
resolvePlatformWorkAuthorDisplayName,
resolvePlatformPublicWorkCode,
resolvePlatformWorkAuthorDisplayName,
resolvePlatformWorldCoverImage,
resolvePlatformWorldCoverSlides,
resolvePlatformWorldFallbackCoverImage,
@@ -246,6 +247,9 @@ const PLATFORM_HOME_TABS: PlatformHomeTab[] = [
'saves',
'profile',
];
const PROFILE_TASK_DAY_MS = 24 * 60 * 60 * 1000;
const PROFILE_TASK_BEIJING_OFFSET_MS = 8 * 60 * 60 * 1000;
const PROFILE_TASK_MIN_RESET_DELAY_MS = 1000;
const AVATAR_MAX_FILE_SIZE = 5 * 1024 * 1024;
const AVATAR_OUTPUT_SIZE = 256;
const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
@@ -301,15 +305,8 @@ function buildProfileTaskCardSummary(center: ProfileTaskCenterResponse | null) {
const progressCount = Math.min(task?.progressCount ?? 0, threshold);
const rewardPoints =
task?.rewardPoints ?? PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS;
const actionLabel =
task?.status === 'claimable'
? '领取'
: task?.status === 'claimed'
? '已完成'
: '去完成';
return {
actionLabel,
progressCount,
progressPercent: Math.round((progressCount / threshold) * 100),
rewardPoints,
@@ -317,6 +314,15 @@ function buildProfileTaskCardSummary(center: ProfileTaskCenterResponse | null) {
};
}
function getDelayUntilNextProfileTaskReset(nowMs = Date.now()) {
const shiftedNow = nowMs + PROFILE_TASK_BEIJING_OFFSET_MS;
const nextDayStart =
Math.floor(shiftedNow / PROFILE_TASK_DAY_MS) * PROFILE_TASK_DAY_MS +
PROFILE_TASK_DAY_MS;
const nextResetAt = nextDayStart - PROFILE_TASK_BEIJING_OFFSET_MS;
return Math.max(PROFILE_TASK_MIN_RESET_DELAY_MS, nextResetAt - nowMs);
}
type ProfileReferralPanel = 'invite' | 'redeem' | 'community';
type ProfilePopupPanel = ProfileReferralPanel | 'saveArchives';
type BarcodeDetectorLike = {
@@ -1801,22 +1807,7 @@ function isExactPublicWorkCodeSearch(
}
function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
const kind = isBigFishGalleryEntry(entry)
? 'big-fish'
: isPuzzleGalleryEntry(entry)
? 'puzzle'
: isMatch3DGalleryEntry(entry)
? 'match3d'
: isSquareHoleGalleryEntry(entry)
? 'square-hole'
: isVisualNovelGalleryEntry(entry)
? 'visual-novel'
: isBarkBattleGalleryEntry(entry)
? 'bark-battle'
: isEdutainmentGalleryEntry(entry)
? `edutainment:${entry.templateId}`
: 'rpg';
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
return buildPlatformPublicGalleryCardKey(entry);
}
function PlatformWorkSearchResults({
@@ -2396,7 +2387,7 @@ function ProfileStatCard({
type="button"
onClick={onClick ? () => onClick(cardKey) : undefined}
aria-label={`${label} ${value}`}
className="platform-profile-stat-card flex min-h-[5.75rem] items-center justify-center gap-2 px-3 py-3 text-center transition"
className="platform-profile-stat-card flex min-h-[5.25rem] items-center justify-center gap-2 px-2.5 py-2.5 text-center transition"
>
<div className="platform-profile-stat-card__icon">
{imageSrc ? (
@@ -2406,10 +2397,10 @@ function ProfileStatCard({
)}
</div>
<div className="min-w-0 text-left">
<div className="platform-profile-stat-card__value whitespace-nowrap text-lg font-black leading-none text-[var(--platform-text-strong)]">
<div className="platform-profile-stat-card__value whitespace-nowrap text-[16px] font-black leading-none text-[var(--platform-text-strong)]">
{value}
</div>
<div className="platform-profile-stat-card__label mt-1 whitespace-nowrap text-[12px] font-medium text-[var(--platform-text-soft)]">
<div className="platform-profile-stat-card__label mt-1 whitespace-nowrap text-[11px] font-medium text-[var(--platform-text-soft)]">
{label}
</div>
</div>
@@ -2445,7 +2436,7 @@ function ProfileShortcutButton({
<button
type="button"
onClick={onClick ?? undefined}
className="platform-profile-shortcut-button flex min-h-[5.25rem] w-full flex-col items-center justify-center gap-2 px-2.5 py-3 text-center transition"
className="platform-profile-shortcut-button flex min-h-[4.75rem] w-full flex-col items-center justify-center gap-1.5 px-2 py-2.5 text-center transition"
>
<div className="platform-profile-shortcut-button__icon">
{imageSrc ? (
@@ -2454,11 +2445,11 @@ function ProfileShortcutButton({
<Icon className="h-[1.125rem] w-[1.125rem]" />
)}
</div>
<div className="platform-profile-shortcut-button__label whitespace-nowrap text-[13px] font-semibold text-[var(--platform-text-strong)]">
<div className="platform-profile-shortcut-button__label whitespace-nowrap text-[12px] font-semibold text-[var(--platform-text-strong)]">
{label}
</div>
{subLabel ? (
<div className="platform-profile-shortcut-button__sub-label flex min-h-4 items-center justify-center gap-1 whitespace-nowrap text-[11px] font-medium text-[var(--platform-text-soft)]">
<div className="platform-profile-shortcut-button__sub-label flex min-h-4 items-center justify-center gap-1 whitespace-nowrap text-[10px] font-medium text-[var(--platform-text-soft)]">
{subLabel}
</div>
) : null}
@@ -2481,13 +2472,13 @@ function ProfileSettingsRow({
<button
type="button"
onClick={onClick}
className="platform-profile-settings-row flex w-full items-center justify-between gap-3 px-4 py-4 text-left transition"
className="platform-profile-settings-row flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition"
>
<span className="flex min-w-0 items-center gap-3">
<span className="platform-profile-settings-row__icon">
<Icon className="h-5 w-5" />
<Icon className="h-4 w-4" />
</span>
<span className="truncate text-[15px] font-semibold text-[var(--platform-text-strong)]">
<span className="truncate text-[14px] font-semibold text-[var(--platform-text-strong)]">
{label}
</span>
</span>
@@ -5018,6 +5009,40 @@ export function RpgEntryHomeView({
loadTaskCenter();
}, [activeTab, isAuthenticated, loadTaskCenter, profileTaskRefreshKey]);
useEffect(() => {
if (activeTab !== 'profile' || !isAuthenticated) {
return undefined;
}
let cancelled = false;
let timer: number | null = null;
const scheduleNextReset = () => {
if (cancelled) {
return;
}
timer = window.setTimeout(() => {
void refreshStoredAccessToken({ clearOnFailure: false })
.catch(() => undefined)
.finally(() => {
if (cancelled) {
return;
}
loadTaskCenter();
scheduleNextReset();
});
}, getDelayUntilNextProfileTaskReset());
};
scheduleNextReset();
return () => {
cancelled = true;
if (timer !== null) {
window.clearTimeout(timer);
}
};
}, [activeTab, isAuthenticated, loadTaskCenter]);
const openTaskCenterPanel = () => {
setIsTaskCenterOpen(true);
setTaskClaimSuccess(null);
@@ -6320,7 +6345,7 @@ export function RpgEntryHomeView({
<div className="platform-profile-header__text min-w-0">
<div className="flex items-center gap-2">
<div className="platform-profile-header__name truncate text-[20px] font-black leading-tight text-[var(--platform-text-strong)]">
<div className="platform-profile-header__name truncate text-[18px] font-black leading-tight text-[var(--platform-text-strong)]">
{authUi.user.displayName}
</div>
<button
@@ -6329,10 +6354,10 @@ export function RpgEntryHomeView({
className="platform-profile-edit-button"
aria-label="修改昵称"
>
<Pencil className="h-5 w-5" />
<Pencil className="h-3.5 w-3.5" />
</button>
</div>
<div className="platform-profile-header__code mt-3 flex flex-wrap items-center gap-2 text-[13px] text-[var(--platform-text-base)]">
<div className="platform-profile-header__code mt-2 flex flex-wrap items-center gap-2 text-[12px] text-[var(--platform-text-base)]">
<span> {publicUserCode}</span>
<button
type="button"
@@ -6361,10 +6386,10 @@ export function RpgEntryHomeView({
<Crown className="platform-profile-membership-card__crown" />
</span>
<span className="min-w-0 flex-1">
<span className="platform-profile-membership-card__title block text-[18px] font-black leading-tight text-white">
<span className="platform-profile-membership-card__title block text-[16px] font-black leading-tight text-white">
</span>
<span className="platform-profile-membership-card__subtitle mt-2 block text-[13px] font-medium text-white/92">
<span className="platform-profile-membership-card__subtitle mt-1.5 block text-[12px] font-medium text-white/92">
</span>
</span>
@@ -6393,7 +6418,7 @@ export function RpgEntryHomeView({
/>
<ProfileStatCard
cardKey="playTime"
label="累计游戏时长"
label="累计游"
value="暂不可用"
icon={Clock3}
imageSrc={profileClockImage}
@@ -6401,7 +6426,7 @@ export function RpgEntryHomeView({
/>
<ProfileStatCard
cardKey="playedWorks"
label="已玩游戏数量"
label="已玩游戏"
value="暂不可用"
icon={BookOpen}
imageSrc={profileGamepadImage}
@@ -6420,7 +6445,7 @@ export function RpgEntryHomeView({
/>
<ProfileStatCard
cardKey="playTime"
label="累计游戏时长"
label="累计游"
value={totalPlayTime}
icon={Clock3}
imageSrc={profileClockImage}
@@ -6428,7 +6453,7 @@ export function RpgEntryHomeView({
/>
<ProfileStatCard
cardKey="playedWorks"
label="已玩游戏数量"
label="已玩游戏"
value={`${formatDashboardCount(playedWorkCount)}`}
icon={BookOpen}
imageSrc={profileGamepadImage}
@@ -6448,15 +6473,15 @@ export function RpgEntryHomeView({
<span className="platform-profile-daily-task-card__title block text-[15px] font-black text-[var(--platform-text-strong)]">
</span>
<span className="platform-profile-daily-task-card__desc mt-4 block text-[13px] font-medium text-[var(--platform-text-base)]">
<span className="platform-profile-daily-task-card__desc mt-2 block text-[12px] font-medium text-[var(--platform-text-base)]">
{' '}
<span className="text-[#c45b2a]">
{profileTaskCardSummary.rewardPoints}
</span>{' '}
</span>
<span className="platform-profile-daily-task-card__progress mt-4 flex items-center gap-3">
<span className="platform-profile-daily-task-card__progress-value text-[14px] font-semibold text-[#dc3f0e]">
<span className="platform-profile-daily-task-card__progress mt-3 flex items-center gap-3">
<span className="platform-profile-daily-task-card__progress-value text-[13px] font-semibold text-[#dc3f0e]">
{profileTaskCardSummary.progressCount} /{' '}
{profileTaskCardSummary.threshold}
</span>
@@ -6475,9 +6500,6 @@ export function RpgEntryHomeView({
alt=""
className="platform-profile-daily-task-card__mascot"
/>
<span className="platform-profile-daily-task-card__action">
{profileTaskCardSummary.actionLabel}
</span>
</button>
<section
@@ -6501,14 +6523,14 @@ export function RpgEntryHomeView({
/>
<ProfileShortcutButton
label="玩家社区"
subLabel="交流心得 领取福利"
subLabel="交流心得"
icon={MessageCircle}
imageSrc={profileCommunityImage}
onClick={() => openProfilePopupPanel('community')}
/>
<ProfileShortcutButton
label="反馈与建议"
subLabel="帮我们做得更好"
subLabel="帮我们优化产品"
icon={MessageCircle}
imageSrc={profileFeedbackImage}
onClick={onOpenFeedback}
@@ -6517,16 +6539,6 @@ export function RpgEntryHomeView({
</section>
<section className="platform-profile-settings-panel" aria-label="设置入口">
<ProfileSettingsRow
label="主题设置"
icon={Palette}
onClick={() => authUi.openSettingsModal('appearance')}
/>
<ProfileSettingsRow
label="账号与安全"
icon={ShieldCheck}
onClick={() => authUi.openSettingsModal('account')}
/>
<ProfileSettingsRow
label="通用设置"
icon={Settings}