视频信息和 AI 总结同页布局方法详解(AI 视频下载总结器的页面设计)

视频信息、字幕、AI 总结、思维导图、AI 问答五类内容挤进一个页面,最难的是让它们互不拥挤。第一版我把它们从上到下排成一个长列表,结果用户想找总结要滚三屏,上线没几天评论区就出现了不少”总结在哪”的反馈。我先后对比了上下流式、左右两栏、弹层抽屉三种布局,最终敲定”左栏固定视频信息 + 右栏 Tab 切换内容”的两栏结构:左侧用 Sticky 钉住视频信息,右侧用折叠面板收纳四类内容。改造后,找总结的操作从三次点击变成一次,移动端也不再打架。下面把这套页面设计的完整思路和踩过的坑写出来。

一、整体布局

整体布局决定用户第一眼看到什么,也决定了后续所有代码的骨架。动手前我拿三个候选方案过了一轮,对比维度是信息密度、实现成本和移动端表现。上下流式最省事,但信息密度低,手机上第一屏只能看到标题;弹层抽屉能把辅助内容藏起来,可实现成本高,还容易让用户找不到入口;两栏方案信息密度高、桌面端体验直观,代价是移动端必须折叠处理。我最终选了左右两栏,当时的对比结论如下表。

布局方案 信息密度 实现成本 移动端表现 适用场景
上下流式 低 低 自然 内容少的简单页
左右两栏 + Sticky 高 中 需折叠处理 信息型详情页
弹层抽屉 中 高 一般 辅助信息补充

表格里两栏方案在移动端写的是”需折叠”,这正是后期投入精力最多的地方。折叠不是简单隐藏,涉及 Tab 状态记忆、隐藏容器尺寸归零导致思维导图渲染异常等一系列问题,这些会在后面的章节逐个展开。整体结构示意如下。

┌─────────────────────────────────────────────────┐
│  Header (导航 + 配额)                            │
├─────────────────────────────────────────────────┤
│  ┌─────────────┐  ┌─────────────────────────┐  │
│  │  Video      │  │  Tabs: 总结 | 思维导图  │  │
│  │  - 缩略图    │  │         | 字幕 | 问答    │  │
│  │  - 标题     │  │                         │  │
│  │  - 时长     │  │  总结内容(流式生成)     │  │
│  │  - 下载     │  │                         │  │
│  │  - 操作     │  │                         │  │
│  │  (Sticky)   │  │                         │  │
│  └─────────────┘  └─────────────────────────┘  │
│  ┌─────────────────────────────────────────┐  │
│  │  AI 问答(追问)                          │  │
│  └─────────────────────────────────────────┘  │
└─────────────────────────────────────────────────┘

二、HTML 结构

布局定了之后要先搭骨架再谈样式。这段 HTML 解决的核心问题是:把页面拆成”左信息、右内容”两个独立区域,同时保证语义清晰,方便后续接入交互逻辑。结构上我把视频信息放进 aside,把四类内容放进 main,主次一目了然,屏幕阅读器和搜索引擎也能正确识别层级。

