4578b65965
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m30s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 2m37s
CI/CD Pipeline / Frontend Lint (push) Successful in 3m3s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 3m18s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m18s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 3m17s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m33s
CI/CD Pipeline / Integration Tests (push) Successful in 2m22s
CI/CD Pipeline / Build Staging API Image (push) Successful in 6m19s
CI/CD Pipeline / Unit Tests (push) Failing after 6m57s
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m31s
CI/CD Pipeline / ACR Image Cleanup (push) Failing after 16s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m46s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m59s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
160 lines
5.2 KiB
TypeScript
160 lines
5.2 KiB
TypeScript
/**
|
|
* 素材选择器 — V21 设计系统
|
|
* 支持批量选择、拖拽排序、预览缩略图、质量分筛选、视图切换
|
|
*
|
|
* 功能:
|
|
* - 批量选择:checkbox 多选 + 全选/反选 + Shift 连选
|
|
* - 拖拽排序:HTML5 DnD,已选素材可拖拽调整顺序
|
|
* - 预览缩略图:悬浮放大预览(视频显示时长角标)
|
|
* - 筛选增强:类型筛选 + 质量分筛选
|
|
* - 视图切换:网格视图 / 列表视图
|
|
*/
|
|
import React, { useCallback } from "react"
|
|
import "./AssetSelector.css"
|
|
import type { AssetSelectorProps } from "./types"
|
|
import useAssetFilter from "./hooks/useAssetFilter"
|
|
import useAssetSelection from "./hooks/useAssetSelection"
|
|
import useDragReorder from "./hooks/useDragReorder"
|
|
import useAssetPreview from "./hooks/useAssetPreview"
|
|
import SelectorToolbar from "./SelectorToolbar"
|
|
import SelectorBatchBar from "./SelectorBatchBar"
|
|
import AssetCard from "./AssetCard"
|
|
import AssetListItem from "./AssetListItem"
|
|
import PreviewOverlay from "./PreviewOverlay"
|
|
import EmptyState from "./EmptyState"
|
|
import type { MediaAsset } from "@/api/template-editor"
|
|
|
|
const AssetSelector: React.FC<AssetSelectorProps> = ({
|
|
assets,
|
|
selectedIds = [],
|
|
onSelectionChange,
|
|
onAssetDragStart,
|
|
onReorder,
|
|
showQualityFilter = true,
|
|
showBatchSelect = true,
|
|
compact = false,
|
|
}) => {
|
|
// 搜索 & 筛选
|
|
const {
|
|
searchText,
|
|
filterType,
|
|
filterQuality,
|
|
viewMode,
|
|
filteredAssets,
|
|
setSearchText,
|
|
setFilterType,
|
|
setFilterQuality,
|
|
setViewMode,
|
|
} = useAssetFilter(assets)
|
|
|
|
// 选中操作
|
|
const { selectedSet, toggleSelect, clearSelection } = useAssetSelection({
|
|
filteredAssets,
|
|
selectedIds,
|
|
onSelectionChange,
|
|
})
|
|
|
|
// 拖拽排序
|
|
const { dragIdx, dragOverIdx, handleDragStart, handleDragOver, handleDrop, handleDragEnd } =
|
|
useDragReorder({
|
|
filteredAssets,
|
|
selectedIds,
|
|
onAssetDragStart,
|
|
onReorder,
|
|
})
|
|
|
|
// 悬浮预览
|
|
const { previewAsset, previewPos, handleMouseEnter, handleMouseLeave } = useAssetPreview()
|
|
|
|
/* ── 点击卡片 ── */
|
|
const handleCardClick = useCallback(
|
|
(asset: MediaAsset, idx: number, e: React.MouseEvent) => {
|
|
const target = e.target as HTMLElement
|
|
if (target.closest("[data-checkbox]")) return
|
|
toggleSelect(asset, idx, e.shiftKey)
|
|
},
|
|
[toggleSelect],
|
|
)
|
|
|
|
const hasSelection = selectedIds.length > 0
|
|
|
|
return (
|
|
<div className="as-container">
|
|
{/* 工具栏 */}
|
|
<SelectorToolbar
|
|
searchText={searchText}
|
|
filterType={filterType}
|
|
filterQuality={filterQuality}
|
|
viewMode={viewMode}
|
|
showQualityFilter={showQualityFilter}
|
|
onSearchChange={setSearchText}
|
|
onTypeChange={setFilterType}
|
|
onQualityChange={setFilterQuality}
|
|
onViewModeChange={setViewMode}
|
|
/>
|
|
|
|
{/* 批量操作栏 */}
|
|
{showBatchSelect && hasSelection && (
|
|
<SelectorBatchBar selectedCount={selectedIds.length} onClear={clearSelection} />
|
|
)}
|
|
|
|
{/* 素材列表 */}
|
|
<div className="as-body">
|
|
{filteredAssets.length === 0 ? (
|
|
<EmptyState />
|
|
) : viewMode === "grid" ? (
|
|
/* 网格视图 */
|
|
<div className={`as-grid${compact ? " compact" : ""}`}>
|
|
{filteredAssets.map((asset, idx) => (
|
|
<AssetCard
|
|
key={asset.id}
|
|
asset={asset}
|
|
isSelected={selectedSet.has(asset.id)}
|
|
isDragging={dragIdx === idx}
|
|
isDragOver={dragOverIdx === idx}
|
|
showCheckbox={showBatchSelect}
|
|
compact={compact}
|
|
onToggleSelect={(a, shiftKey) => toggleSelect(a, idx, shiftKey)}
|
|
onCardClick={(a, e) => handleCardClick(a, idx, e)}
|
|
onDragStart={(e) => handleDragStart(e, idx)}
|
|
onDragOver={(e) => handleDragOver(e, idx)}
|
|
onDrop={(e) => handleDrop(e, idx)}
|
|
onDragEnd={handleDragEnd}
|
|
onMouseEnter={(e) => handleMouseEnter(asset, e)}
|
|
onMouseLeave={handleMouseLeave}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : (
|
|
/* 列表视图 */
|
|
<div className="as-list">
|
|
{filteredAssets.map((asset, idx) => (
|
|
<AssetListItem
|
|
key={asset.id}
|
|
asset={asset}
|
|
isSelected={selectedSet.has(asset.id)}
|
|
isDragging={dragIdx === idx}
|
|
isDragOver={dragOverIdx === idx}
|
|
showCheckbox={showBatchSelect}
|
|
onToggleSelect={(a, shiftKey) => toggleSelect(a, idx, shiftKey)}
|
|
onCardClick={(a, e) => handleCardClick(a, idx, e)}
|
|
onDragStart={(e) => handleDragStart(e, idx)}
|
|
onDragOver={(e) => handleDragOver(e, idx)}
|
|
onDrop={(e) => handleDrop(e, idx)}
|
|
onDragEnd={handleDragEnd}
|
|
onMouseEnter={(e) => handleMouseEnter(asset, e)}
|
|
onMouseLeave={handleMouseLeave}
|
|
/>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 悬浮预览 */}
|
|
{previewAsset && <PreviewOverlay asset={previewAsset} position={previewPos} />}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default AssetSelector
|