feat: 新增引导式新手指引 (OnboardingTour) 组件,全站页面接入

This commit is contained in:
OmniAI Developer
2026-06-08 21:30:48 +08:00
parent 1e756808c1
commit 6ed65ca3ee
24 changed files with 1414 additions and 164 deletions
@@ -98,6 +98,10 @@ function DigitalHumanPage({
activeTaskIdRef.current = activeTaskId;
const keepaliveRestoredRef = useRef(false);
const [isDragging, setIsDragging] = useState(false);
const imageInputRef = useRef<HTMLInputElement | null>(null);
const audioInputRef = useRef<HTMLInputElement | null>(null);
const canvasDragCounterRef = useRef(0);
const [isCanvasDragging, setIsCanvasDragging] = useState(false);
useEffect(() => {
return () => {
@@ -171,6 +175,39 @@ function DigitalHumanPage({
}
};
const handleCanvasDragOver = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.dataTransfer.types.includes("Files")) setIsCanvasDragging(true); };
const handleCanvasDragLeave = (e: DragEvent) => { e.preventDefault(); e.stopPropagation(); if (e.currentTarget === e.target || !e.currentTarget.contains(e.relatedTarget as Node)) setIsCanvasDragging(false); };
const handleCanvasDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
setIsCanvasDragging(false);
const file = e.dataTransfer.files[0];
if (!file) return;
if (file.type.startsWith("image/")) {
if (imagePreview) URL.revokeObjectURL(imagePreview);
setImageName(file.name);
setImageFile(file);
setImagePreview(URL.createObjectURL(file));
pushDebugEntry("选择图片", `${file.name} / ${file.type || "unknown"} / ${formatFileSize(file.size)}`);
setNotice(`已拖放参考图 ${file.name}`);
} else if (file.type.startsWith("audio/")) {
if (audioPreview) URL.revokeObjectURL(audioPreview);
setAudioName(file.name);
setAudioFile(file);
setAudioPreview(URL.createObjectURL(file));
pushDebugEntry("选择音频", `${file.name} / ${file.type || "unknown"} / ${formatFileSize(file.size)}`);
setNotice(`已拖放音频 ${file.name}`);
}
};
const handleCanvasClick = () => {
if (!imagePreview) {
imageInputRef.current?.click();
} else if (!audioPreview) {
audioInputRef.current?.click();
}
};
const handleDownloadResult = async () => {
if (!resultVideoUrl || isDownloadingResult) return;
setIsDownloadingResult(true);
@@ -463,6 +500,7 @@ function DigitalHumanPage({
<div className="studio-panel__section-body">
<label className={imageName ? "studio-upload-slot--filled" : "studio-upload-slot--empty"}>
<input
ref={imageInputRef}
type="file"
accept="image/*"
onChange={(event) => {
@@ -501,6 +539,7 @@ function DigitalHumanPage({
<div className="studio-panel__section-body">
<label className={audioName ? "studio-upload-slot--filled" : "studio-upload-slot--empty"}>
<input
ref={audioInputRef}
type="file"
accept="audio/*"
onChange={(event) => {
@@ -541,12 +580,21 @@ function DigitalHumanPage({
<img src={imagePreview} alt="参考人像" />
</div>
) : (
<div className="studio-canvas-ghost">
<div
className={`studio-canvas-ghost${isCanvasDragging ? " is-dragging" : ""}`}
onClick={handleCanvasClick}
onDragOver={handleCanvasDragOver}
onDragLeave={handleCanvasDragLeave}
onDrop={handleCanvasDrop}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") handleCanvasClick(); }}
>
<div className="studio-canvas-ghost__icon">
<CustomerServiceOutlined />
</div>
<div className="studio-canvas-ghost__title"></div>
<div className="studio-canvas-ghost__hint"></div>
<div className="studio-canvas-ghost__hint"> (PNG/JPG/WEBP) (MP3/WAV/M4A)</div>
</div>
)
}