<div class="detail-page">
    <!-- 顶部进度条 -->
    <div id="progressBar" class="progress-bar">
        <div class="progress-fill" style="width: 0%"></div>
        <span id="progressText">准备中...</span>
    </div>
    
    <div class="detail-grid">
        <!-- 左栏:视频信息(Sticky) -->
        <aside class="video-info">
            img 标签显示视频缩略图
            <h2 id="videoTitle">视频标题</h2>
            
            <div class="video-meta">
                <span class="badge" id="videoPlatform">YouTube</span>
                <span id="videoUploader">作者</span>
                <span id="videoDuration">12:30</span>
            </div>
            
            <div class="video-actions">
                <button class="btn-primary" id="btnSummary">
                    开始总结
                </button>
                <button class="btn-secondary" id="btnDownload">
                    下载视频
                </button>
                <button class="btn-secondary" id="btnCopyLink">
                    复制链接
                </button>
            </div>
            
            <div class="quota-info" id="quotaInfo">
                今日已用 1/3 次
            </div>
        </aside>
        
        <!-- 右栏:内容 Tab -->
        <main class="content">
            <nav class="tabs">
                <button class="tab active" data-tab="summary">
                    AI 总结
                </button>
                <button class="tab" data-tab="mindmap">思维导图</button>
                <button class="tab" data-tab="subtitle">字幕</button>
                <button class="tab" data-tab="qa">问答</button>
            </nav>
            
            <div class="tab-content" id="tabSummary">
                <article id="summary" class="markdown"></article>
                <div id="summaryActions" class="actions" hidden>
                    <button data-on-click="copySummary()">📋 复制</button>
                    <button data-on-click="exportMD()">⬇ 导出 Markdown</button>
                    <button data-on-click="exportPDF()">📄 导出 PDF</button>
                </div>
            </div>
            
            <div class="tab-content" id="tabMindmap" hidden>
                <div id="mindmap" class="mindmap-container"></div>
                <div class="mindmap-toolbar">
                    <button data-on-click="mm.fit()">适应</button>
                    <button data-on-click="mm.rescale(1.2)">放大</button>
                    <button data-on-click="exportMindmapPNG()">导出</button>
                </div>
            </div>
            
            <div class="tab-content" id="tabSubtitle" hidden>
                <pre id="subtitle" class="subtitle"></pre>
                <button data-on-click="downloadSubtitle()">下载字幕</button>
            </div>
            
            <div class="tab-content" id="tabQa" hidden>
                <div id="chatHistory" class="chat-history"></div>
                <form id="qaForm" class="qa-input">
                    <input type="text" id="qaInput" 
                           placeholder="向 AI 提问...">
                    <button type="submit">发送</button>
                </form>
            </div>
        </main>
    </div>
</div>

有几个细节容易忽略:进度条放在 body 最外层而不是内容区内,避免被滚动容器带走;左栏按钮统一用 btn-primary 和 btn-secondary 两级样式,视觉上一眼分清主操作。另外 Tab 内容用 hidden 属性控制显隐,而不是手动改 display,隐藏的元素不参与布局计算,切换更干净。

三、CSS 布局

样式部分我在 flex 和 grid 之间纠结了一阵。导航栏这种一行排开的组件用 flex 顺手,但详情页这种”固定侧栏 + 自适应内容”的二维结构,grid 的模板列更直白,所以 .detail-grid 用 grid,内部的按钮和元信息用 flex 排。

.detail-page {
    max-width: 1400px;
    margin: 0 auto;
    padding: 20px;
}

.detail-grid {
    display: grid;
    grid-template-columns: 360px 1fr;
    gap: 24px;
}

@media (max-width: 768px) {
    .detail-grid {
        grid-template-columns: 1fr;
    }
}

