3862996045
CI/CD Pipeline / Production Browser E2E (push) Failing after 1312h43m40s
CI/CD Pipeline / Deploy Production (push) Failing after 1312h43m42s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 1312h48m16s
CI/CD Pipeline / Build Production API Image (push) Failing after 1312h48m20s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 1312h49m4s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 1312h51m34s
CI/CD Pipeline / PR Build API Image (push) Failing after 1312h49m8s
CI/CD Pipeline / Build Staging API Image (push) Successful in 13m4s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m5s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m56s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 58s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 2m0s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m5s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m12s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 21s
CI/CD Pipeline / Unit Tests (push) Successful in 6m21s
CI/CD Pipeline / Integration Tests (push) Successful in 2m38s
CI/CD Pipeline / Frontend Lint (push) Successful in 46s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 2m39s
CI/CD Pipeline / PR Build Web Image (push) Failing after 1313h21m29s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m40s
CI/CD Pipeline / Build Production Web Image (push) Failing after 1313h20m41s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 35s
149 lines
4.5 KiB
TypeScript
149 lines
4.5 KiB
TypeScript
/**
|
|
* 左侧面板 — V8 原型 1:1 还原
|
|
* Tab 切换:模板列表 + 素材库
|
|
*/
|
|
import React, { useState } from "react"
|
|
import type { EditingTemplate } from "@/api/editing-planner"
|
|
import { MODE_LABELS } from "@/api/editing-planner"
|
|
import type { MediaAsset } from "@/api/template-editor"
|
|
import AssetSelector from "@/components/asset-selector/AssetSelector"
|
|
|
|
interface MediaPanelProps {
|
|
templates: EditingTemplate[]
|
|
loading: boolean
|
|
searchQuery: string
|
|
currentFilter: string
|
|
filterCategories: string[]
|
|
loadedTemplateId: string | null
|
|
onLoadTemplate: (id: string) => void
|
|
onSearchChange: (q: string) => void
|
|
onFilterChange: (f: string) => void
|
|
// 素材相关
|
|
mediaAssets?: MediaAsset[]
|
|
onAssetSelect?: (ids: string[]) => void
|
|
selectedAssetIds?: string[]
|
|
}
|
|
|
|
const MediaPanel: React.FC<MediaPanelProps> = ({
|
|
templates,
|
|
loading,
|
|
searchQuery,
|
|
currentFilter,
|
|
filterCategories,
|
|
loadedTemplateId,
|
|
onLoadTemplate,
|
|
onSearchChange,
|
|
onFilterChange,
|
|
mediaAssets = [],
|
|
onAssetSelect,
|
|
selectedAssetIds = [],
|
|
}) => {
|
|
const [activeTab, setActiveTab] = useState<"templates" | "assets">("templates")
|
|
|
|
return (
|
|
<div className="ep-left-panel">
|
|
{/* Tab 切换 */}
|
|
<div className="ep-left-tabs">
|
|
<button
|
|
className={`ep-left-tab ${activeTab === "templates" ? "active" : ""}`}
|
|
onClick={() => setActiveTab("templates")}
|
|
>
|
|
📋 模板
|
|
</button>
|
|
<button
|
|
className={`ep-left-tab ${activeTab === "assets" ? "active" : ""}`}
|
|
onClick={() => setActiveTab("assets")}
|
|
>
|
|
📁 素材
|
|
</button>
|
|
</div>
|
|
|
|
{/* 模板 Tab */}
|
|
{activeTab === "templates" && (
|
|
<>
|
|
{/* 搜索 */}
|
|
<div className="ep-search-wrap ep-media-panel-inner">
|
|
<span className="ep-search-icon">🔍</span>
|
|
<input
|
|
className="ep-search-input"
|
|
placeholder="搜索模板..."
|
|
value={searchQuery}
|
|
onChange={(e) => onSearchChange(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
{/* Chip 分类筛选 */}
|
|
<div className="ep-filter-chips">
|
|
{filterCategories.map((cat) => (
|
|
<button
|
|
key={cat}
|
|
className={`ep-filter-chip ${currentFilter === cat ? "active" : ""}`}
|
|
onClick={() => onFilterChange(cat)}
|
|
>
|
|
{cat}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* 模板列表 */}
|
|
<div className="ep-template-list">
|
|
{loading ? (
|
|
<div className="ep-loading">
|
|
<span>⏳</span>
|
|
<span>加载中...</span>
|
|
</div>
|
|
) : templates.length === 0 ? (
|
|
<div className="ep-empty">
|
|
<span>📭</span>
|
|
<span>暂无模板</span>
|
|
</div>
|
|
) : (
|
|
templates.map((tpl) => (
|
|
<div
|
|
key={tpl.id}
|
|
className={`ep-template-card ${loadedTemplateId === tpl.id ? "active" : ""}`}
|
|
onClick={() => onLoadTemplate(tpl.id)}
|
|
>
|
|
<div className="ep-template-card-header">
|
|
<span className="ep-template-card-name">{tpl.name}</span>
|
|
<span className="ep-template-card-mode">{MODE_LABELS[tpl.mode]}</span>
|
|
</div>
|
|
<div className="ep-template-card-meta">
|
|
<span>⏱️ {tpl.estimated_duration}s</span>
|
|
<span>📐 {tpl.segments.length}片段</span>
|
|
</div>
|
|
{tpl.tags.length > 0 && (
|
|
<div className="ep-template-card-tags">
|
|
{tpl.tags.map((tag) => (
|
|
<span key={tag} className="ep-template-tag">
|
|
{tag}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
|
|
{/* 素材 Tab */}
|
|
{activeTab === "assets" && (
|
|
<div className="ep-assets-tab">
|
|
<AssetSelector
|
|
assets={mediaAssets}
|
|
selectedIds={selectedAssetIds}
|
|
onSelectionChange={onAssetSelect}
|
|
showQualityFilter={false}
|
|
showBatchSelect={false}
|
|
compact
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default MediaPanel
|