2e14873f2d
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore. Co-authored-by: Cursor <cursoragent@cursor.com>
124 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|