.video-info {
    position: sticky;
    top: 80px;  /* 顶部导航高度 */
    align-self: start;
    background: white;
    border-radius: 12px;
    padding: 16px;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.video-thumb {
    width: 100%;
    aspect-ratio: 16/9;
    object-fit: cover;
    border-radius: 8px;
    margin-bottom: 12px;
}

.content {
    background: white;
    border-radius: 12px;
    padding: 24px;
    min-height: 600px;
}

.tabs {
    display: flex;
    gap: 8px;
    border-bottom: 1px solid #e5e7eb;
    margin-bottom: 16px;
}

.tab {
    padding: 8px 16px;
    background: none;
    border: none;
    cursor: pointer;
    color: #6b7280;
    border-bottom: 2px solid transparent;
}

.tab.active {
    color: #3b82f6;
    border-bottom-color: #3b82f6;
}

这段 CSS 用 grid-template-columns 定死 360px 的左栏宽度,右侧 1fr 吃掉剩余空间。移动端在 768px 以下退化成单列,媒体查询里只需要覆盖这一条模板列,其余样式全部继承,这是 grid 比手写浮动布局省心的地方。

四、Tab 切换

Tab 切换最初考虑过纯 CSS 锚点方案,用 :target 就能实现,但遇到两个问题:一是切换时无法触发思维导图的重新布局,二是状态不留在 URL 里,刷新就丢。最后改成 JS 统一管理。

// tabs.js
function setupTabs() {
    document.querySelectorAll('.tab').forEach(tab => {
        tab.addEventListener('click', () => {
            const target = tab.dataset.tab
            
            // 更新按钮状态
            document.querySelectorAll('.tab').forEach(t => 
                t.classList.remove('active'))
            tab.classList.add('active')
            
            // 切换内容
            document.querySelectorAll('.tab-content').forEach(c => 
                c.hidden = true)
            document.getElementById(`tab${capitalize(target)}`).hidden = false
            
            // 触发布局更新(如思维导图 fit)
            if (target === 'mindmap' && window.mm) {
                setTimeout(() => window.mm.fit(), 100)
            }
        })
    })
}

function capitalize(s) {
    return s[0].toUpperCase() + s.slice(1)
}

这段代码用 data-tab 约定内容区 id,切换时先清掉所有 active 和 hidden,再点亮目标项。注意思维导图在隐藏 Tab 里 offsetWidth 是 0,所以切到 mindmap 时要等渲染完成再调 fit(),这也是第十二节踩坑清单里的老问题。

五、进度条

AI 总结是流式生成的,短则十几秒长则一两分钟,干等最劝退。我对比过转圈 spinner 和顶部进度条:spinner 只告诉用户”在等”,进度条还能展示当前进行到哪一步,用户的焦虑感明显更低。进度条要占的位置也很讲究,放进内容区会被滚动带走,所以做成固定定位贴在顶部导航下方。

<div id="progressBar" class="progress-bar" hidden>
    <div class="progress-fill" id="progressFill"></div>
    <div class="progress-text" id="progressText">准备中...</div>
</div>
.progress-bar {
    position: fixed;
    top: 64px;  /* 顶部导航下方 */
    left: 0;
    right: 0;
    height: 4px;
    background: #e5e7eb;
    z-index: 50;
}

.progress-fill {
    height: 100%;
    background: linear-gradient(90deg, #3b82f6, #8b5cf6);
    transition: width 0.3s;
}

.progress-text {
    position: absolute;
    right: 20px;
    top: 8px;
    font-size: 12px;
    color: #6b7280;
}
function updateProgress(stage, percent) {
    const bar = document.getElementById('progressBar')
    const fill = document.getElementById('progressFill')
    const text = document.getElementById('progressText')
    
    bar.hidden = false
    fill.style.width = `${percent}%`
    
    const stageNames = {
        downloading: '下载视频',
        transcribing: '提取字幕',
        summarizing: 'AI 总结',
        generating_mindmap: '生成思维导图'
    }
    text.textContent = `${stageNames[stage] || stage} ${percent}%`
}

function hideProgress() {
    document.getElementById('progressBar').hidden = true
}

percent 由后端按下载、转字幕、总结、导图四个阶段上报,前端只负责渲染,这样即使接口变慢,用户也能看到进度在推进。注意 stage 名和 percent 要一起更新,否则会出现卡在 100% 不动的错觉。

六、左右栏交互

左栏 Sticky 是这套布局的灵魂。用户滚动查看总结时,视频标题始终停在视野里,随时知道自己看的是哪条视频。第一次实现时 Sticky 怎么都不生效,排查半天发现是父容器加了 overflow: hidden,把粘性定位直接废掉了,这个坑后面专门记了一笔。

.video-info {
    position: sticky;
    top: 80px;
    align-self: start;
    max-height: calc(100vh - 100px);
    overflow-y: auto;
}

这段代码把左栏固定在顶部导航下方 80px 处,并限制最大高度、配合 overflow-y: auto,保证缩略图很高时也不会顶出屏幕。移动端单列布局里 Sticky 没有意义,直接在媒体查询里恢复 static。

@media (max-width: 768px) {
    .video-info {
        position: static;
    }
}

七、Markdown 内容样式

AI 返回的总结是 Markdown 文本,前端渲染成 HTML 后如果没有样式,标题、列表、引用会堆成一团。我一开始直接用了浏览器默认样式,桌面看着还行,手机上标题和正文混在一起,可读性很差。后来给 .markdown 容器定制了一套排版规则,把常见元素的间距、配色、层级都收进来。

.markdown {
    line-height: 1.7;
    color: #1f2937;
    font-size: 16px;
}

.markdown h1 {
    font-size: 2em;
    font-weight: bold;
    margin: 0.67em 0;
    border-bottom: 1px solid #e5e7eb;
    padding-bottom: 0.3em;
}

.markdown h2 {
    font-size: 1.5em;
    font-weight: bold;
    margin: 1em 0 0.5em;
}

.markdown p {
    margin: 0.8em 0;
}

.markdown code {
    background: #f1f5f9;
    padding: 2px 6px;
    border-radius: 4px;
    font-family: 'JetBrains Mono', monospace;
    font-size: 0.9em;
}

.markdown pre {
    background: #1e293b;
    color: #e2e8f0;
    padding: 16px;
    border-radius: 8px;
    overflow-x: auto;
}

.markdown pre code {
    background: transparent;
    color: inherit;
    padding: 0;
}

.markdown blockquote {
    border-left: 4px solid #3b82f6;
    padding-left: 16px;
    color: #6b7280;
    margin: 1em 0;
}

.markdown ul, .markdown ol {
    padding-left: 2em;
    margin: 0.8em 0;
}

核心是把行高、字体、代码块背景统一起来。pre 用深色底浅色字,和页面白底拉开对比;行内 code 用浅灰底。注意这些样式都挂在 .markdown 类名下,别污染页面里其他组件。

八、AI 问答区

问答区本来想跳转到独立页面,让用户在新页面追问,后来发现跳出详情页会打断阅读节奏,用户追完两轮就回不来了。于是改成详情页底部内嵌一个对话区,提问时带着视频 ID 一起提交,回答直接渲染在下方。

<div class="chat-history" id="chatHistory">
    <div class="chat-empty">
        <p>💡 试着问 AI:</p>
        <ul>
            <li>这个视频讲了什么?</li>
            <li>主要观点是什么?</li>
            <li>有什么实际应用?</li>
        </ul>
    </div>
</div>

<form id="qaForm" class="qa-input">
    <input type="text" id="qaInput" 
           placeholder="向 AI 提问关于视频的问题..." 
           autocomplete="off">
    <button type="submit">发送</button>
</form>
async function askQuestion(question) {
    // 1) 展示用户问题
    appendChatMessage('user', question)
    
    // 2) 流式 AI 回答
    const aiMessageEl = appendChatMessage('ai', '')
    
    const response = await fetch('/api/qa', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ 
            video_id: currentVideoId, 
            question 
        })
    })
    
    const reader = response.body.getReader()
    const decoder = new TextDecoder()
    let answer = ''
    let buffer = ''
    
    while (true) {
        const { done, value } = await reader.read()
        if (done) break
        
        buffer += decoder.decode(value, { stream: true })
        const events = buffer.split('\n\n')
        buffer = events.pop()
        
        for (const evt of events) {
            const lines = evt.split('\n')
            let data = ''
            for (const line of lines) {
                if (line.startsWith('data:')) data += line.slice(5).trim()
            }
            
            if (data) {
                const obj = JSON.parse(data)
                answer += obj.text
                aiMessageEl.textContent = answer
            }
        }
    }
}

