完善 TipTap 编辑器与个人资料页:头像裁剪、Tab 缩进、文章内链、Markdown 工具与 Tooltip,并更新样式与 Feed 缓存。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-16 05:57:15 +08:00
parent e4d1dd139e
commit 57172eb053
56 changed files with 3128 additions and 531 deletions

View File

@@ -0,0 +1,116 @@
import { useCallback, useEffect, useState } from 'react';
import Cropper, { type Area } from 'react-easy-crop';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Spinner } from '@/components/ui/spinner';
import { notify } from '@/lib/notify';
import { getCroppedAvatarFile } from '../utils/avatarCrop';
interface Props {
open: boolean;
imageSrc: string | null;
fileName?: string;
onOpenChange: (open: boolean) => void;
onConfirm: (file: File) => void;
}
export default function AvatarCropDialog({
open,
imageSrc,
fileName,
onOpenChange,
onConfirm,
}: Props) {
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [croppedAreaPixels, setCroppedAreaPixels] = useState<Area | null>(null);
const [confirming, setConfirming] = useState(false);
useEffect(() => {
if (open) {
setCrop({ x: 0, y: 0 });
setZoom(1);
setCroppedAreaPixels(null);
}
}, [open, imageSrc]);
const onCropComplete = useCallback((_: Area, pixels: Area) => {
setCroppedAreaPixels(pixels);
}, []);
const handleConfirm = async () => {
if (!imageSrc || !croppedAreaPixels) return;
setConfirming(true);
try {
const file = await getCroppedAvatarFile(imageSrc, croppedAreaPixels, fileName);
onConfirm(file);
onOpenChange(false);
} catch {
notify.error('裁剪失败,请重试');
} finally {
setConfirming(false);
}
};
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="avatar-crop-dialog">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
GIF JPG
</DialogDescription>
</DialogHeader>
<div className="avatar-crop-stage">
{imageSrc ? (
<Cropper
image={imageSrc}
crop={crop}
zoom={zoom}
aspect={1}
cropShape="round"
showGrid={false}
onCropChange={setCrop}
onZoomChange={setZoom}
onCropComplete={onCropComplete}
/>
) : (
<div className="avatar-crop-loading">
<Spinner size="lg" />
</div>
)}
</div>
<div className="avatar-crop-zoom">
<span className="avatar-crop-zoom-label"></span>
<input
type="range"
min={1}
max={3}
step={0.01}
value={zoom}
onChange={e => setZoom(Number(e.target.value))}
aria-label="缩放"
/>
</div>
<DialogFooter>
<Button variant="outline" disabled={confirming} onClick={() => onOpenChange(false)}>
</Button>
<Button loading={confirming} disabled={!imageSrc} onClick={handleConfirm}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}