7ea463ed08
保留 SpacetimeDB 历史表、迁移白名单与最小兼容读取定义 移除旧创作前后端、worker、业务过程及纯业务 crate 的编译依赖 恢复现役创作、项目、我的入口及桌面移动导航 收紧 Vite、TypeScript、ESLint、Vitest 与静态资源退役边界 补齐开发栈、网关、原生壳和文档退役约束
685 lines
21 KiB
TypeScript
685 lines
21 KiB
TypeScript
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<string>((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<string>((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 (
|
|
<PlatformProfileModalShell
|
|
title="修改昵称"
|
|
onClose={onClose}
|
|
closeLabel="关闭昵称修改"
|
|
closeVariant="profileCompact"
|
|
panelClassName="platform-remap-surface !max-w-sm rounded-[1.4rem]"
|
|
bodyClassName="px-5 py-5"
|
|
footerClassName="grid grid-cols-2 gap-3 px-5 pb-5 pt-0 sm:px-5"
|
|
footer={
|
|
<>
|
|
<PlatformActionButton tone="secondary" onClick={onClose}>
|
|
取消
|
|
</PlatformActionButton>
|
|
<PlatformActionButton onClick={onSubmit} disabled={isSaving}>
|
|
{isSaving ? '保存中' : '保存'}
|
|
</PlatformActionButton>
|
|
</>
|
|
}
|
|
>
|
|
<label className="block">
|
|
<span className="sr-only">新昵称</span>
|
|
<PlatformTextField
|
|
autoFocus
|
|
value={value}
|
|
onChange={(event) => onChange(event.target.value)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
onSubmit();
|
|
}
|
|
}}
|
|
maxLength={20}
|
|
surface="editorDark"
|
|
size="lg"
|
|
density="roomy"
|
|
className="rounded-2xl border-white/12 bg-white/10 text-[var(--platform-text-strong)] focus:border-[var(--platform-surface-hover-border)]"
|
|
placeholder="输入新昵称"
|
|
/>
|
|
</label>
|
|
{error ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="tinted"
|
|
className="mt-3 rounded-2xl border-rose-400/25 text-rose-600"
|
|
>
|
|
{error}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
</PlatformProfileModalShell>
|
|
);
|
|
}
|
|
|
|
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<LegalDocumentId | null>(null);
|
|
const { copyState, copyText } = useCopyFeedback();
|
|
const avatarFileInputRef = useRef<HTMLInputElement | null>(null);
|
|
const [isNicknameModalOpen, setIsNicknameModalOpen] = useState(false);
|
|
const [nicknameInput, setNicknameInput] = useState('');
|
|
const [nicknameError, setNicknameError] = useState<string | null>(null);
|
|
const [isSavingNickname, setIsSavingNickname] = useState(false);
|
|
const [avatarSource, setAvatarSource] = useState<string | null>(null);
|
|
const [avatarImageSize, setAvatarImageSize] = useState<{
|
|
width: number;
|
|
height: number;
|
|
} | null>(null);
|
|
const [avatarCrop, setAvatarCrop] = useState<SquareImageCropRect>({
|
|
x: 0,
|
|
y: 0,
|
|
size: 1,
|
|
});
|
|
const [avatarError, setAvatarError] = useState<string | null>(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 (
|
|
<main
|
|
className="platform-profile-page platform-remap-surface w-full"
|
|
aria-label="我的"
|
|
>
|
|
<section className="platform-profile-header">
|
|
<img
|
|
src={profileStillLifeImage}
|
|
alt=""
|
|
aria-hidden="true"
|
|
className="platform-profile-scene-decor"
|
|
/>
|
|
<div className="platform-profile-header__identity">
|
|
<div className="platform-profile-header__identity-row flex min-w-0 items-center gap-4">
|
|
<img
|
|
src={profileMascotImage}
|
|
alt=""
|
|
aria-hidden="true"
|
|
className="platform-profile-avatar h-[5.15rem] w-[5.15rem] shrink-0 rounded-full object-cover"
|
|
/>
|
|
<div className="min-w-0">
|
|
<div className="text-base font-black text-[var(--platform-text-strong)]">
|
|
尚未登录
|
|
</div>
|
|
<PlatformActionButton className="mt-3" onClick={onLogin}>
|
|
登录
|
|
</PlatformActionButton>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const avatarUrl = user.avatarUrl?.trim() || null;
|
|
const publicUserCode = resolveActivePublicUserCode(user);
|
|
|
|
return (
|
|
<main
|
|
className="platform-profile-page platform-remap-surface w-full space-y-4 pb-2"
|
|
aria-label="我的"
|
|
>
|
|
<section className="platform-profile-header">
|
|
<img
|
|
src={profileStillLifeImage}
|
|
alt=""
|
|
aria-hidden="true"
|
|
className="platform-profile-scene-decor"
|
|
/>
|
|
<div className="platform-profile-header__identity">
|
|
<div className="platform-profile-header__identity-row flex min-w-0 items-center gap-4">
|
|
<button
|
|
type="button"
|
|
className="platform-profile-avatar relative h-[5.15rem] w-[5.15rem] shrink-0 overflow-hidden rounded-full"
|
|
aria-label="上传头像"
|
|
onClick={openAvatarPicker}
|
|
>
|
|
{avatarUrl ? (
|
|
<img
|
|
src={avatarUrl}
|
|
alt=""
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
) : (
|
|
<img
|
|
src={profileMascotImage}
|
|
alt=""
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
)}
|
|
<span className="platform-profile-camera absolute bottom-0 right-0 flex h-7 w-7 items-center justify-center rounded-full">
|
|
<Camera className="h-3.5 w-3.5" aria-hidden="true" />
|
|
</span>
|
|
</button>
|
|
<input
|
|
ref={avatarFileInputRef}
|
|
type="file"
|
|
aria-label="上传头像"
|
|
accept="image/jpeg,image/png,image/webp"
|
|
className="hidden"
|
|
onChange={(event) =>
|
|
handleAvatarFileChange(event.target.files?.[0] ?? null)
|
|
}
|
|
/>
|
|
<div className="platform-profile-header__text min-w-0">
|
|
<div className="flex items-center gap-2">
|
|
<div className="truncate text-[18px] font-black leading-tight text-[var(--platform-text-strong)]">
|
|
{user.displayName}
|
|
</div>
|
|
<PlatformIconButton
|
|
label="修改昵称"
|
|
icon={<Pencil className="h-3.5 w-3.5" />}
|
|
onClick={openNicknameModal}
|
|
className="platform-profile-edit-button"
|
|
/>
|
|
</div>
|
|
<div className="mt-2 flex flex-wrap items-center gap-2 text-[12px] text-[var(--platform-text-base)]">
|
|
<span>陶泥号: {publicUserCode}</span>
|
|
<CopyFeedbackButton
|
|
state={copyState}
|
|
idleLabel="复制"
|
|
showIcon={false}
|
|
className="platform-profile-copy-button"
|
|
onClick={() => {
|
|
void copyText(publicUserCode);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section className="platform-profile-stats-panel" aria-label="我的数据">
|
|
<div className="platform-profile-stats-grid grid grid-cols-3 gap-3">
|
|
{isLoadingDashboard ? (
|
|
<>
|
|
<ProfileStatCardSkeleton />
|
|
<ProfileStatCardSkeleton />
|
|
<ProfileStatCardSkeleton />
|
|
</>
|
|
) : (
|
|
<>
|
|
<ProfileStatCard
|
|
cardKey="wallet"
|
|
label="泥点余额"
|
|
value={
|
|
dashboard
|
|
? formatDashboardCount(dashboard.walletBalance)
|
|
: '暂不可用'
|
|
}
|
|
icon={Coins}
|
|
imageSrc={profilePointImage}
|
|
onClick={onOpenWalletLedger}
|
|
/>
|
|
<ProfileStatCard
|
|
cardKey="playTime"
|
|
label="累计游戏时长"
|
|
value={
|
|
dashboard
|
|
? formatTotalPlayTime(dashboard.totalPlayTimeMs)
|
|
: '暂不可用'
|
|
}
|
|
icon={History}
|
|
imageSrc={profileClockImage}
|
|
/>
|
|
<ProfileStatCard
|
|
cardKey="playedWorks"
|
|
label="已玩游戏数量"
|
|
value={
|
|
dashboard
|
|
? `${formatDashboardCount(dashboard.playedWorldCount)}个`
|
|
: '暂不可用'
|
|
}
|
|
icon={UserRound}
|
|
imageSrc={profileGamepadImage}
|
|
/>
|
|
</>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
<section
|
|
className="platform-profile-shortcut-panel"
|
|
aria-label="常用功能"
|
|
>
|
|
<div className="platform-profile-shortcut-grid grid w-full grid-cols-4">
|
|
<ProfileShortcutButton
|
|
label="泥点充值"
|
|
subLabel="充值泥点"
|
|
icon={Coins}
|
|
imageSrc={profileCoinsImage}
|
|
onClick={onOpenRecharge}
|
|
/>
|
|
<ProfileShortcutButton
|
|
label="兑换码"
|
|
subLabel="领取福利"
|
|
icon={Ticket}
|
|
imageSrc={profileGiftImage}
|
|
onClick={onOpenRewardCode}
|
|
/>
|
|
<ProfileShortcutButton
|
|
label="玩家社区"
|
|
subLabel="交流心得"
|
|
icon={MessageCircle}
|
|
imageSrc={profileCommunityImage}
|
|
onClick={onOpenCommunity}
|
|
/>
|
|
<ProfileShortcutButton
|
|
label="反馈与建议"
|
|
subLabel="帮我们优化产品"
|
|
icon={MessageCircle}
|
|
imageSrc={profileFeedbackImage}
|
|
onClick={onOpenFeedback}
|
|
/>
|
|
</div>
|
|
</section>
|
|
|
|
<section
|
|
className="platform-profile-settings-panel"
|
|
aria-label="设置入口"
|
|
>
|
|
<ProfileSettingsRow
|
|
label="通用设置"
|
|
icon={Settings}
|
|
onClick={onOpenSettings}
|
|
/>
|
|
<ProfileSettingsRow
|
|
label="开发者 API Key"
|
|
icon={KeyRound}
|
|
onClick={onOpenApiKeys}
|
|
/>
|
|
</section>
|
|
|
|
<ProfileLegalSection onOpenDocument={setActiveLegalDocumentId} />
|
|
<LegalDocumentModal
|
|
document={activeLegalDocument}
|
|
open={Boolean(activeLegalDocument)}
|
|
onClose={() => setActiveLegalDocumentId(null)}
|
|
/>
|
|
{isNicknameModalOpen ? (
|
|
<ProfileNicknameModal
|
|
value={nicknameInput}
|
|
error={nicknameError}
|
|
isSaving={isSavingNickname}
|
|
onChange={(value) => {
|
|
setNicknameInput(value);
|
|
setNicknameError(null);
|
|
}}
|
|
onClose={() => setIsNicknameModalOpen(false)}
|
|
onSubmit={submitNickname}
|
|
/>
|
|
) : null}
|
|
{avatarSource && avatarImageSize ? (
|
|
<SquareImageCropModal
|
|
source={avatarSource}
|
|
imageSize={avatarImageSize}
|
|
cropRect={avatarCrop}
|
|
titleId="profile-avatar-crop-title"
|
|
labels={{
|
|
title: '裁剪头像',
|
|
close: '关闭头像裁剪',
|
|
editor: '头像裁剪操作区',
|
|
previewAlt: '头像裁剪预览',
|
|
cancel: '取消',
|
|
submit: '上传',
|
|
saving: '上传中',
|
|
}}
|
|
error={avatarError}
|
|
isSaving={isSavingAvatar}
|
|
onCropRectChange={updateAvatarCrop}
|
|
onClose={() => {
|
|
setAvatarSource(null);
|
|
setAvatarImageSize(null);
|
|
setAvatarError(null);
|
|
}}
|
|
onSubmit={submitAvatar}
|
|
/>
|
|
) : null}
|
|
{avatarError && !avatarSource ? (
|
|
<div className="pointer-events-none fixed left-1/2 top-5 z-[90] w-[min(92vw,22rem)] -translate-x-1/2 rounded-2xl border border-rose-400/25 bg-white px-4 py-3 text-center text-sm font-semibold text-rose-600 shadow-2xl">
|
|
{avatarError}
|
|
</div>
|
|
) : null}
|
|
</main>
|
|
);
|
|
}
|
|
|
|
export default PlatformActiveProfileView;
|