后端接口用 SSE 流式返回,前端读 reader 时要按 \n\n 分段解析 data: 行,把增量拼接到 AI 消息节点上。解析 buffer 要留一个尾巴,否则最后一段数据会丢,这个 bug 我调了整整一个下午。

九、复制/导出

用户总结完大概率想把内容带走,复制和导出是刚需。导出格式支持 Markdown 和 PDF 两种,Markdown 走 turndown 反解析,PDF 交给后端生成,前端只发一个请求。

function copySummary() {
    const text = document.getElementById('summary').textContent
    navigator.clipboard.writeText(text)
        .then(() => showToast('已复制到剪贴板'))
}

function exportMD() {
    const html = document.getElementById('summary').innerHTML
    // 简单反解析回 Markdown(用 turndown.js)
    const md = turndown.turndown(html)
    downloadFile(md, 'summary.md', 'text/markdown')
}

复制这里有个坑:如果直接复制 innerHTML,会把富文本里的 HTML 标签一起带进剪贴板,粘贴到笔记软件全是乱码。改成 textContent 后问题消失,顺带也规避了脚本注入风险。

十、响应式

详情页在手机上最容易翻车,两栏宽度不够就得重新排。这个章节的 CSS 把断点分成两档:1024px 以下左栏收窄到 300px,768px 以下整体单列。收窄比直接单列更平滑,平板竖屏时还能保留双栏的观感。

