2025年12月13日星期六

短视频分镜任务脚本

1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/Bg6shY若失效,可用地址

1.购买服务器

阿里云:

服务器购买地址

https://t.aliyun.com/U/Bg6shY

若失效,可用地址

https://www.aliyun.com/daily-act/ecs/activity_selection?source=5176.29345612&userCode=49hts92d

腾讯云:

https://curl.qcloud.com/wJpWmSfU

若失效,可用地址

https://cloud.tencent.com/act/cps/redirect?redirect=2446&cps_key=ad201ee2ef3b771157f72ee5464b1fea&from=console

华为云

https://activity.huaweicloud.com/cps.html?fromacct=64b5cf7cc11b4840bb4ed2ea0b2f4468&utm_source=V1g3MDY4NTY=&utm_medium=cps&utm_campaign=201905

2.部署教程

2024年最新青龙面板跑脚本教程(一)持续更新中

3.代码如下

<!DOCTYPE html><html lang="zh-CN"><head>    <meta charset="UTF-8">    <meta name="viewport" content="width=device-width, initial-scale=1.0">    <title>短视频分镜脚本编辑器 - Pro版</title>    <!-- Tailwind CSS -->    <script src="https://cdn.tailwindcss.com"></script>    <!-- React & ReactDOM -->    <script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>    <!-- Babel for JSX -->    <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>    <!-- PDF Libraries -->    <script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
    <script>        tailwind.config = {            theme: {                extend: {                    colors: {                        slate: {                            850'#1e293b',                        }                    },                    fontFamily: {                        sans: ['Inter''Segoe UI''Roboto''system-ui''sans-serif'],                    },                    boxShadow: {                        'soft''0 4px 20px -2px rgba(0, 0, 0, 0.05)',                        'glow''0 0 15px rgba(99, 102, 241, 0.1)',                    }                }            }        }    </script>    <style>        body { font-family'Inter', sans-serif; -webkit-font-smoothing: antialiased; }        ::-webkit-scrollbar { width6pxheight6px; }        ::-webkit-scrollbar-track { background: transparent; }        ::-webkit-scrollbar-thumb { background#cbd5e1border-radius3px; }        ::-webkit-scrollbar-thumb:hover { background#94a3b8; }
        .glass-panel {            backgroundrgba(2552552550.95);            backdrop-filterblur(10px);        }    </style></head><body class="bg-slate-100 text-slate-800">    <div id="root"></div>
    <script type="text/babel">        const { useState, useRef, useEffect } = React;
        // --- Icons ---        const Icon = ({ children, className, ...props }) => (            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className={className} {...props}>                {children}            </svg>        );        const Plus = (props) => <Icon {...props}><path d="M5 12h14"/><path d="M12 5v14"/></Icon>;        const Trash2 = (props) => <Icon {...props}><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></Icon>;        const GripVertical = (props) => <Icon {...props}><circlecx="9"cy="12"r="1"/><circlecx="9"cy="5"r="1"/><circlecx="9"cy="19"r="1"/><circlecx="15"cy="12"r="1"/><circlecx="15"cy="5"r="1"/><circlecx="15"cy="19"r="1"/></Icon>;        const FileDown = (props) => <Icon {...props}><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><path d="M12 18v-6"/><path d="m9 15 3 3 3-3"/></Icon>;        const Settings = (props) => <Icon {...props}><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.1a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></Icon>;        const X = (props) => <Icon {...props}><path d="M18 6 6 18"/><path d="m6 6 12 12"/></Icon>;        const LayoutList = (props) => <Icon {...props}><rect width="7" height="7" x="3" y="3" rx="1"/><rect width="7" height="7" x="3" y="14" rx="1"/><path d="M14 4h7"/><path d="M14 9h7"/><path d="M14 15h7"/><path d="M14 20h7"/></Icon>;        const LayoutGrid = (props) => <Icon {...props}><rectwidth="7"height="7"x="3"y="3"rx="1"/><rectwidth="7"height="7"x="14"y="3"rx="1"/><rectwidth="7"height="7"x="14"y="14"rx="1"/><rectwidth="7"height="7"x="3"y="14"rx="1"/></Icon>;        const ImageIcon = (props) => <Icon {...props}><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></Icon>;        const Minus = (props) => <Icon {...props}><line x1="5" y1="12" x2="19" y2="12"/></Icon>;
        // --- Main Component ---        function StoryboardEditor() {            const [activeTab, setActiveTab] = useState('table');             const [fontSize, setFontSize] = useState(14); 
            // Initial Data            const [columns, setColumns] = useState([                { id'shot'name'镜号'type'text'width60 },                { id'visual'name'画面'type'image'width260 },                { id'reference'name'参考'type'image'width260 },                { id'size'name'景别'type'text'width100 },                { id'duration'name'时长 (秒)'type'text'width100 },                { id'content'name'内容/台词'type'textarea'width260 },                { id'angle'name'摄像机角度'type'text'width140 },            ]);
            const [rows, setRows] = useState([                { id1height200data: { shot'1'size'全景'duration'3s'content'主角走进房间,环顾四周'angle'平视' } },                { id2height200data: { shot'2'size'特写'duration'2s'content'特写主角惊讶的眼神'angle'微仰' } },                { id3height200data: { shot'3'size''duration''content''angle'' } },            ]);
            const contentRef = useRef(null);            const [isExporting, setIsExporting] = useState(false);
            // --- States for Interactions ---            const [resizing, setResizing] = useState(null);             const [draggedRowIndex, setDraggedRowIndex] = useState(null);            const [contextMenu, setContextMenu] = useState(null);
            // --- Effects ---            useEffect(() => {                const handleMouseMove = (e) => {                    if (!resizing) return;                    if (resizing.type === 'col') {                        const deltaX = e.clientX - resizing.startX;                        const newWidth = Math.max(50, resizing.startSize + deltaX);                        setColumns(cols => cols.map(col => col.id === resizing.id ? { ...col, width: newWidth } : col));                    } else if (resizing.type === 'row') {                        const deltaY = e.clientY - resizing.startY;                        const newHeight = Math.max(60, resizing.startSize + deltaY);                        setRows(rs => rs.map(row => row.id === resizing.id ? { ...row, height: newHeight } : row));                    }                };
                const handleMouseUp = () => {                    setResizing(null);                    document.body.style.cursor = 'default';                };
                const handleClickOutside = () => {                    if (contextMenu) setContextMenu(null);                };
                if (resizing) {                    window.addEventListener('mousemove', handleMouseMove);                    window.addEventListener('mouseup', handleMouseUp);                }                window.addEventListener('click', handleClickOutside);
                return () => {                    window.removeEventListener('mousemove', handleMouseMove);                    window.removeEventListener('mouseup', handleMouseUp);                    window.removeEventListener('click', handleClickOutside);                };            }, [resizing, contextMenu]);
            // --- Helper Functions ---            const startResizeCol = (e, colId, currentWidth) => {                e.preventDefault(); e.stopPropagation();                setResizing({ type'col'id: colId, startX: e.clientXstartSize: currentWidth });                document.body.style.cursor = 'col-resize';            };
            const startResizeRow = (e, rowId, currentHeight) => {                e.preventDefault(); e.stopPropagation();                setResizing({ type'row'id: rowId, startY: e.clientYstartSize: currentHeight });                document.body.style.cursor = 'row-resize';            };
            const handleDragStart = (e, index) => {                setDraggedRowIndex(index);                e.dataTransfer.effectAllowed = "move";            };
            const handleDragOver = (e, index) => {                e.preventDefault();                if (draggedRowIndex === null || draggedRowIndex === index) return;                const newRows = [...rows];                const draggedRow = newRows[draggedRowIndex];                newRows.splice(draggedRowIndex, 1);                newRows.splice(index, 0, draggedRow);                setRows(newRows);                setDraggedRowIndex(index);            };
            const handleDrop = (e) => {                e.preventDefault();                setDraggedRowIndex(null);            };
            const handleContextMenu = (e, colId) => {                e.preventDefault();                setContextMenu({ x: e.clientXy: e.clientYcolId: colId });            };
            const deleteColumnFromMenu = () => {                if (contextMenu && contextMenu.colId !== 'shot') {                    setColumns(columns.filter(col => col.id !== contextMenu.colId));                }                setContextMenu(null);            };
            const addRow = () => {                const newId = rows.length > 0 ? Math.max(...rows.map(r => r.id)) + 1 : 1;                const nextShotNum = rows.length + 1;                setRows([...rows, { id: newId, height200data: { shot: nextShotNum.toString() } }]);            };
            const deleteRow = (id) => {                setRows(rows.filter(row => row.id !== id));            };
            const updateCell = (rowId, colId, value) => {                setRows(rows.map(row => {                    if (row.id === rowId) return { ...row, data: { ...row.data, [colId]: value } };                    return row;                }));            };
            const addColumn = () => {                const newColId = `custom_${Date.now()}`;                setColumns([...columns, { id: newColId, name"新项目"type'text'width140 }]);            };
            const updateColumnName = (colId, newName) => {                setColumns(columns.map(col => col.id === colId ? { ...col, name: newName } : col));            };
            const handleImageUpload = (rowId, colId, e) => {                const file = e.target.files[0];                if (file) {                    const reader = new FileReader();                    reader.onloadend = () => updateCell(rowId, colId, reader.result);                    reader.readAsDataURL(file);                }            };
            const increaseFontSize = () => setFontSize(prev => Math.min(prev + 232));            const decreaseFontSize = () => setFontSize(prev => Math.max(prev - 210));
            // --- PDF Export Logic (Fixed for text disappearance & image distortion) ---            const exportToPDF = async () => {                setIsExporting(true);                try {                    const { jsPDF } = window.jspdf;                    const pdf = new jsPDF('l''mm''a4');
                    const pdfPageWidth = pdf.internal.pageSize.getWidth();                    const pdfPageHeight = pdf.internal.pageSize.getHeight();                    const margin = 10;                    const printableWidthMM = pdfPageWidth - (margin * 2);
                    const element = contentRef.current;                    const clone = element.cloneNode(true);
                    // Style the clone for capture                    const mmToPx = 3.78                    const printableWidthPx = printableWidthMM * mmToPx;
                    clone.style.position = 'absolute';                    clone.style.top = '-9999px';                    clone.style.left = '0';                    clone.style.zIndex = '-1';                    clone.style.backgroundColor = '#ffffff'                    clone.style.padding = '0'
                    if (activeTab === 'board') {                        clone.style.width = `${printableWidthPx}px`;                        clone.classList.remove('max-w-[1920px]');                        clone.style.maxWidth = 'none';                    } else {                        clone.style.width = `${element.scrollWidth}px`;                    }
                    document.body.appendChild(clone);
                    // --- 修复1:将 Input 替换为普通文本 Div (解决文字消失) ---                    const originalInputs = element.querySelectorAll('input, textarea');                    const cloneInputs = clone.querySelectorAll('input, textarea');
                    originalInputs.forEach((input, index) => {                        if (cloneInputs[index]) {                            const replacementDiv = document.createElement('div');                            const value = input.value;                            replacementDiv.innerText = value || '';
                            const computedStyle = window.getComputedStyle(input);                            replacementDiv.style.font = computedStyle.font;                            replacementDiv.style.fontSize = computedStyle.fontSize;                            replacementDiv.style.fontWeight = computedStyle.fontWeight;                            replacementDiv.style.color = computedStyle.color;                            replacementDiv.style.textAlign = computedStyle.textAlign;                            replacementDiv.style.padding = computedStyle.padding;                            replacementDiv.style.lineHeight = computedStyle.lineHeight;                            replacementDiv.style.whiteSpace = 'pre-wrap'                            replacementDiv.style.wordBreak = 'break-word';                            replacementDiv.style.width = '100%';                            replacementDiv.style.height = '100%';                            replacementDiv.style.display = 'flex';                            replacementDiv.style.alignItems = 'center'
                            if (input.tagName === 'TEXTAREA') {                                replacementDiv.style.alignItems = 'flex-start'                            }
                            cloneInputs[index].parentNode.replaceChild(replacementDiv, cloneInputs[index]);                        }                    });
                    // --- 修复2:将图片替换为背景图 (解决图片被拉伸变形) ---                    const cloneImages = clone.querySelectorAll('img');                    cloneImages.forEach((img) => {                        if (img.classList.contains('object-contain') && img.src) {                            const div = document.createElement('div');                            div.style.width = '100%';                            div.style.height = '100%';                            div.style.backgroundImage = `url("${img.src}")`;                            div.style.backgroundSize = 'contain';                            div.style.backgroundPosition = 'center center';                            div.style.backgroundRepeat = 'no-repeat';
                            img.parentNode.replaceChild(div, img);                        }                    });
                    // Wait for rendering                    await new Promise(r => setTimeout(r, 800));
                    const canvas = await window.html2canvas(clone, {                        scale2,                        useCORStrue,                        backgroundColor'#ffffff',                        windowWidth: clone.scrollWidth,                        windowHeight: clone.scrollHeight,                        scrollY0                    });
                    document.body.removeChild(clone);
                    const imgData = canvas.toDataURL('image/png');                    const imgProps = pdf.getImageProperties(imgData);                    const pdfImgRatio = imgProps.width / imgProps.height;
                    const renderWidth = printableWidthMM;                    const renderHeight = renderWidth / pdfImgRatio;                    const pageContentHeight = pdfPageHeight - (margin * 2);
                    let heightLeft = renderHeight;                    let positionY = margin;
                    pdf.addImage(imgData, 'PNG', margin, positionY, renderWidth, renderHeight);                    heightLeft -= pageContentHeight;
                    while (heightLeft > 0) {                        positionY -= pageContentHeight;                        pdf.addPage();                        pdf.addImage(imgData, 'PNG', margin, positionY, renderWidth, renderHeight);                        heightLeft -= pageContentHeight;                    }
                    pdf.save(`分镜脚本_${activeTab === 'table' ? '表格' : '故事版'}.pdf`);
                } catch (error) {                    console.error(error);                    alert("导出失败,请重试:" + error.message);                } finally {                    setIsExporting(false);                }            };
            return (                <div className="min-h-screen pb-10 flex flex-col bg-slate-50" onClick={() => setContextMenu(null)}>                    {/* Header */}                    <header className="bg-slate-900 border-b border-slate-800 sticky top-0 z-20 shadow-lg px-8 py-4 flex justify-between items-center text-white">                        <div className="flex items-center gap-8">                            <div className="flex items-center gap-3">                                <div className="bg-indigo-500 w-8 h-8 rounded-lg flex items-center justify-center shadow-glow">                                    <LayoutGrid className="w-5 h-5 text-white" />                                </div>                                <h1 className="text-lg font-bold tracking-tight">分镜脚本 <span className="text-indigo-400 font-light">Pro</span></h1>                            </div>                            <div className="flex bg-slate-800 p-1 rounded-full border border-slate-700">                                <button                                    onClick={() => setActiveTab('table')}                                    className={`flex items-center gap-2 px-5 py-1.5 text-xs font-bold uppercase tracking-wider rounded-full transition-all duration-300 ${                                        activeTab === 'table'                                         ? 'bg-indigo-500 text-white shadow-md transform scale-105'                                         : 'text-slate-400 hover:text-white hover:bg-slate-700'                                    }`}                                >                                    <LayoutList className="w-3 h-3" />                                    列表模式                                </button>                                <button                                    onClick={() => setActiveTab('board')}                                    className={`flex items-center gap-2 px-5 py-1.5 text-xs font-bold uppercase tracking-wider rounded-full transition-all duration-300 ${                                        activeTab === 'board'                                         ? 'bg-indigo-500 text-white shadow-md transform scale-105'                                         : 'text-slate-400 hover:text-white hover:bg-slate-700'                                    }`}                                >                                    <LayoutGrid className="w-3 h-3" />                                    故事版                                </button>                            </div>                        </div>
                        <div className="flex gap-4 items-center">                            <div className="flex items-center bg-slate-800/50 border border-slate-700 rounded-lg p-1 mr-2 backdrop-blur-sm">                                <button onClick={decreaseFontSize} className="p-1.5 hover:bg-slate-700 rounded text-slate-400 hover:text-white transition-colors" title="减小字体">                                    <Minus className="w-3 h-3" />                                </button>                                <span className="px-3 text-xs font-mono text-slate-300 w-10 text-center select-none">{fontSize}px</span>                                <button onClick={increaseFontSize} className="p-1.5 hover:bg-slate-700 rounded text-slate-400 hover:text-white transition-colors" title="增大字体">                                    <Plus className="w-3 h-3" />                                </button>                            </div>
                            <div className="h-6 w-px bg-slate-700 mx-1"></div>
                            {activeTab === 'table' && (                                <button                                     onClick={addColumn}                                    className="flex items-center gap-2 px-4 py-2 bg-transparent border border-slate-600 text-slate-300 rounded-lg hover:bg-slate-800 hover:border-slate-500 hover:text-white text-sm font-medium transition-colors"                                >                                    <Settings className="w-4 h-4" />                                    <span className="hidden sm:inline">增加列</span>                                </button>                            )}                            <button                                 onClick={exportToPDF}                                disabled={isExporting}                                className="flex items-center gap-2 px-5 py-2 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg text-sm font-medium transition-all shadow-lg hover:shadow-indigo-500/25 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"                            >                                {isExporting ? (                                    <span className="flex items-center gap-2"><div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin"></div> 生成中...</span>                                ) : (                                    <>                                        <FileDown className="w-4 h-4" />                                        导出 PDF                                    </>                                )}                            </button>                        </div>                    </header>
                    {/* Main Content */}                    <main className="p-8 flex-1 overflow-x-auto">                        <div ref={contentRef} className="bg-transparent min-w-full mx-auto max-w-[1920px]">
                            {/* Table Mode */}                            {activeTab === 'table' && (                                <div className="bg-white rounded-2xl shadow-soft border border-slate-200 overflow-hidden select-none">                                    {/* Table Header */}                                    <div className="flex border-b border-slate-200 bg-slate-50 min-w-max">                                        <div className="w-14 py-4 text-center font-bold text-slate-400 text-xs uppercase tracking-wider flex-shrink-0 flex items-center justify-center border-r border-slate-100">                                            序号                                        </div>                                        {columns.map((col) => (                                            <div                                                 key={col.id}                                                 className="px-3 py-4 text-left font-bold text-slate-500 text-xs uppercase tracking-wider flex-shrink-0 border-r border-slate-100 group relative flex items-center transition-colors hover:bg-slate-100"                                                style={{ width: `${col.width}px` }}                                                onContextMenu={(e) => handleContextMenu(e, col.id)}                                            >                                                <input                                                    type="text"                                                    value={col.name}                                                    onChange={(e) => updateColumnName(col.id, e.target.value)}                                                    className="bg-transparent border-none rounded px-2 py-1 w-full focus:outline-none focus:ring-2 focus:ring-indigo-500/50 focus:bg-white transition-all z-10 relative font-bold text-slate-700"                                                    style={{ fontSize: `${Math.max(fontSize, 12)}px` }}                                                />                                                {col.id !== 'shot' && (                                                    <button                                                        onClick={() => { if (window.confirm('确定要删除这一列吗?')) setColumns(columns.filter(c => c.id !== col.id)); }}                                                        className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-slate-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity z-20"                                                    >                                                        <X className="w-3 h-3" />                                                    </button>                                                )}                                                <div                                                     className="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize hover:bg-indigo-400 z-30 transition-colors"                                                    onMouseDown={(e) => startResizeCol(e, col.id, col.width)}                                                />                                            </div>                                        ))}                                        <div className="w-12 flex-shrink-0 bg-slate-50"></div>                                    </div>
                                    {/* Table Body */}                                    <div className="divide-y divide-slate-100 min-w-max bg-white">                                        {rows.map((row, index) => (                                            <div                                                 key={row.id}                                                 className={`flex group hover:bg-slate-50/80 transition-colors relative ${draggedRowIndex === index ? 'opacity-50 bg-indigo-50 ring-2 ring-indigo-500 z-10: ''}`}                                                style={{ height: `${row.height}px` }}                                                onDragOver={(e) => handleDragOver(e, index)}                                                onDrop={handleDrop}                                            >                                                <div                                                     className="w-14 border-r border-slate-100 flex flex-col items-center justify-center flex-shrink-0 relative bg-white group-hover:bg-slate-50 transition-colors cursor-move"                                                    draggable="true"                                                    onDragStart={(e) => handleDragStart(e, index)}                                                >                                                    <div className="w-8 h-8 rounded-lg flex items-center justify-center text-slate-300 group-hover:text-indigo-500 group-hover:bg-indigo-50 transition-all">                                                        <span className="font-mono font-bold text-sm text-slate-400 group-hover:hidden">{index + 1}</span>                                                        <GripVertical className="w-4 h-4 hidden group-hover:block" />                                                    </div>                                                    <div                                                         className="absolute bottom-0 left-0 right-0 h-1 cursor-row-resize hover:bg-indigo-400 z-30 transition-colors opacity-0 group-hover:opacity-100"                                                        onMouseDown={(e) => startResizeRow(e, row.id, row.height)}                                                    />                                                </div>
                                                {columns.map((col) => (                                                    <div                                                         key={col.id}                                                         className="p-2 flex-shrink-0 border-r border-slate-100/50 border-dashed flex items-center h-full relative"                                                        style={{ width: `${col.width}px` }}                                                    >                                                        {col.type === 'text' && (                                                            <input                                                                 type="text"                                                                 value={row.data[col.id] || ''}                                                                onChange={(e) => updateCell(row.id, col.id, e.target.value)}                                                                placeholder="点击输入..."                                                                className="w-full h-full bg-transparent border border-transparent rounded-lg px-3 focus:border-indigo-200 focus:bg-white focus:ring-4 focus:ring-indigo-500/10 focus:outline-none text-slate-700 placeholder-slate-300 transition-all"                                                                style={{ fontSize: `${fontSize}px` }}                                                            />                                                        )}                                                        {col.type === 'textarea' && (                                                            <textarea                                                                 value={row.data[col.id] || ''}                                                                onChange={(e) => updateCell(row.id, col.id, e.target.value)}                                                                placeholder="输入内容..."                                                                className="w-full h-full bg-transparent border border-transparent rounded-lg p-3 focus:border-indigo-200 focus:bg-white focus:ring-4 focus:ring-indigo-500/10 focus:outline-none text-slate-700 placeholder-slate-300 resize-none leading-relaxed transition-all"                                                                style={{ fontSize: `${fontSize}px` }}                                                            />                                                        )}                                                        {col.type === 'image' && (                                                            <div className="w-full h-[90%] bg-slate-50 border-2 border-dashed border-slate-200 rounded-xl hover:border-indigo-400 hover:bg-indigo-50/30 transition-all relative group/image overflow-hidden flex items-center justify-center mx-1">                                                                {row.data[col.id] ? (                                                                    <>                                                                        <img src={row.data[col.id]} alt="预览" className="w-full h-full object-contain" />                                                                        <div className="absolute inset-0 bg-black/0 group-hover/image:bg-black/10 transition-colors flex items-start justify-end p-2">                                                                            <button                                                                                 onClick={() => updateCell(row.id, col.id, null)}                                                                                className="p-1.5 bg-white text-red-500 rounded-lg shadow-sm opacity-0 group-hover/image:opacity-100 hover:bg-red-50 transition-all transform translate-y-2 group-hover/image:translate-y-0"                                                                            >                                                                                <Trash2 className="w-3 h-3" />                                                                            </button>                                                                        </div>                                                                    </>                                                                ) : (                                                                    <label className="absolute inset-0 flex flex-col items-center justify-center cursor-pointer text-slate-300 hover:text-indigo-500 transition-colors">                                                                        <div className="p-3 bg-white rounded-full shadow-sm mb-2 group-hover:scale-110 transition-transform">                                                                            <ImageIcon className="w-5 h-5" />                                                                        </div>                                                                        <span className="text-[10px] font-bold uppercase tracking-wide">上传</span>                                                                        <input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(row.id, col.id, e)} />                                                                    </label>                                                                )}                                                            </div>                                                        )}                                                    </div>                                                ))}                                                <div className="w-12 flex items-center justify-center flex-shrink-0 opacity-0 group-hover:opacity-100 transition-all">                                                    <button onClick={() => deleteRow(row.id)} className="p-2 bg-white border border-slate-200 hover:border-red-200 hover:bg-red-50 text-slate-400 hover:text-red-500 rounded-lg shadow-sm">                                                        <Trash2 className="w-4 h-4" />                                                    </button>                                                </div>                                            </div>                                        ))}                                    </div>                                    <div onClick={addRow} className="p-4 bg-slate-50 border-t border-slate-200 flex items-center justify-center text-slate-400 cursor-pointer hover:bg-slate-100 hover:text-indigo-600 transition-all group">                                        <div className="flex items-center gap-2 px-4 py-2 rounded-lg border border-dashed border-slate-300 group-hover:border-indigo-400 bg-white">                                            <Plus className="w-4 h-4" />                                            <span className="text-sm font-bold">添加新镜头</span>                                        </div>                                    </div>                                </div>                            )}
                            {/* Board Mode */}                            {activeTab === 'board' && (                                <div className="space-y-8">                                    <div className="flex justify-between items-center px-1">                                        <div>                                            <h2 className="text-2xl font-bold text-slate-800 tracking-tight">故事版预览</h2>                                            <p className="text-slate-500 text-sm">拖拽卡片调整顺序</p>                                        </div>                                        <button onClick={addRow} className="px-5 py-2.5 bg-indigo-600 hover:bg-indigo-700 text-white rounded-xl font-bold text-sm flex items-center shadow-lg shadow-indigo-500/20 transition-all active:scale-95">                                            <Plus className="w-4 h-4 mr-2" /> 新建镜头                                        </button>                                    </div>
                                    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-8">                                        {rows.map((row, index) => (                                            <div                                                 key={row.id}                                                draggable="true"                                                onDragStart={(e) => handleDragStart(e, index)}                                                onDragOver={(e) => handleDragOver(e, index)}                                                onDrop={handleDrop}                                                className={`bg-white rounded-2xl border border-slate-200 shadow-soft hover:shadow-xl hover:-translate-y-1 transition-all duration-300 overflow-hidden flex flex-col group ${draggedRowIndex === index ? 'opacity-40 scale-95 ring-2 ring-indigo-500' : ''}`}                                            >                                                <div className="px-5 py-4 border-b border-slate-100 flex justify-between items-center bg-white">                                                    <div className="flex items-center gap-2">                                                        <span className="flex items-center justify-center px-2 py-1 rounded bg-slate-100 text-xs font-bold text-slate-500">镜头 {row.data.shot}</span>                                                    </div>                                                    <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">                                                        <div className="cursor-move p-1.5 hover:bg-slate-100 rounded-md text-slate-400 hover:text-slate-600 transition-colors">                                                            <GripVertical className="w-4 h-4" />                                                        </div>                                                        <button onClick={() => deleteRow(row.id)} className="p-1.5 hover:bg-red-50 rounded-md text-slate-400 hover:text-red-500 transition-colors">                                                            <Trash2 className="w-4 h-4" />                                                        </button>                                                    </div>                                                </div>
                                                <div className="aspect-video bg-slate-50 relative group/img border-b border-slate-100 flex items-center justify-center overflow-hidden">                                                    <div className="absolute inset-0 opacity-[0.03]" style={{ backgroundImage: 'radial-gradient(#000 1pxtransparent 1px)', backgroundSize: '20px 20px' }}></div>
                                                    {row.data.visual ? (                                                        <>                                                            <img src={row.data.visual} alt="Story visual" className="w-full h-full object-contain z-10" />                                                            <div className="absolute inset-0 bg-black/0 group-hover/img:bg-black/10 z-20 transition-all flex items-start justify-end p-3">                                                                <button                                                                     onClick={() => updateCell(row.id, 'visual', null)}                                                                    className="p-2 bg-white text-red-500 rounded-lg shadow-lg opacity-0 group-hover/img:opacity-100 hover:bg-red-50 transition-all transform -translate-y-2 group-hover/img:translate-y-0"                                                                >                                                                    <Trash2 className="w-4 h-4" />                                                                </button>                                                            </div>                                                        </>                                                    ) : (                                                        <label className="w-full h-full flex flex-col items-center justify-center cursor-pointer text-slate-300 hover:text-indigo-500 hover:bg-indigo-50/20 transition-all z-10 group/upload">                                                            <div className="p-4 bg-white rounded-full shadow-sm mb-3 group-hover/upload:scale-110 group-hover/upload:shadow-md transition-all">                                                                <ImageIcon className="w-6 h-6" />                                                            </div>                                                            <span className="text-xs font-bold uppercase tracking-wide">上传画面</span>                                                            <input type="file" accept="image/*" className="hidden" onChange={(e) => handleImageUpload(row.id, 'visual', e)} />                                                        </label>                                                    )}                                                </div>
                                                <div className="p-5 flex-1 flex flex-col gap-4">                                                    <div>                                                        <label className="text-[10px] font-bold text-slate-400 uppercase tracking-wider block mb-2">内容描述</label>                                                        <textarea                                                            value={row.data.content || ''}                                                            onChange={(e) => updateCell(row.id, 'content', e.target.value)}                                                            placeholder="描述动作或台词..."                                                            rows={3}                                                            className="w-full bg-slate-50 border-none rounded-lg p-3 focus:ring-2 focus:ring-indigo-500/20 focus:bg-white text-slate-700 placeholder-slate-400 resize-none leading-relaxed text-sm transition-all"                                                            style={{ fontSize: `${fontSize}px` }}                                                        />                                                    </div>
                                                    <div className="grid grid-cols-2 gap-3 mt-auto pt-4 border-t border-slate-100">                                                        <div className="bg-slate-50 rounded-lg p-2 px-3">                                                            <label className="text-[10px] font-bold text-slate-400 uppercase block mb-1">景别</label>                                                            <input                                                                 type="text"                                                                 value={row.data.size || ''}                                                                 onChange={(e) => updateCell(row.id, 'size', e.target.value)}                                                                className="w-full bg-transparent border-none p-0 focus:ring-0 text-slate-700 font-medium"                                                                 placeholder="-"                                                                style={{ fontSize: `${Math.max(fontSize - 2, 12)}px` }}                                                            />                                                        </div>                                                        <div className="bg-slate-50 rounded-lg p-2 px-3">                                                            <label className="text-[10px] font-bold text-slate-400 uppercase block mb-1">时长</label>                                                            <input                                                                 type="text"                                                                 value={row.data.duration || ''}                                                                 onChange={(e) => updateCell(row.id, 'duration', e.target.value)}                                                                className="w-full bg-transparent border-none p-0 focus:ring-0 text-slate-700 font-medium"                                                                 placeholder="-"                                                                style={{ fontSize: `${Math.max(fontSize - 2, 12)}px` }}                                                            />                                                        </div>                                                    </div>                                                </div>                                            </div>                                        ))}
                                        <div                                             onClick={addRow}                                            className="min-h-[400px] rounded-2xl border-2 border-dashed border-slate-200 flex flex-col items-center justify-center text-slate-300 cursor-pointer hover:border-indigo-400 hover:text-indigo-500 hover:bg-indigo-50/10 transition-all gap-4 group"                                        >                                            <div className="w-16 h-16 rounded-full bg-slate-50 group-hover:bg-white flex items-center justify-center shadow-sm group-hover:shadow-md transition-all group-hover:scale-110">                                                <Plus className="w-8 h-8" />                                            </div>                                            <span className="font-bold text-sm uppercase tracking-wider">创建新镜头</span>                                        </div>                                    </div>                                </div>                            )}                        </div>
                        {contextMenu && (                            <div                                 className="fixed bg-white/90 backdrop-blur-md border border-slate-200 shadow-xl rounded-xl py-2 z-50 min-w-[160px] animate-in fade-in zoom-in-95 duration-100"                                style={{ top: contextMenu.yleft: contextMenu.x }}                            >                                <div className="px-4 py-2 text-[10px] font-bold text-slate-400 uppercase tracking-wider border-b border-slate-100 mb-1">                                    列选项                                </div>                                <button                                     onClick={deleteColumnFromMenu}                                    className={`w-full text-left px-4 py-2.5 text-sm hover:bg-red-50 hover:text-red-600 flex items-center gap-2 transition-colors ${contextMenu.colId === 'shot' ? 'opacity-50 cursor-not-allowed: 'text-slate-600'}`}                                    disabled={contextMenu.colId === 'shot'}                                >                                    <Trash2 className="w-4 h-4" />                                    删除此列                                </button>                            </div>                        )}                    </main>                </div>            );        }
        const root = ReactDOM.createRoot(document.getElementById('root'));        root.render(<StoryboardEditor />);    </script></body></html>
解析

这是在浏览器里跑的 短视频分镜脚本编辑器(Pro 版)

  • 支持两种视图:

    • 列表模式(表格):按行按列填分镜(镜号、画面、参考图、景别、时长、文案、机位等)。

    • 故事版模式(卡片瀑布流):每个镜头一张卡片,上面有画面图+文案+景别+时长,更适合预览和展示。

  • 支持:

    • 拖拽调整行顺序(镜头顺序)。

    • 拖拽调整列宽 / 行高

    • 动态增删列、增删镜头。

    • 每格支持 文字输入 或 图片上传预览

    • 调整整体 字体大小

    • 一键将当前视图 导出为 PDF(做了针对 input/textarea/图片的修复,避免 PDF 里文字消失/图片变形)。

可以理解为:一个带表格+故事板视图的分镜脚本工具 + PDF 导出器。

主要结构

在 StoryboardEditor 组件里:

  • activeTab:当前是 "table"(列表)还是 "board"(故事版) 模式。

  • fontSize:全局文字字号。

  • columns:列配置数组,每个列包括:

    • idnametype(text/textarea/image)、width

  • rows:分镜行数据,每一行:

    • id:行 ID

    • height:行高

    • data:一个对象 { shot, visual, reference, size, duration, content, angle, ... }

  • contentRef:指向导出 PDF 时要截图的 DOM 容器。

  • isExporting:当前是否正在导出 PDF。

  • resizing:当前是否在拖动调节行/列大小。

  • draggedRowIndex:当前被拖拽的行索引,用来实现拖拽排序。

  • contextMenu:右键列头时弹出的 "列选项" 菜单位置和目标列 ID。

主要方法

1. 列/行尺寸调整

const startResizeCol = (e, colId, currentWidth) => { ... }
const startResizeRow = (e, rowId, currentHeight) => { ... }
  • 作用:在列头右边 / 行底部按下鼠标,开始 列宽 / 行高拖拽调整

  • 配合 resizing 状态,在 useEffect 里监听 mousemove / mouseup

    • 鼠标移动时实时更新 columns.width 或 rows.height

    • 鼠标抬起时结束拖拽。

2. 行拖拽排序相关

const handleDragStart = (e, index) => { ... }
const handleDragOver = (e, index) => { ... }
const handleDrop = (e) => { ... }
  • handleDragStart:开始拖拽某一行时记录 draggedRowIndex

  • handleDragOver:拖拽到另一个行上方时,动态调整 rows 的顺序(实现行之间的交换/重排)。

  • handleDrop:拖拽结束,清空 draggedRowIndex

列表模式和故事版模式都用这套逻辑。

3. 列右键菜单(删除列)

const handleContextMenu = (e, colId) => { ... }
const deleteColumnFromMenu = () => { ... }
  • handleContextMenu

    • 在列头 onContextMenu 触发,记录右键菜单的屏幕坐标 + 列 ID。

    • 显示一个简单的 "列选项"浮层

  • deleteColumnFromMenu

    • 根据 contextMenu.colId 删除对应列(除了 shot 列被保护不能删)。

4. 行/列增删 & 单元格更新

const addRow = () => { ... }
const deleteRow = (id) => { ... }
const addColumn = () => { ... }
const updateCell = (rowId, colId, value) => { ... }
const updateColumnName = (colId, newName) => { ... }
  • addRow:新增一个镜头行,默认高度 200,并自动给 shot 填一个递增的镜号(1,2,3…)。

  • deleteRow(id):删除指定 id 的行。

  • addColumn:动态新增一列(默认类型 text,名为 "新项目")。

  • updateCell:更新某一行某一列的内容。

  • updateColumnName:列头可编辑,修改列名。

5. 图片上传

const handleImageUpload = (rowId, colId, e) => { ... }
  • 用 <input type="file"> 选中图片;

  • FileReader 读为 base64

  • 把 base64 存进对应 rows[rowId].data[colId]

  • 渲染时 <img src={row.data[colId]} /> 显示预览。

在列表模式的 image 单元格、故事版模式的 "画面" 区域都复用这套逻辑。

6. 字体大小调整

const increaseFontSize = () => { ... }
const decreaseFontSize = () => { ... }
  • 改变全局 fontSize 状态;

  • 各输入框/文本区域都用 style={{ fontSize: ${fontSize}px }} 绑定,整体字体跟着变。

7. 导出 PDF 核心逻辑

const exportToPDF = async () => { ... }

这是整个文件里最复杂、也是最关键的 "输出" 功能:

  1. 设置 isExporting=true,按钮进入 Loading 状态。

  2. 拿到 jsPDF 实例,配置 A4 横向页面。

  3. 克隆 contentRef 对应的 DOM 节点到一个隐藏区域(避免影响页面,专门用于截图)。

  4. 修复点1:表单控件转纯文本

    • 找出 clone 中所有 input, textarea

    • 逐个用 <div> 替换掉,把真实 value 写入,并复制原本的样式(字体、对齐等);

    • 这样 html2canvas 截图时不会出现 "文字看不见 / 光标状态" 的问题。

  5. 修复点2:图片转背景图,避免拉伸

    • 对 class="object-contain" 的 img,换成 div,用 background-image + background-size: contain 呈现;

    • 保证在不同缩放下不会变形。

  6. 等 DOM 渲染稳定一会儿(setTimeout 800ms)。

  7. 调 html2canvas 把整个 clone 渲为一张高分辨率 Canvas。

  8. canvas.toDataURL() 生成 PNG,再用 jsPDF 按宽度等比缩放,自动分页(高度超出 A4 就新增 page,继续绘制)。

  9. 最后 pdf.save('分镜脚本_表格/故事版.pdf') 下载文件。

  10. 无论成功失败,finally 里重置 isExporting=false

一句话:把当前模式下的分镜 UI 渲染成图片 → 按比例填充到 PDF,多页拆分 → 下载。

8. 视图渲染部分

  • Table 模式

    • 表头:可编辑列名、可右键、可拖动调整列宽、可删除列。

    • 表体:每行可拖拽排序、拖动行底部调行高、最后一列是删行按钮。

    • 单元格根据 col.type 分三种渲染:文本输入、文本域、图片上传区。

    • 一个可横向滚动的表格:

  • Board 模式

    • 顶部:镜头编号 + 拖拽/删除按钮。

    • 中间:画面区域(上传/预览)。

    • 底部:内容描述 textarea + 景别/时长两个小块输入。

    • 一个响应式网格(1~4列),每个镜头一张卡片:



注意

本文部分变量已做脱敏处理,仅用于测试和学习研究,禁止用于商业用途,不能保证其合法性,准确性,完整性和有效性,请根据情况自行判断。技术层面需要提供帮助,可以通过打赏的方式进行探讨。


历史脚本txt文件获取>>
服务器搭建,人工服务咨询>>

没有评论:

发表评论

蚂蚁森林自动收能量任务脚本

1.购买服务器阿里云:服务器购买地址https://t.aliyun.com/U/Bg6shY若失效,可用地址 1.购买服务器 阿里云: 服务器购买地址 https : //t.aliyun.com/U/Bg6shY 若失效,可用地址 https ://www.aliyun....