import { Camera, Coins, History, KeyRound, MessageCircle, Pencil, Settings, Ticket, UserRound, } from 'lucide-react'; import { useCallback, useRef, useState } from 'react'; import profileClockImage from '../../../media/profile/_Image (1).png'; import profileGamepadImage from '../../../media/profile/_Image (2).png'; import profileStillLifeImage from '../../../media/profile/_Image (3).png'; import profileCoinsImage from '../../../media/profile/_Image (4).png'; import profileGiftImage from '../../../media/profile/_Image (6).png'; import profileCommunityImage from '../../../media/profile/_Image (7).png'; import profileFeedbackImage from '../../../media/profile/_Image (8).png'; import profileMascotImage from '../../../media/profile/_Image (9).png'; import profilePointImage from '../../../media/profile/_Image.png'; import type { AuthUser } from '../../../packages/shared/src/contracts/auth'; import type { ProfileDashboardSummary } from '../../../packages/shared/src/contracts/runtime'; import { updateAuthProfile } from '../../services/authService'; import { canUseNativeHostCapability, type HostFileImportImageResult, importHostImageFile, } from '../../services/host-bridge/hostBridge'; import { CopyFeedbackButton } from '../common/CopyFeedbackButton'; import { LegalDocumentModal } from '../common/LegalDocumentModal'; import { getLegalDocument, type LegalDocumentId, } from '../common/legalDocuments'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformIconButton } from '../common/PlatformIconButton'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; import { SquareImageCropModal } from '../common/SquareImageCropModal'; import { buildCenteredSquareImageCropRect, clampSquareImageCropRect, type SquareImageCropRect, } from '../common/squareImageCropModel'; import { useCopyFeedback } from '../common/useCopyFeedback'; import { resolveActivePublicUserCode } from './platformActiveProfileModel'; import { PlatformProfileModalShell } from './PlatformProfileModalShell'; import { ProfileLegalSection, ProfileSettingsRow, ProfileShortcutButton, ProfileStatCard, ProfileStatCardSkeleton, } from './PlatformProfilePrimitives'; type PlatformActiveProfileViewProps = { dashboard: ProfileDashboardSummary | null; isLoadingDashboard: boolean; onLogin: () => void; onOpenApiKeys: () => void; onOpenCommunity: () => void; onOpenFeedback: () => void; onOpenRecharge: () => void; onOpenRewardCode: () => void; onOpenSettings: () => void; onOpenWalletLedger: () => void; onUserUpdated: (user: AuthUser) => void; user: AuthUser | null | undefined; }; 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']); function hostAvatarImageResultToFile(result: HostFileImportImageResult) { const binary = atob(result.base64Data); const bytes = new Uint8Array(binary.length); for (let index = 0; index < binary.length; index += 1) { bytes[index] = binary.charCodeAt(index); } return new File([bytes], result.fileName, { type: result.mimeType }); } function validateProfileDisplayName(value: string) { const normalized = value.trim(); if (!normalized) { return '请输入昵称'; } const length = Array.from(normalized).length; if (length < 2 || length > 20) { return '昵称需要 2 到 20 位'; } if (!/^[\u4e00-\u9fffa-zA-Z0-9_]+$/u.test(normalized)) { return '昵称仅支持中文、英文、数字和下划线'; } return null; } function readImageIntrinsicSize(src: string) { return new Promise<{ width: number; height: number }>((resolve, reject) => { const image = new Image(); image.onload = () => { resolve({ width: image.naturalWidth, height: image.naturalHeight, }); }; image.onerror = () => reject(new Error('图片读取失败')); image.src = src; }); } function loadAvatarFile(file: File) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result !== 'string') { reject(new Error('图片读取失败')); return; } resolve(reader.result); }; reader.onerror = () => reject(new Error('图片读取失败')); reader.readAsDataURL(file); }); } function cropAvatarImage(params: { source: string; cropX: number; cropY: number; cropSize: number; }) { return new Promise((resolve, reject) => { const image = new Image(); image.onload = () => { const canvas = document.createElement('canvas'); canvas.width = AVATAR_OUTPUT_SIZE; canvas.height = AVATAR_OUTPUT_SIZE; const context = canvas.getContext('2d'); if (!context) { reject(new Error('头像裁剪失败')); return; } context.drawImage( image, params.cropX, params.cropY, params.cropSize, params.cropSize, 0, 0, AVATAR_OUTPUT_SIZE, AVATAR_OUTPUT_SIZE, ); resolve(canvas.toDataURL('image/png')); }; image.onerror = () => reject(new Error('头像裁剪失败')); image.src = params.source; }); } function ProfileNicknameModal({ value, error, isSaving, onChange, onClose, onSubmit, }: { value: string; error: string | null; isSaving: boolean; onChange: (value: string) => void; onClose: () => void; onSubmit: () => void; }) { return ( 取消 {isSaving ? '保存中' : '保存'} } > {error ? ( {error} ) : null} ); } function formatDashboardCount(value: number) { return Math.max(0, Math.round(value)).toLocaleString('zh-CN'); } function formatTotalPlayTime(value: number) { const hours = Math.max(0, Math.round(value / 360000) / 10); return `${hours.toLocaleString('zh-CN', { maximumFractionDigits: 1, })}小时`; } export function PlatformActiveProfileView({ dashboard, isLoadingDashboard, onLogin, onOpenApiKeys, onOpenCommunity, onOpenFeedback, onOpenRecharge, onOpenRewardCode, onOpenSettings, onOpenWalletLedger, onUserUpdated, user, }: PlatformActiveProfileViewProps) { const [activeLegalDocumentId, setActiveLegalDocumentId] = useState(null); const { copyState, copyText } = useCopyFeedback(); const avatarFileInputRef = useRef(null); const [isNicknameModalOpen, setIsNicknameModalOpen] = useState(false); const [nicknameInput, setNicknameInput] = useState(''); const [nicknameError, setNicknameError] = useState(null); const [isSavingNickname, setIsSavingNickname] = useState(false); const [avatarSource, setAvatarSource] = useState(null); const [avatarImageSize, setAvatarImageSize] = useState<{ width: number; height: number; } | null>(null); const [avatarCrop, setAvatarCrop] = useState({ x: 0, y: 0, size: 1, }); const [avatarError, setAvatarError] = useState(null); const [isSavingAvatar, setIsSavingAvatar] = useState(false); const activeLegalDocument = activeLegalDocumentId ? getLegalDocument(activeLegalDocumentId) : null; const openNicknameModal = () => { if (!user) { onLogin(); return; } setNicknameInput(user.displayName); setNicknameError(null); setIsNicknameModalOpen(true); }; const submitNickname = () => { if (!user || isSavingNickname) { return; } const validationError = validateProfileDisplayName(nicknameInput); if (validationError) { setNicknameError(validationError); return; } setIsSavingNickname(true); setNicknameError(null); void updateAuthProfile({ displayName: nicknameInput.trim() }) .then((nextUser) => { onUserUpdated(nextUser); setIsNicknameModalOpen(false); }) .catch((error: unknown) => { setNicknameError( error instanceof Error ? error.message : '昵称保存失败', ); }) .finally(() => setIsSavingNickname(false)); }; const openAvatarPicker = () => { if (!user) { onLogin(); return; } setAvatarError(null); if (canUseNativeHostCapability('file.importImage')) { void (async () => { try { const importedImageFile = await importHostImageFile(); if (!importedImageFile) { return; } handleAvatarFileChange( hostAvatarImageResultToFile(importedImageFile), ); } catch (error: unknown) { setAvatarError( error instanceof Error ? error.message : '头像图片导入失败', ); } })(); return; } avatarFileInputRef.current?.click(); }; const handleAvatarFileChange = (file: File | null) => { if (avatarFileInputRef.current) { avatarFileInputRef.current.value = ''; } if (!file) { return; } if (!AVATAR_ALLOWED_TYPES.has(file.type)) { setAvatarError('头像仅支持 jpg、png、webp'); return; } if (file.size > AVATAR_MAX_FILE_SIZE) { setAvatarError('头像图片不能超过 5MB'); return; } setAvatarError(null); void loadAvatarFile(file) .then(async (source) => { const imageSize = await readImageIntrinsicSize(source); setAvatarSource(source); setAvatarImageSize(imageSize); setAvatarCrop(buildCenteredSquareImageCropRect(imageSize)); }) .catch((error: unknown) => { setAvatarError( error instanceof Error ? error.message : '头像图片读取失败', ); }); }; const updateAvatarCrop = useCallback( (nextCrop: SquareImageCropRect) => { if (!avatarImageSize) { return; } setAvatarCrop(clampSquareImageCropRect(avatarImageSize, nextCrop)); }, [avatarImageSize], ); const submitAvatar = () => { if ( !avatarSource || !avatarImageSize || avatarCrop.size <= 0 || isSavingAvatar ) { return; } setIsSavingAvatar(true); setAvatarError(null); void cropAvatarImage({ source: avatarSource, cropX: avatarCrop.x, cropY: avatarCrop.y, cropSize: avatarCrop.size, }) .then((avatarDataUrl) => updateAuthProfile({ avatarDataUrl })) .then((nextUser) => { onUserUpdated(nextUser); setAvatarSource(null); setAvatarImageSize(null); }) .catch((error: unknown) => { setAvatarError(error instanceof Error ? error.message : '头像上传失败'); }) .finally(() => setIsSavingAvatar(false)); }; if (!user) { return (
尚未登录
登录
); } const avatarUrl = user.avatarUrl?.trim() || null; const publicUserCode = resolveActivePublicUserCode(user); return (
handleAvatarFileChange(event.target.files?.[0] ?? null) } />
{user.displayName}
} onClick={openNicknameModal} className="platform-profile-edit-button" />
陶泥号: {publicUserCode} { void copyText(publicUserCode); }} />
{isLoadingDashboard ? ( <> ) : ( <> )}
setActiveLegalDocumentId(null)} /> {isNicknameModalOpen ? ( { setNicknameInput(value); setNicknameError(null); }} onClose={() => setIsNicknameModalOpen(false)} onSubmit={submitNickname} /> ) : null} {avatarSource && avatarImageSize ? ( { setAvatarSource(null); setAvatarImageSize(null); setAvatarError(null); }} onSubmit={submitAvatar} /> ) : null} {avatarError && !avatarSource ? (
{avatarError}
) : null}
); } export default PlatformActiveProfileView;