@media (max-width: 1024px) {
    .detail-grid {
        grid-template-columns: 300px 1fr;
    }
}

@media (max-width: 768px) {
    .detail-grid {
        grid-template-columns: 1fr;
    }
    
    .video-info {
        position: static;
    }
    
    .tabs {
        overflow-x: auto;
        white-space: nowrap;
    }
}

移动端额外处理了 Tab 的横向滑动:四个 Tab 在窄屏上放不下时允许横滑而不是换行,保住桌面端的一行布局,视觉上也不会多出一截空白。

十一、可访问性

详情页信息密度高,不加语义标记的话,用屏幕阅读器的用户会迷失。给 Tab 加上 role 和 aria-selected,给内容面板加 role=tabpanel 和 aria-labelledby,读屏器就能正确播报当前状态。这块起初没在意,后来在无障碍走查时被单独列了一条问题才补上。

<button class="tab" data-tab="summary" 
        role="tab" aria-selected="true">总结</button>

<article id="summary" role="tabpanel" 
         aria-labelledby="tab-summary"></article>

键盘用户习惯用方向键在 Tab 间移动,所以额外监听 keydown,按左右方向键时切换 Tab。焦点管理要跟上,切到新面板后把焦点移到面板标题上,否则读屏器会停在原地。

document.querySelectorAll('.tab').forEach(tab => {
    tab.addEventListener('keydown', (e) => {
        if (e.key === 'ArrowRight') {
            // 切换到下一个 tab
        }
    })
})

十二、踩过的坑

这一节列的是真实上线后修过的问题,每条背后都是一次线上事故或测试反馈。最典型的是 Sticky 不生效和隐藏 Tab 里容器尺寸为 0,前者是父级 overflow 问题,后者需要在切换时主动 fit。按踩到的频率排了序。

  • Sticky 不生效:父元素 overflow: hidden 会让 sticky 失效。
  • 思维导图容器大小为 0:隐藏 tab 时 offsetWidth = 0。切换时 fit。
  • 流式渲染跳动:内容高度变化导致布局跳动。min-height 锁定。
  • 图片撑破布局:CSS max-width: 100%。
  • 长总结滚动条:内容区加 overflow-y: auto。
  • 移动端 Sticky 抖动:禁用 Sticky 用普通布局。
  • Tab 状态丢失:URL hash 记录当前 tab,刷新恢复。
  • 复制丢失格式:用 textContent 而非 innerHTML。

清单里每一条都能回溯到前面某个章节的实现细节,建议对照着读,大部分问题在第一版就能避开,省得后期返工。

十三、最佳实践清单

如果时间紧,没法把全部细节实现,优先保下面这七条,它们是性价比高的部分,覆盖了布局、交互、适配三个最影响体验的层面。

  1. 两栏布局(信息 + 内容);
  2. 视频信息 Sticky;
  3. Tab 切换保持状态;
  4. 进度条 + 阶段提示;
  5. 移动端单列布局;
  6. 内容区 min-height 锁定;
  7. 复制/导出辅助功能。

按这份清单做出来的详情页,至少能在桌面和手机两端保持可用,剩下的优化可以后续迭代。

常见问题(FAQ)

Q1:要不要加无限滚动?

不要。AI 总结是一次性内容,分页不自然。

Q2:思维导图放在哪里?

独立 tab 切换。内容区放不下。

Q3:要不要支持暗黑模式?

要。prefers-color-scheme: dark。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
蓝队管理员的头像蓝队管理员普通用户

相关推荐

返回顶部