- web/: React + Vite + TypeScript 前端 - backend/: Django + DRF + SimpleJWT 后端 - prototype/: HTML 设计原型 - docs/: PRD 和设计评审文档 - test: 单元测试 + E2E 极限测试
894 lines
36 KiB
HTML
894 lines
36 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>即梦 — AI 视频生成</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
colors: {
|
||
'page-bg': '#0a0a0f',
|
||
'bar-bg': '#16161e',
|
||
'bar-border': '#2a2a38',
|
||
'primary': '#00b8e6',
|
||
'txt-primary': '#ffffff',
|
||
'txt-secondary': '#8a8a9a',
|
||
'txt-disabled': '#4a4a5a',
|
||
'hover-bg': 'rgba(255,255,255,0.06)',
|
||
'dropdown-bg': '#1e1e2a',
|
||
'upload-bg': 'rgba(255,255,255,0.04)',
|
||
'upload-border': '#2a2a38',
|
||
'send-disabled': '#3a3a4a',
|
||
'send-active': '#00b8e6',
|
||
'sidebar-bg': '#0e0e14',
|
||
},
|
||
fontFamily: {
|
||
'sans': ['Noto Sans SC', 'system-ui', 'sans-serif'],
|
||
},
|
||
borderRadius: {
|
||
'bar': '20px',
|
||
'btn': '8px',
|
||
'thumb': '8px',
|
||
'drop': '12px',
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<style>
|
||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||
html, body { height: 100%; overflow: hidden; }
|
||
body { font-family: 'Noto Sans SC', system-ui, sans-serif; background: #0a0a0f; color: #fff; }
|
||
|
||
/* Scrollbar */
|
||
::-webkit-scrollbar { width: 4px; }
|
||
::-webkit-scrollbar-track { background: transparent; }
|
||
::-webkit-scrollbar-thumb { background: #2a2a38; border-radius: 4px; }
|
||
|
||
/* Textarea */
|
||
.prompt-textarea {
|
||
background: transparent;
|
||
border: none;
|
||
outline: none;
|
||
resize: none;
|
||
color: #fff;
|
||
font-size: 14px;
|
||
line-height: 1.6;
|
||
width: 100%;
|
||
min-height: 24px;
|
||
max-height: 144px;
|
||
font-family: 'Noto Sans SC', system-ui, sans-serif;
|
||
}
|
||
.prompt-textarea::placeholder {
|
||
color: #5a5a6a;
|
||
}
|
||
|
||
/* Upload area dashed border */
|
||
.upload-trigger {
|
||
border: 1.5px dashed #3a3a48;
|
||
background: rgba(255,255,255,0.03);
|
||
transition: all 0.2s;
|
||
cursor: pointer;
|
||
}
|
||
.upload-trigger:hover {
|
||
border-color: #5a5a6a;
|
||
background: rgba(255,255,255,0.06);
|
||
}
|
||
|
||
/* Toolbar button base */
|
||
.toolbar-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 0 12px;
|
||
height: 32px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
color: #8a8a9a;
|
||
background: transparent;
|
||
border: none;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
white-space: nowrap;
|
||
position: relative;
|
||
}
|
||
.toolbar-btn:hover {
|
||
background: rgba(255,255,255,0.06);
|
||
color: #b0b0c0;
|
||
}
|
||
.toolbar-btn.active-primary {
|
||
color: #00b8e6;
|
||
}
|
||
.toolbar-btn.active-primary:hover {
|
||
color: #33ccf0;
|
||
}
|
||
|
||
/* Dropdown menu */
|
||
.dropdown-menu {
|
||
position: absolute;
|
||
bottom: calc(100% + 8px);
|
||
left: 0;
|
||
background: #1e1e2a;
|
||
border: 1px solid #2a2a38;
|
||
border-radius: 12px;
|
||
padding: 6px;
|
||
min-width: 160px;
|
||
z-index: 100;
|
||
opacity: 0;
|
||
transform: translateY(8px);
|
||
pointer-events: none;
|
||
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||
box-shadow: 0 8px 32px rgba(0,0,0,0.4);
|
||
}
|
||
.dropdown-menu.open {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
pointer-events: auto;
|
||
}
|
||
.dropdown-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 8px 12px;
|
||
border-radius: 8px;
|
||
font-size: 13px;
|
||
color: #b0b0c0;
|
||
cursor: pointer;
|
||
transition: all 0.12s;
|
||
}
|
||
.dropdown-item:hover {
|
||
background: rgba(255,255,255,0.06);
|
||
color: #fff;
|
||
}
|
||
.dropdown-item.selected {
|
||
color: #00b8e6;
|
||
}
|
||
.dropdown-item .check-icon {
|
||
margin-left: auto;
|
||
opacity: 0;
|
||
}
|
||
.dropdown-item.selected .check-icon {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* Send button */
|
||
.send-btn {
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
border: none;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.send-btn.disabled {
|
||
background: #3a3a4a;
|
||
cursor: not-allowed;
|
||
}
|
||
.send-btn.enabled {
|
||
background: #00b8e6;
|
||
box-shadow: 0 2px 12px rgba(0,184,230,0.3);
|
||
}
|
||
.send-btn.enabled:hover {
|
||
background: #00ccff;
|
||
box-shadow: 0 4px 20px rgba(0,184,230,0.5);
|
||
}
|
||
|
||
/* Thumbnail */
|
||
.thumb-item {
|
||
position: relative;
|
||
width: 80px;
|
||
height: 80px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: #1a1a24;
|
||
flex-shrink: 0;
|
||
}
|
||
.thumb-item img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
.thumb-close {
|
||
position: absolute;
|
||
top: 4px;
|
||
right: 4px;
|
||
width: 18px;
|
||
height: 18px;
|
||
background: rgba(0,0,0,0.7);
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
opacity: 0;
|
||
transition: opacity 0.15s;
|
||
}
|
||
.thumb-item:hover .thumb-close { opacity: 1; }
|
||
.thumb-label {
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 0;
|
||
right: 0;
|
||
padding: 2px 0;
|
||
text-align: center;
|
||
font-size: 10px;
|
||
color: #fff;
|
||
background: linear-gradient(transparent, rgba(0,0,0,0.7));
|
||
}
|
||
|
||
/* Sidebar icon */
|
||
.sidebar-item {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 4px;
|
||
padding: 12px 0;
|
||
color: #5a5a6a;
|
||
cursor: pointer;
|
||
transition: color 0.15s;
|
||
font-size: 11px;
|
||
}
|
||
.sidebar-item:hover, .sidebar-item.active {
|
||
color: #b0b0c0;
|
||
}
|
||
.sidebar-item.active {
|
||
color: #00b8e6;
|
||
}
|
||
|
||
/* Keyframe arrow animation */
|
||
@keyframes arrow-pulse {
|
||
0%, 100% { opacity: 0.5; }
|
||
50% { opacity: 1; }
|
||
}
|
||
.arrow-animate { animation: arrow-pulse 2s ease-in-out infinite; }
|
||
|
||
/* Toast */
|
||
.toast {
|
||
position: fixed;
|
||
top: 60px;
|
||
left: 50%;
|
||
transform: translateX(-50%) translateY(-20px);
|
||
background: #1e1e2a;
|
||
border: 1px solid #2a2a38;
|
||
color: #fff;
|
||
padding: 10px 24px;
|
||
border-radius: 10px;
|
||
font-size: 13px;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||
z-index: 999;
|
||
}
|
||
.toast.show {
|
||
opacity: 1;
|
||
transform: translateX(-50%) translateY(0);
|
||
}
|
||
|
||
/* Responsive mobile */
|
||
@media (max-width: 767px) {
|
||
.toolbar-label { display: none; }
|
||
.sidebar { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Toast notification -->
|
||
<div id="toast" class="toast">已发送生成请求</div>
|
||
|
||
<!-- Layout: Sidebar + Main -->
|
||
<div class="flex h-full">
|
||
|
||
<!-- Sidebar -->
|
||
<aside class="sidebar w-[60px] h-full bg-[#0e0e14] border-r border-[#1a1a24] flex flex-col items-center py-4 flex-shrink-0 z-50">
|
||
<!-- Logo -->
|
||
<div class="mb-6 cursor-pointer">
|
||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||
<path d="M4 8L14 2L24 8V20L14 26L4 20V8Z" fill="#00b8e6" opacity="0.9"/>
|
||
<path d="M14 2L24 8L14 14L4 8L14 2Z" fill="#33ccf0"/>
|
||
<path d="M10 10L18 6" stroke="#fff" stroke-width="1.5" stroke-linecap="round" opacity="0.6"/>
|
||
</svg>
|
||
</div>
|
||
|
||
<div class="sidebar-item" title="灵感">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-4 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1"/></svg>
|
||
<span>灵感</span>
|
||
</div>
|
||
<div class="sidebar-item active" title="生成">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
||
<span>生成</span>
|
||
</div>
|
||
<div class="sidebar-item" title="资产">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 3v18"/></svg>
|
||
<span>资产</span>
|
||
</div>
|
||
<div class="sidebar-item" title="画布">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 01-2.83 2.83l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||
<span>画布</span>
|
||
</div>
|
||
|
||
<!-- Bottom items -->
|
||
<div class="mt-auto flex flex-col items-center gap-1">
|
||
<div class="sidebar-item" title="API">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||
<span class="text-[10px]">API</span>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- Main Content Area -->
|
||
<main class="flex-1 flex flex-col relative overflow-hidden">
|
||
<!-- Empty content area (feed would go here) -->
|
||
<div class="flex-1 flex items-center justify-center">
|
||
<p class="text-txt-disabled text-sm opacity-40 select-none">在下方输入提示词,开始创作 AI 视频</p>
|
||
</div>
|
||
|
||
<!-- InputBar — Fixed at bottom -->
|
||
<div class="w-full px-4 pb-5 pt-2" id="inputbar-wrapper">
|
||
<div class="mx-auto" style="max-width: 900px;">
|
||
<div class="bg-bar-bg border border-bar-border rounded-bar overflow-hidden" id="inputbar">
|
||
|
||
<!-- Upper area: Upload + Prompt -->
|
||
<div class="p-4 pb-2 flex gap-3" id="input-area">
|
||
|
||
<!-- Upload Section — Universal Mode -->
|
||
<div id="upload-universal" class="flex-shrink-0 flex gap-2 items-start">
|
||
<!-- Empty state: upload trigger -->
|
||
<div id="upload-trigger-btn" class="upload-trigger rounded-btn w-[80px] h-[80px] flex flex-col items-center justify-center gap-1" onclick="triggerUpload()">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||
<span class="text-[11px] text-txt-disabled">参考内容</span>
|
||
</div>
|
||
|
||
<!-- Thumbnail container (hidden initially) -->
|
||
<div id="thumb-container" class="flex gap-2 items-start" style="display:none;"></div>
|
||
|
||
<!-- Add more button (hidden initially) -->
|
||
<div id="add-more-btn" class="upload-trigger rounded-btn w-[80px] h-[80px] flex items-center justify-center" style="display:none;" onclick="triggerUpload()">
|
||
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Upload Section — Keyframe Mode (hidden) -->
|
||
<div id="upload-keyframe" class="flex-shrink-0 flex gap-3 items-center" style="display:none;">
|
||
<div class="upload-trigger rounded-btn w-[80px] h-[80px] flex flex-col items-center justify-center gap-1" onclick="triggerUpload('first')">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>
|
||
<span class="text-[11px] text-txt-disabled">首帧</span>
|
||
</div>
|
||
<div class="arrow-animate text-txt-disabled">
|
||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M7 12h10M17 12l-3-3M17 12l-3 3M7 12l3-3M7 12l3 3"/></svg>
|
||
</div>
|
||
<div class="upload-trigger rounded-btn w-[80px] h-[80px] flex flex-col items-center justify-center gap-1" onclick="triggerUpload('last')">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||
<span class="text-[11px] text-txt-disabled">尾帧</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Prompt Input -->
|
||
<div class="flex-1 py-1">
|
||
<textarea id="prompt-input" class="prompt-textarea" rows="1"
|
||
placeholder="上传1-5张参考图或视频,输入文字,自由组合图、文、音、视频多元素,定义精彩互动。"
|
||
oninput="autoResize(this); updateSendBtn()"
|
||
></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Divider -->
|
||
<div class="h-px bg-bar-border mx-4 opacity-50"></div>
|
||
|
||
<!-- Toolbar -->
|
||
<div class="flex items-center px-3 py-2 gap-1" id="toolbar">
|
||
|
||
<!-- 视频生成 dropdown -->
|
||
<div class="relative">
|
||
<button class="toolbar-btn active-primary" onclick="toggleDropdown('gen-dropdown')">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
||
<span class="toolbar-label">视频生成</span>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||
</button>
|
||
<div id="gen-dropdown" class="dropdown-menu" style="min-width:150px;">
|
||
<div class="dropdown-item selected" onclick="selectDropdown('gen-dropdown','视频生成',this)">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2" ry="2"/></svg>
|
||
视频生成
|
||
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
|
||
</div>
|
||
<div class="dropdown-item" onclick="selectDropdown('gen-dropdown','图片生成',this)">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>
|
||
图片生成
|
||
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Model selector -->
|
||
<div class="relative">
|
||
<button class="toolbar-btn" onclick="toggleDropdown('model-dropdown')">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
||
<span class="toolbar-label" id="model-label">Seedance 2.0</span>
|
||
</button>
|
||
<div id="model-dropdown" class="dropdown-menu" style="min-width:190px;">
|
||
<div class="dropdown-item selected" onclick="selectModel('Seedance 2.0', this)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2L2 7l10 5 10-5-10-5z"/></svg>
|
||
Seedance 2.0
|
||
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
|
||
</div>
|
||
<div class="dropdown-item" onclick="selectModel('Seedance 2.0 Fast', this)">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
|
||
Seedance 2.0 Fast
|
||
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Mode selector -->
|
||
<div class="relative">
|
||
<button class="toolbar-btn" onclick="toggleDropdown('mode-dropdown')" id="mode-btn">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" id="mode-icon"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||
<span class="toolbar-label" id="mode-label">全能参考</span>
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
|
||
</button>
|
||
<div id="mode-dropdown" class="dropdown-menu" style="min-width:150px;">
|
||
<div class="dropdown-item selected" id="mode-universal-item" onclick="switchMode('universal')">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>
|
||
全能参考
|
||
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
|
||
</div>
|
||
<div class="dropdown-item" id="mode-keyframe-item" onclick="switchMode('keyframe')">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M7 12h10M17 12l-3-3M17 12l-3 3M7 12l3-3M7 12l3 3"/></svg>
|
||
首尾帧
|
||
<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Aspect ratio selector -->
|
||
<div class="relative">
|
||
<button class="toolbar-btn" id="ratio-btn" onclick="toggleDropdown('ratio-dropdown')">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
|
||
<span class="toolbar-label" id="ratio-label">21:9</span>
|
||
</button>
|
||
<div id="ratio-dropdown" class="dropdown-menu" style="min-width:120px;">
|
||
<div class="dropdown-item" onclick="selectRatio('16:9',this)">16:9<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
<div class="dropdown-item" onclick="selectRatio('9:16',this)">9:16<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
<div class="dropdown-item" onclick="selectRatio('1:1',this)">1:1<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
<div class="dropdown-item selected" onclick="selectRatio('21:9',this)">21:9<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
<div class="dropdown-item" onclick="selectRatio('4:3',this)">4:3<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
<div class="dropdown-item" onclick="selectRatio('3:4',this)">3:4<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Duration selector -->
|
||
<div class="relative">
|
||
<button class="toolbar-btn" id="duration-btn" onclick="toggleDropdown('duration-dropdown')">
|
||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||
<span class="toolbar-label" id="duration-label">15s</span>
|
||
</button>
|
||
<div id="duration-dropdown" class="dropdown-menu" style="min-width:100px;">
|
||
<div class="dropdown-item" onclick="selectDuration('5s',this)">5s<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
<div class="dropdown-item" onclick="selectDuration('10s',this)">10s<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
<div class="dropdown-item selected" onclick="selectDuration('15s',this)">15s<svg class="check-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- @ button (universal mode only) -->
|
||
<button class="toolbar-btn" id="at-btn" onclick="insertAt()">
|
||
<span style="font-size:15px;font-weight:600;">@</span>
|
||
</button>
|
||
|
||
<!-- Spacer -->
|
||
<div class="flex-1"></div>
|
||
|
||
<!-- Credits indicator -->
|
||
<div class="flex items-center gap-1 text-txt-secondary text-xs mr-2 opacity-70">
|
||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||
<span>30</span>
|
||
</div>
|
||
|
||
<!-- Send button -->
|
||
<button class="send-btn disabled" id="send-btn" onclick="handleSend()">
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></svg>
|
||
</button>
|
||
</div>
|
||
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<!-- Hidden file input -->
|
||
<input type="file" id="file-input" accept="image/*,video/*" multiple style="display:none;" onchange="handleFiles(event)">
|
||
|
||
<script>
|
||
// ======== State ========
|
||
let state = {
|
||
mode: 'universal', // 'universal' | 'keyframe'
|
||
model: 'seedance_2.0',
|
||
ratio: '21:9',
|
||
duration: '15s',
|
||
prevRatio: '21:9',
|
||
prevDuration: '15s',
|
||
references: [], // [{id, file, previewUrl, label}]
|
||
firstFrame: null,
|
||
lastFrame: null,
|
||
uploadTarget: null, // 'first' | 'last' | null
|
||
};
|
||
let fileCounter = 0;
|
||
let openDropdownId = null;
|
||
|
||
// ======== Textarea auto-resize ========
|
||
function autoResize(el) {
|
||
el.style.height = 'auto';
|
||
el.style.height = Math.min(el.scrollHeight, 144) + 'px';
|
||
}
|
||
|
||
// ======== Send button state ========
|
||
function updateSendBtn() {
|
||
const btn = document.getElementById('send-btn');
|
||
const hasText = document.getElementById('prompt-input').value.trim().length > 0;
|
||
const hasFiles = state.mode === 'universal' ? state.references.length > 0 : (state.firstFrame || state.lastFrame);
|
||
if (hasText || hasFiles) {
|
||
btn.classList.remove('disabled');
|
||
btn.classList.add('enabled');
|
||
} else {
|
||
btn.classList.remove('enabled');
|
||
btn.classList.add('disabled');
|
||
}
|
||
}
|
||
|
||
// ======== File upload ========
|
||
function triggerUpload(target) {
|
||
state.uploadTarget = target || null;
|
||
const input = document.getElementById('file-input');
|
||
input.multiple = state.mode === 'universal';
|
||
input.click();
|
||
}
|
||
|
||
function handleFiles(event) {
|
||
const files = Array.from(event.target.files);
|
||
if (!files.length) return;
|
||
|
||
if (state.mode === 'universal') {
|
||
const remaining = 5 - state.references.length;
|
||
if (remaining <= 0) {
|
||
showToast('最多上传5张参考内容');
|
||
return;
|
||
}
|
||
const toAdd = files.slice(0, remaining);
|
||
toAdd.forEach(file => {
|
||
fileCounter++;
|
||
const previewUrl = URL.createObjectURL(file);
|
||
const type = file.type.startsWith('video') ? '视频' : '图片';
|
||
state.references.push({
|
||
id: 'ref_' + fileCounter,
|
||
file,
|
||
previewUrl,
|
||
label: type + fileCounter,
|
||
});
|
||
});
|
||
if (files.length > remaining) {
|
||
showToast('最多上传5张参考内容');
|
||
}
|
||
renderUniversalThumbs();
|
||
} else {
|
||
// Keyframe mode
|
||
const file = files[0];
|
||
const previewUrl = URL.createObjectURL(file);
|
||
if (state.uploadTarget === 'first') {
|
||
state.firstFrame = { file, previewUrl };
|
||
} else {
|
||
state.lastFrame = { file, previewUrl };
|
||
}
|
||
renderKeyframeThumbs();
|
||
}
|
||
|
||
updateSendBtn();
|
||
event.target.value = '';
|
||
}
|
||
|
||
function renderUniversalThumbs() {
|
||
const trigger = document.getElementById('upload-trigger-btn');
|
||
const container = document.getElementById('thumb-container');
|
||
const addMore = document.getElementById('add-more-btn');
|
||
|
||
if (state.references.length === 0) {
|
||
trigger.style.display = 'flex';
|
||
container.style.display = 'none';
|
||
addMore.style.display = 'none';
|
||
return;
|
||
}
|
||
|
||
trigger.style.display = 'none';
|
||
container.style.display = 'flex';
|
||
addMore.style.display = state.references.length < 5 ? 'flex' : 'none';
|
||
|
||
container.innerHTML = state.references.map((ref, i) => `
|
||
<div class="thumb-item" id="${ref.id}">
|
||
<img src="${ref.previewUrl}" alt="${ref.label}">
|
||
<div class="thumb-close" onclick="removeRef('${ref.id}')">
|
||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||
</div>
|
||
<div class="thumb-label">${ref.label}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function removeRef(id) {
|
||
const ref = state.references.find(r => r.id === id);
|
||
if (ref) URL.revokeObjectURL(ref.previewUrl);
|
||
state.references = state.references.filter(r => r.id !== id);
|
||
renderUniversalThumbs();
|
||
updateSendBtn();
|
||
}
|
||
|
||
function renderKeyframeThumbs() {
|
||
const section = document.getElementById('upload-keyframe');
|
||
const frames = section.querySelectorAll('.upload-trigger, .thumb-item-kf');
|
||
// Re-render the keyframe section
|
||
section.innerHTML = '';
|
||
|
||
// First frame
|
||
if (state.firstFrame) {
|
||
const div = document.createElement('div');
|
||
div.className = 'thumb-item';
|
||
div.style.width = '80px';
|
||
div.style.height = '80px';
|
||
div.innerHTML = `<img src="${state.firstFrame.previewUrl}" alt="首帧"><div class="thumb-close" onclick="removeKeyframe('first')"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></div><div class="thumb-label">首帧</div>`;
|
||
section.appendChild(div);
|
||
} else {
|
||
const div = document.createElement('div');
|
||
div.className = 'upload-trigger rounded-btn';
|
||
div.style.cssText = 'width:80px;height:80px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;';
|
||
div.onclick = () => triggerUpload('first');
|
||
div.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg><span class="text-[11px] text-txt-disabled">首帧</span>`;
|
||
section.appendChild(div);
|
||
}
|
||
|
||
// Arrow
|
||
const arrow = document.createElement('div');
|
||
arrow.className = 'arrow-animate text-txt-disabled';
|
||
arrow.innerHTML = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M7 12h10M17 12l-3-3M17 12l-3 3M7 12l3-3M7 12l3 3"/></svg>`;
|
||
section.appendChild(arrow);
|
||
|
||
// Last frame
|
||
if (state.lastFrame) {
|
||
const div = document.createElement('div');
|
||
div.className = 'thumb-item';
|
||
div.style.width = '80px';
|
||
div.style.height = '80px';
|
||
div.innerHTML = `<img src="${state.lastFrame.previewUrl}" alt="尾帧"><div class="thumb-close" onclick="removeKeyframe('last')"><svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg></div><div class="thumb-label">尾帧</div>`;
|
||
section.appendChild(div);
|
||
} else {
|
||
const div = document.createElement('div');
|
||
div.className = 'upload-trigger rounded-btn';
|
||
div.style.cssText = 'width:80px;height:80px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:4px;';
|
||
div.onclick = () => triggerUpload('last');
|
||
div.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#5a5a6a" stroke-width="1.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg><span class="text-[11px] text-txt-disabled">尾帧</span>`;
|
||
section.appendChild(div);
|
||
}
|
||
}
|
||
|
||
function removeKeyframe(which) {
|
||
if (which === 'first') {
|
||
if (state.firstFrame) URL.revokeObjectURL(state.firstFrame.previewUrl);
|
||
state.firstFrame = null;
|
||
} else {
|
||
if (state.lastFrame) URL.revokeObjectURL(state.lastFrame.previewUrl);
|
||
state.lastFrame = null;
|
||
}
|
||
renderKeyframeThumbs();
|
||
updateSendBtn();
|
||
}
|
||
|
||
// ======== Dropdowns ========
|
||
function toggleDropdown(id) {
|
||
const el = document.getElementById(id);
|
||
if (openDropdownId && openDropdownId !== id) {
|
||
document.getElementById(openDropdownId).classList.remove('open');
|
||
}
|
||
el.classList.toggle('open');
|
||
openDropdownId = el.classList.contains('open') ? id : null;
|
||
}
|
||
|
||
function closeAllDropdowns() {
|
||
document.querySelectorAll('.dropdown-menu.open').forEach(d => d.classList.remove('open'));
|
||
openDropdownId = null;
|
||
}
|
||
|
||
document.addEventListener('click', (e) => {
|
||
if (!e.target.closest('.toolbar-btn') && !e.target.closest('.dropdown-menu')) {
|
||
closeAllDropdowns();
|
||
}
|
||
});
|
||
|
||
function selectDropdown(dropdownId, label, item) {
|
||
const menu = document.getElementById(dropdownId);
|
||
menu.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
|
||
item.classList.add('selected');
|
||
closeAllDropdowns();
|
||
}
|
||
|
||
function selectModel(name, item) {
|
||
document.getElementById('model-label').textContent = name;
|
||
const menu = document.getElementById('model-dropdown');
|
||
menu.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
|
||
item.classList.add('selected');
|
||
closeAllDropdowns();
|
||
}
|
||
|
||
function selectRatio(ratio, item) {
|
||
state.ratio = ratio;
|
||
state.prevRatio = ratio;
|
||
document.getElementById('ratio-label').textContent = ratio;
|
||
const menu = document.getElementById('ratio-dropdown');
|
||
menu.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
|
||
item.classList.add('selected');
|
||
closeAllDropdowns();
|
||
}
|
||
|
||
function selectDuration(duration, item) {
|
||
state.duration = duration;
|
||
if (state.mode === 'universal') state.prevDuration = duration;
|
||
document.getElementById('duration-label').textContent = duration;
|
||
const menu = document.getElementById('duration-dropdown');
|
||
menu.querySelectorAll('.dropdown-item').forEach(i => i.classList.remove('selected'));
|
||
item.classList.add('selected');
|
||
closeAllDropdowns();
|
||
}
|
||
|
||
// ======== Mode switching ========
|
||
function switchMode(mode) {
|
||
if (state.mode === mode) { closeAllDropdowns(); return; }
|
||
state.mode = mode;
|
||
|
||
// Update mode dropdown selection
|
||
document.getElementById('mode-universal-item').classList.toggle('selected', mode === 'universal');
|
||
document.getElementById('mode-keyframe-item').classList.toggle('selected', mode === 'keyframe');
|
||
closeAllDropdowns();
|
||
|
||
const modeLabel = document.getElementById('mode-label');
|
||
const modeIcon = document.getElementById('mode-icon');
|
||
const uploadUniv = document.getElementById('upload-universal');
|
||
const uploadKf = document.getElementById('upload-keyframe');
|
||
const ratioBtn = document.getElementById('ratio-btn');
|
||
const ratioLabel = document.getElementById('ratio-label');
|
||
const durationLabel = document.getElementById('duration-label');
|
||
const atBtn = document.getElementById('at-btn');
|
||
const promptInput = document.getElementById('prompt-input');
|
||
|
||
if (mode === 'keyframe') {
|
||
modeLabel.textContent = '首尾帧';
|
||
modeIcon.innerHTML = '<path d="M7 12h10M17 12l-3-3M17 12l-3 3M7 12l3-3M7 12l3 3"/>';
|
||
uploadUniv.style.display = 'none';
|
||
uploadKf.style.display = 'flex';
|
||
renderKeyframeThumbs();
|
||
|
||
// Ratio: auto match, disabled
|
||
ratioLabel.textContent = '自动匹配';
|
||
ratioBtn.style.opacity = '0.5';
|
||
ratioBtn.style.pointerEvents = 'none';
|
||
|
||
// Duration: 5s
|
||
durationLabel.textContent = '5s';
|
||
state.duration = '5s';
|
||
document.querySelectorAll('#duration-dropdown .dropdown-item').forEach(i => {
|
||
i.classList.toggle('selected', i.textContent.trim() === '5s');
|
||
});
|
||
|
||
// Hide @ button
|
||
atBtn.style.display = 'none';
|
||
|
||
// Update placeholder
|
||
promptInput.placeholder = '输入描述,定义首帧到尾帧的运动过程';
|
||
|
||
// Clear universal references
|
||
state.references.forEach(r => URL.revokeObjectURL(r.previewUrl));
|
||
state.references = [];
|
||
renderUniversalThumbs();
|
||
} else {
|
||
modeLabel.textContent = '全能参考';
|
||
modeIcon.innerHTML = '<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>';
|
||
uploadUniv.style.display = 'flex';
|
||
uploadKf.style.display = 'none';
|
||
|
||
// Restore ratio
|
||
ratioLabel.textContent = state.prevRatio;
|
||
state.ratio = state.prevRatio;
|
||
ratioBtn.style.opacity = '1';
|
||
ratioBtn.style.pointerEvents = 'auto';
|
||
|
||
// Restore duration
|
||
durationLabel.textContent = state.prevDuration;
|
||
state.duration = state.prevDuration;
|
||
document.querySelectorAll('#duration-dropdown .dropdown-item').forEach(i => {
|
||
i.classList.toggle('selected', i.textContent.trim() === state.prevDuration);
|
||
});
|
||
|
||
// Show @ button
|
||
atBtn.style.display = 'inline-flex';
|
||
|
||
// Update placeholder
|
||
promptInput.placeholder = '上传1-5张参考图或视频,输入文字,自由组合图、文、音、视频多元素,定义精彩互动。';
|
||
|
||
// Clear keyframe
|
||
if (state.firstFrame) URL.revokeObjectURL(state.firstFrame.previewUrl);
|
||
if (state.lastFrame) URL.revokeObjectURL(state.lastFrame.previewUrl);
|
||
state.firstFrame = null;
|
||
state.lastFrame = null;
|
||
}
|
||
|
||
updateSendBtn();
|
||
}
|
||
|
||
// ======== @ Insert ========
|
||
function insertAt() {
|
||
const input = document.getElementById('prompt-input');
|
||
const start = input.selectionStart;
|
||
const end = input.selectionEnd;
|
||
const text = input.value;
|
||
input.value = text.substring(0, start) + '@' + text.substring(end);
|
||
input.selectionStart = input.selectionEnd = start + 1;
|
||
input.focus();
|
||
updateSendBtn();
|
||
}
|
||
|
||
// ======== Send ========
|
||
function handleSend() {
|
||
const btn = document.getElementById('send-btn');
|
||
if (btn.classList.contains('disabled')) return;
|
||
showToast('已发送生成请求');
|
||
}
|
||
|
||
// ======== Toast ========
|
||
function showToast(msg) {
|
||
const toast = document.getElementById('toast');
|
||
toast.textContent = msg;
|
||
toast.classList.add('show');
|
||
setTimeout(() => toast.classList.remove('show'), 2000);
|
||
}
|
||
|
||
// ======== Keyboard shortcut ========
|
||
document.addEventListener('keydown', (e) => {
|
||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||
handleSend();
|
||
}
|
||
});
|
||
|
||
// ======== Drag and drop ========
|
||
const inputbar = document.getElementById('inputbar');
|
||
inputbar.addEventListener('dragover', (e) => {
|
||
e.preventDefault();
|
||
inputbar.style.borderColor = '#00b8e6';
|
||
});
|
||
inputbar.addEventListener('dragleave', () => {
|
||
inputbar.style.borderColor = '#2a2a38';
|
||
});
|
||
inputbar.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
inputbar.style.borderColor = '#2a2a38';
|
||
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/') || f.type.startsWith('video/'));
|
||
if (!files.length) return;
|
||
|
||
// Simulate file input
|
||
const dt = new DataTransfer();
|
||
files.forEach(f => dt.items.add(f));
|
||
const input = document.getElementById('file-input');
|
||
input.files = dt.files;
|
||
input.dispatchEvent(new Event('change'));
|
||
});
|
||
|
||
// ======== Auto-focus ========
|
||
window.addEventListener('load', () => {
|
||
document.getElementById('prompt-input').focus();
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|