Files
memind/src/components/AvatarPicker.tsx
T
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

124 lines
3.1 KiB
TypeScript

import { useEffect, useId, useRef, useState } from 'react';
import { useUserAvatar } from '../hooks/useUserAvatar';
import {
AVATAR_PICKER_OPEN_EVENT,
getAvatarPickerInput,
openAvatarPicker,
registerAvatarPickerInput,
} from '../utils/userAvatar';
import { UserAvatar } from './UserAvatar';
type AvatarPickerProps = {
variant?: 'sidebar' | 'compact';
};
export function AvatarPicker({ variant = 'sidebar' }: AvatarPickerProps) {
const inputId = useId();
const inputRef = useRef<HTMLInputElement>(null);
const ownsInput = true;
const { avatarUrl, updateAvatar, resetAvatar } = useUserAvatar();
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
useEffect(() => {
if (!ownsInput) return;
registerAvatarPickerInput(inputRef.current);
const handleOpen = () => {
if (!busy) inputRef.current?.click();
};
window.addEventListener(AVATAR_PICKER_OPEN_EVENT, handleOpen);
return () => {
if (getAvatarPickerInput() === inputRef.current) {
registerAvatarPickerInput(null);
}
window.removeEventListener(AVATAR_PICKER_OPEN_EVENT, handleOpen);
};
}, [busy, ownsInput]);
const triggerPicker = () => {
if (busy) return;
if (ownsInput) {
inputRef.current?.click();
} else {
openAvatarPicker();
}
};
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) return;
setBusy(true);
setError(null);
try {
await updateAvatar(file);
} catch (err) {
setError(err instanceof Error ? err.message : '头像更新失败');
} finally {
setBusy(false);
}
};
const fileInput = ownsInput ? (
<input
ref={inputRef}
id={inputId}
type="file"
accept="image/jpeg,image/png,image/webp,image/gif"
className="avatar-picker-input"
onChange={(e) => void handleChange(e)}
/>
) : null;
if (variant === 'compact') {
return (
<>
<UserAvatar
avatarUrl={avatarUrl}
size="sm"
onClick={triggerPicker}
title={error ?? '点击更换头像'}
/>
{fileInput}
</>
);
}
return (
<div className="avatar-picker">
<button
type="button"
className="avatar-picker-trigger"
disabled={busy}
onClick={triggerPicker}
>
<UserAvatar avatarUrl={avatarUrl} size="md" />
<span className="avatar-picker-text">
<span className="avatar-picker-label">{busy ? '处理中…' : '更换我的头像'}</span>
<span className="avatar-picker-hint">JPG · PNG · WebP · 2MB</span>
</span>
</button>
{avatarUrl && (
<button
type="button"
className="avatar-picker-reset"
disabled={busy}
onClick={() => {
resetAvatar();
setError(null);
}}
>
</button>
)}
{error && <p className="avatar-picker-error">{error}</p>}
{fileInput}
</div>
);
}