初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
import React, { useState, useRef } from 'react'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { cn } from '@/utils/cn'
import { Button } from '@/components/ui/button'
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { useSize } from './hooks/useSize'
import { PPTEditProvider, type SlideJson } from '@/components/html-editor/context'
import { PPTEditToolBar } from '@/components/html-editor/components/toolbar-web'
import { usePPTEditor } from '@/components/ppt-editor/hooks/usePPTEditor'
import { FloatingToolbar } from './FloatingToolbar'
import type { TaskArtifact } from '../nova-sdk'
export interface SlideItem {
content: string
file_name: string
file_type: string
id: string
path : string
title: string
[key: string]: unknown
}
export interface PptPreviewProps {
/** conversationId */
taskId: string | null | undefined
/** PPT 文件的 URL */
url: string
/** TaskArtifact */
artifact: TaskArtifact
/** 是否可编辑 */
editable?: boolean
}
/**
* PPT 预览组件
*/
export const PptPreview = ({ url, artifact, taskId, editable = true}: PptPreviewProps) => {
const [currentIndex, setCurrentIndex] = useState(0)
const [slideList, setSlideList] = useState<SlideItem[]>([])
const [sliderJson,setSliderJson] = useState<SlideJson>()
const [loading, setLoading] = useState(true)
const containerRef = useRef<HTMLDivElement>(null)
React.useEffect(() => {
if (!url) return
setLoading(true)
fetch(url)
.then(res => res.json())
.then(data => {
const slides = data.slide_list || []
setSlideList(slides)
setSliderJson(data)
})
.catch(() => setSlideList([]))
.finally(() => setLoading(false))
}, [url])
if (loading) {
return (
<div className="flex-1 flex flex-col items-center justify-center p-8 text-muted-foreground h-full">
<div className="w-8 h-8 border-2 border-muted border-t-primary rounded-full animate-spin" />
<span className="text-sm mt-2">...</span>
</div>
)
}
if (!slideList || slideList.length === 0) {
return (
<div className="flex-1 flex flex-col items-center justify-center p-8 text-muted-foreground h-full">
<div className="text-6xl mb-4">📊</div>
<p className="text-sm"></p>
</div>
)
}
return (
<PPTEditProvider slides={slideList}>
<div className='relative h-full' ref={containerRef}>
{editable && <PPTEditToolBar containerRef={containerRef} />}
<PptSlideViewer
slideList={slideList}
currentIndex={currentIndex}
setCurrentIndex={setCurrentIndex}
artifact={artifact}
sliderJson={sliderJson}
taskId={taskId}
editable={editable}
/>
</div>
</PPTEditProvider>
)
}
function PptSlideViewer({
slideList,
currentIndex,
setCurrentIndex,
artifact,
sliderJson,
taskId,
editable
}: {
slideList: SlideItem[]
currentIndex: number
setCurrentIndex: (index: number) => void
artifact: TaskArtifact
sliderJson: SlideJson | undefined
taskId:string | null | undefined
editable: boolean
}) {
const containerRef = useRef<HTMLDivElement>(null)
const iframeRef = useRef<HTMLIFrameElement>(null)
const size = useSize(containerRef)
const [iframeHeight, setIframeHeight] = useState(720)
const [loadState, setLoadState] = useState<'loading' | 'loaded' | 'error'>('loading')
const currentSlide = slideList[currentIndex]
const scale = size ? size.width / 1280 : 1
const handleIframeLoad = (event: React.SyntheticEvent<HTMLIFrameElement>) => {
const iframe = event.currentTarget
try {
const actualHeight = iframe.contentDocument?.documentElement.scrollHeight
if (actualHeight && actualHeight > 0) {
setIframeHeight(actualHeight)
}
setLoadState('loaded')
} catch (error) {
console.warn('Cannot access iframe content:', error)
setLoadState('loaded')
}
}
const handleIframeError = () => {
setLoadState('error')
}
// 切换幻灯片时重置加载状态
React.useEffect(() => {
setLoadState('loading')
setIframeHeight(720)
}, [currentIndex])
const editState = usePPTEditor(currentSlide.id, iframeRef, scale, sliderJson, artifact, taskId, editable)
return (
<div className="flex flex-col h-full">
{/* 悬浮工具栏 */}
{editable && (
<div style={{ width: 'max-content', margin: '0 auto', display: 'flex', justifyContent: 'center', padding: '12px 0 0', zIndex: 100, position: 'relative' }}>
<FloatingToolbar
canUndo={editState.canUndo}
canRedo={editState.canRedo}
onUndo={() => editState.undo.current()}
onRedo={() => editState.redo.current()}
onSave={editState.manualSave}
isSaving={editState.isSaving}
saveType={editState.saveType}
/>
</div>
)}
{/* 主预览区 */}
<div className="flex flex-1 flex-col items-center overflow-hidden py-4">
<ScrollArea className="flex-1 w-full px-4 rounded-lg">
<div className="flex min-h-full w-full justify-center">
<div
ref={containerRef}
className="relative w-full flex-none overflow-hidden rounded-lg border border-solid border-border bg-card shadow-[0_18px_42px_var(--editor-shadow)]"
style={{
height: scale ? `${iframeHeight * scale}px` : '720px',
}}
>
{loadState === 'loading' && (
<div className="absolute inset-0 flex flex-col items-center justify-center bg-card/92 backdrop-blur-sm">
<div className="w-8 h-8 border-2 border-muted border-t-primary rounded-full animate-spin" />
<p className="text-muted-foreground text-sm mt-2">...</p>
</div>
)}
<iframe
ref={iframeRef}
srcDoc={currentSlide.content}
className={cn(
'w-[1280px] border-0 origin-top-left transition-opacity duration-300 ease-in-out',
loadState === 'loading' ? 'opacity-0' : 'opacity-100'
)}
title={`Slide ${currentIndex + 1}`}
sandbox="allow-same-origin allow-scripts"
onLoad={handleIframeLoad}
onError={handleIframeError}
style={{
height: `${iframeHeight}px`,
transform: `scale(${scale})`,
}}
/>
</div>
</div>
</ScrollArea>
{/* 页码和导航 */}
<div className="mt-4 flex shrink-0 items-center gap-4">
<Button
variant="outline"
size="icon"
onClick={() => setCurrentIndex(Math.max(0, currentIndex - 1))}
disabled={currentIndex === 0}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<span className="text-sm font-medium text-foreground">
{currentIndex + 1} / {slideList.length}
</span>
<Button
variant="outline"
size="icon"
onClick={() => setCurrentIndex(Math.min(slideList.length - 1, currentIndex + 1))}
disabled={currentIndex === slideList.length - 1}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
{/* 缩略图列表 */}
{slideList.length > 1 && (
<div className="shrink-0 border-t border-border/80 bg-secondary/35 p-3">
<ScrollArea className="w-full">
<div className="flex gap-2 pb-1 w-max">
{slideList.map((slide, index) => (
<button
key={index}
type="button"
className={cn(
'relative aspect-video w-48 shrink-0 overflow-hidden rounded-lg border-2 bg-card transition-all shadow-[0_10px_24px_var(--editor-shadow)]',
currentIndex === index
? 'border-primary shadow-md ring-2 ring-primary/20'
: 'border-transparent hover:border-muted'
)}
onClick={() => setCurrentIndex(index)}
>
<div className="w-full h-full overflow-hidden">
<iframe
srcDoc={slide.content}
className="w-full h-full border-0 pointer-events-none origin-top-left"
title={`Thumbnail ${index + 1}`}
sandbox="allow-same-origin"
style={{
transform: 'scale(1)',
}}
/>
</div>
<div className="absolute inset-0 bg-transparent pointer-events-none" />
<div className="pointer-events-none absolute bottom-0 left-0 right-0 bg-black/55 py-1 text-center text-xs text-white">
{index + 1}
</div>
</button>
))}
</div>
<ScrollBar orientation="horizontal" />
</ScrollArea>
</div>
)}
</div>
)
}
export default PptPreview