refactor(product-library): 深化拆分,抽离筛选栏/批量操作栏/空状态 (#993)
This commit is contained in:
Executable → Regular
+44
-197
@@ -1,38 +1,29 @@
|
||||
/**
|
||||
* 成片库页面 — V21 设计系统
|
||||
* 卡片网格布局,支持视频播放/下载/分享、批量操作、筛选
|
||||
* 使用 useQuery 对接后端真实 API(api/products.ts)
|
||||
*
|
||||
* 代码结构(三阶段重构后):
|
||||
* - types.ts: 类型定义
|
||||
* - constants.ts: 常量配置
|
||||
* - utils/index.ts: 工具函数
|
||||
* - components/ProductCard.tsx: 产品卡片组件
|
||||
* - components/VideoPlayer.tsx: 视频播放器组件
|
||||
* - hooks/useProductList.ts: 列表查询与筛选
|
||||
* - hooks/useProductActions.ts: 单个/批量操作
|
||||
* 主组件仅保留 Hook 组装与整体布局
|
||||
* 列表查询 → hooks/useProductList
|
||||
* 操作逻辑 → hooks/useProductActions
|
||||
* 筛选栏 → components/ProductFilterBar
|
||||
* 批量操作栏 → components/ProductBatchBar
|
||||
* 空状态 → components/ProductEmptyState
|
||||
* 产品卡片 → components/ProductCard
|
||||
* 视频播放 → components/VideoPlayer
|
||||
*/
|
||||
import React, { useState } from "react"
|
||||
import { Popconfirm, message } from "antd"
|
||||
import {
|
||||
SearchOutlined,
|
||||
VideoCameraOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined,
|
||||
CheckOutlined,
|
||||
CloudUploadOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { Button, Input, Select } from "@/components/ui"
|
||||
import { VideoCameraOutlined, DownloadOutlined } from "@ant-design/icons"
|
||||
import { Button } from "@/components/ui"
|
||||
import type { ProductItem } from "./types"
|
||||
import { ProductCard } from "./components/ProductCard"
|
||||
import { VideoPlayer } from "./components/VideoPlayer"
|
||||
import { ProductFilterBar } from "./components/ProductFilterBar"
|
||||
import { ProductBatchBar } from "./components/ProductBatchBar"
|
||||
import { ProductEmptyState } from "./components/ProductEmptyState"
|
||||
import { useProductList } from "./hooks/useProductList"
|
||||
import { useProductActions } from "./hooks/useProductActions"
|
||||
import "./products.css"
|
||||
|
||||
/* ============================================================
|
||||
* 主组件
|
||||
* ============================================================ */
|
||||
const ProductLibrary: React.FC = () => {
|
||||
const {
|
||||
products,
|
||||
@@ -83,58 +74,20 @@ const ProductLibrary: React.FC = () => {
|
||||
setPlayingProduct,
|
||||
})
|
||||
|
||||
// ── Loading 状态 ──
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="xx-products-page">
|
||||
<div className="xx-products-empty">
|
||||
<div className="xx-products-empty-icon">⏳</div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <ProductEmptyState type="loading" />
|
||||
}
|
||||
|
||||
// ── Error 状态 ──
|
||||
if (isError) {
|
||||
console.error("[ProductLibrary] 加载失败:", error)
|
||||
const errorMsg = error?.message || "加载失败"
|
||||
// 404 视为空数据(API 尚未就绪或无数据)
|
||||
const is404 = errorMsg.includes("404") || errorMsg.includes("Not Found")
|
||||
if (is404) {
|
||||
return (
|
||||
<div className="xx-products-page">
|
||||
<div className="xx-products-header">
|
||||
<h2>
|
||||
<VideoCameraOutlined /> 成片库
|
||||
</h2>
|
||||
</div>
|
||||
<div className="xx-products-empty">
|
||||
<div className="xx-products-empty-icon">🎬</div>
|
||||
<p>暂无成片数据</p>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-tertiary)",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
完成视频生成后,成片将自动保存到这里
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <ProductEmptyState type="404" />
|
||||
}
|
||||
return (
|
||||
<div className="xx-products-page">
|
||||
<div className="xx-products-empty">
|
||||
<div className="xx-products-empty-icon">❌</div>
|
||||
<p>{errorMsg || "加载失败,请稍后重试"}</p>
|
||||
<Button buttonType="primary" buttonSize="sm" onClick={() => refetch()}>
|
||||
重新加载
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
return <ProductEmptyState type="error" errorMessage={errorMsg} onRetry={refetch} />
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -153,129 +106,35 @@ const ProductLibrary: React.FC = () => {
|
||||
|
||||
{/* 批量操作栏 */}
|
||||
{batchMode && (
|
||||
<div className="xx-products-batch-bar">
|
||||
<div className="xx-products-batch-bar-left">
|
||||
<div
|
||||
className={`xx-products-checkbox${allSelected ? " checked" : ""}`}
|
||||
onClick={handleSelectAll}
|
||||
>
|
||||
{allSelected && <CheckOutlined />}
|
||||
</div>
|
||||
<span className="xx-products-select-all" onClick={handleSelectAll}>
|
||||
{allSelected ? "取消全选" : "全选"}
|
||||
</span>
|
||||
<span className="xx-products-batch-count">已选择 {selectedIds.size} 项</span>
|
||||
</div>
|
||||
<div className="xx-products-batch-bar-right">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
buttonSize="sm"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleBatchDownload}
|
||||
disabled={batchDownloading}
|
||||
>
|
||||
{batchDownloading ? "打包中..." : "批量下载"}
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={handleBatchPublish}
|
||||
>
|
||||
批量发布
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`确定删除选中的 ${selectedIds.size} 个视频?`}
|
||||
onConfirm={handleBatchDelete}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button buttonType="danger" buttonSize="sm" icon={<DeleteOutlined />}>
|
||||
批量删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button buttonType="ghost" buttonSize="sm" onClick={clearSelection}>
|
||||
取消选择
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ProductBatchBar
|
||||
allSelected={allSelected}
|
||||
selectedCount={selectedIds.size}
|
||||
batchDownloading={batchDownloading}
|
||||
onSelectAll={handleSelectAll}
|
||||
onBatchDownload={handleBatchDownload}
|
||||
onBatchPublish={handleBatchPublish}
|
||||
onBatchDelete={handleBatchDelete}
|
||||
onClearSelection={clearSelection}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 筛选栏 */}
|
||||
<div className="xx-products-filters">
|
||||
<div className="xx-products-filters-left">
|
||||
<Input
|
||||
placeholder="搜索成片名称..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => setSearchText(e.target.value)}
|
||||
allowClear
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Select
|
||||
value={filterStatus}
|
||||
onChange={setFilterStatus}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: "all", label: "全部状态" },
|
||||
{ value: "completed", label: "已完成" },
|
||||
{ value: "processing", label: "处理中" },
|
||||
{ value: "review", label: "待复核" },
|
||||
{ value: "failed", label: "失败" },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={filterTime}
|
||||
onChange={setFilterTime}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: "all", label: "全部时间" },
|
||||
{ value: "today", label: "今天" },
|
||||
{ value: "week", label: "近一周" },
|
||||
{ value: "month", label: "近一月" },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={filterDuration}
|
||||
onChange={setFilterDuration}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: "all", label: "全部时长" },
|
||||
{ value: "short", label: "≤1分钟" },
|
||||
{ value: "medium", label: "1-3分钟" },
|
||||
{ value: "long", label: ">3分钟" },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={filterProject}
|
||||
onChange={setFilterProject}
|
||||
style={{ width: 140 }}
|
||||
options={[{ value: "all", label: "全部项目" }, ...projectOptions]}
|
||||
/>
|
||||
<Select
|
||||
value={filterReviewStatus}
|
||||
onChange={setFilterReviewStatus}
|
||||
style={{ width: 130 }}
|
||||
options={[
|
||||
{ value: "all", label: "全部复核" },
|
||||
{ value: "none", label: "未设置" },
|
||||
{ value: "pending_review", label: "待复核" },
|
||||
{ value: "approved", label: "已通过" },
|
||||
{ value: "rejected", label: "需修改" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="xx-products-filters-right">
|
||||
<span
|
||||
style={{
|
||||
fontSize: "var(--font-size-sm)",
|
||||
color: "var(--text-tertiary)",
|
||||
}}
|
||||
>
|
||||
共 {filteredProducts.length} 个成片
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ProductFilterBar
|
||||
searchText={searchText}
|
||||
onSearchChange={setSearchText}
|
||||
filterStatus={filterStatus}
|
||||
onFilterStatusChange={setFilterStatus}
|
||||
filterTime={filterTime}
|
||||
onFilterTimeChange={setFilterTime}
|
||||
filterDuration={filterDuration}
|
||||
onFilterDurationChange={setFilterDuration}
|
||||
filterProject={filterProject}
|
||||
onFilterProjectChange={setFilterProject}
|
||||
filterReviewStatus={filterReviewStatus}
|
||||
onFilterReviewStatusChange={setFilterReviewStatus}
|
||||
projectOptions={projectOptions}
|
||||
resultCount={filteredProducts.length}
|
||||
/>
|
||||
|
||||
{/* 卡片网格 */}
|
||||
{filteredProducts.length > 0 ? (
|
||||
@@ -297,19 +156,7 @@ const ProductLibrary: React.FC = () => {
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="xx-products-empty">
|
||||
<div className="xx-products-empty-icon">
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
<p>暂无成片,去智能剪辑吧</p>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => message.info("跳转到生成页面")}
|
||||
>
|
||||
去生成
|
||||
</Button>
|
||||
</div>
|
||||
<ProductEmptyState type="empty" />
|
||||
)}
|
||||
|
||||
{/* 视频播放弹窗 */}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* ProductLibrary 批量操作栏
|
||||
*/
|
||||
import React from "react"
|
||||
import {
|
||||
CheckOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined,
|
||||
CloudUploadOutlined,
|
||||
} from "@ant-design/icons"
|
||||
import { Button } from "@/components/ui"
|
||||
import { Popconfirm } from "antd"
|
||||
|
||||
export interface ProductBatchBarProps {
|
||||
allSelected: boolean
|
||||
selectedCount: number
|
||||
batchDownloading: boolean
|
||||
onSelectAll: () => void
|
||||
onBatchDownload: () => void
|
||||
onBatchPublish: () => void
|
||||
onBatchDelete: () => void
|
||||
onClearSelection: () => void
|
||||
}
|
||||
|
||||
export const ProductBatchBar: React.FC<ProductBatchBarProps> = ({
|
||||
allSelected,
|
||||
selectedCount,
|
||||
batchDownloading,
|
||||
onSelectAll,
|
||||
onBatchDownload,
|
||||
onBatchPublish,
|
||||
onBatchDelete,
|
||||
onClearSelection,
|
||||
}) => {
|
||||
return (
|
||||
<div className="xx-products-batch-bar">
|
||||
<div className="xx-products-batch-bar-left">
|
||||
<div
|
||||
className={`xx-products-checkbox${allSelected ? " checked" : ""}`}
|
||||
onClick={onSelectAll}
|
||||
>
|
||||
{allSelected && <CheckOutlined />}
|
||||
</div>
|
||||
<span className="xx-products-select-all" onClick={onSelectAll}>
|
||||
{allSelected ? "取消全选" : "全选"}
|
||||
</span>
|
||||
<span className="xx-products-batch-count">已选择 {selectedCount} 项</span>
|
||||
</div>
|
||||
<div className="xx-products-batch-bar-right">
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
buttonSize="sm"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={onBatchDownload}
|
||||
disabled={batchDownloading}
|
||||
>
|
||||
{batchDownloading ? "打包中..." : "批量下载"}
|
||||
</Button>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
icon={<CloudUploadOutlined />}
|
||||
onClick={onBatchPublish}
|
||||
>
|
||||
批量发布
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={`确定删除选中的 ${selectedCount} 个视频?`}
|
||||
onConfirm={onBatchDelete}
|
||||
okText="删除"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button buttonType="danger" buttonSize="sm" icon={<DeleteOutlined />}>
|
||||
批量删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button buttonType="ghost" buttonSize="sm" onClick={onClearSelection}>
|
||||
取消选择
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductBatchBar
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* ProductLibrary 空状态/加载/错误页面
|
||||
*/
|
||||
import React from "react"
|
||||
import { VideoCameraOutlined } from "@ant-design/icons"
|
||||
import { Button } from "@/components/ui"
|
||||
import { message } from "antd"
|
||||
|
||||
export type ProductEmptyStateType = "loading" | "empty" | "404" | "error"
|
||||
|
||||
export interface ProductEmptyStateProps {
|
||||
type: ProductEmptyStateType
|
||||
errorMessage?: string
|
||||
onRetry?: () => void
|
||||
}
|
||||
|
||||
export const ProductEmptyState: React.FC<ProductEmptyStateProps> = ({
|
||||
type,
|
||||
errorMessage,
|
||||
onRetry,
|
||||
}) => {
|
||||
if (type === "loading") {
|
||||
return (
|
||||
<div className="xx-products-page">
|
||||
<div className="xx-products-empty">
|
||||
<div className="xx-products-empty-icon">⏳</div>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === "404") {
|
||||
return (
|
||||
<div className="xx-products-page">
|
||||
<div className="xx-products-header">
|
||||
<h2>
|
||||
<VideoCameraOutlined /> 成片库
|
||||
</h2>
|
||||
</div>
|
||||
<div className="xx-products-empty">
|
||||
<div className="xx-products-empty-icon">🎬</div>
|
||||
<p>暂无成片数据</p>
|
||||
<p
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: "var(--text-tertiary)",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
完成视频生成后,成片将自动保存到这里
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === "error") {
|
||||
return (
|
||||
<div className="xx-products-page">
|
||||
<div className="xx-products-empty">
|
||||
<div className="xx-products-empty-icon">❌</div>
|
||||
<p>{errorMessage || "加载失败,请稍后重试"}</p>
|
||||
{onRetry && (
|
||||
<Button buttonType="primary" buttonSize="sm" onClick={onRetry}>
|
||||
重新加载
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// empty
|
||||
return (
|
||||
<div className="xx-products-empty">
|
||||
<div className="xx-products-empty-icon">
|
||||
<VideoCameraOutlined />
|
||||
</div>
|
||||
<p>暂无成片,去智能剪辑吧</p>
|
||||
<Button
|
||||
buttonType="primary"
|
||||
buttonSize="sm"
|
||||
onClick={() => message.info("跳转到生成页面")}
|
||||
>
|
||||
去生成
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductEmptyState
|
||||
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* ProductLibrary 筛选栏
|
||||
*/
|
||||
import React from "react"
|
||||
import { SearchOutlined } from "@ant-design/icons"
|
||||
import { Input, Select } from "@/components/ui"
|
||||
|
||||
export interface ProductFilterBarProps {
|
||||
searchText: string
|
||||
onSearchChange: (text: string) => void
|
||||
filterStatus: string
|
||||
onFilterStatusChange: (val: string) => void
|
||||
filterTime: string
|
||||
onFilterTimeChange: (val: string) => void
|
||||
filterDuration: string
|
||||
onFilterDurationChange: (val: string) => void
|
||||
filterProject: string
|
||||
onFilterProjectChange: (val: string) => void
|
||||
filterReviewStatus: string
|
||||
onFilterReviewStatusChange: (val: string) => void
|
||||
projectOptions: Array<{ value: string; label: string }>
|
||||
resultCount: number
|
||||
}
|
||||
|
||||
export const ProductFilterBar: React.FC<ProductFilterBarProps> = ({
|
||||
searchText,
|
||||
onSearchChange,
|
||||
filterStatus,
|
||||
onFilterStatusChange,
|
||||
filterTime,
|
||||
onFilterTimeChange,
|
||||
filterDuration,
|
||||
onFilterDurationChange,
|
||||
filterProject,
|
||||
onFilterProjectChange,
|
||||
filterReviewStatus,
|
||||
onFilterReviewStatusChange,
|
||||
projectOptions,
|
||||
resultCount,
|
||||
}) => {
|
||||
return (
|
||||
<div className="xx-products-filters">
|
||||
<div className="xx-products-filters-left">
|
||||
<Input
|
||||
placeholder="搜索成片名称..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchText}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
allowClear
|
||||
style={{ width: 220 }}
|
||||
/>
|
||||
<Select
|
||||
value={filterStatus}
|
||||
onChange={onFilterStatusChange}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: "all", label: "全部状态" },
|
||||
{ value: "completed", label: "已完成" },
|
||||
{ value: "processing", label: "处理中" },
|
||||
{ value: "review", label: "待复核" },
|
||||
{ value: "failed", label: "失败" },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={filterTime}
|
||||
onChange={onFilterTimeChange}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: "all", label: "全部时间" },
|
||||
{ value: "today", label: "今天" },
|
||||
{ value: "week", label: "近一周" },
|
||||
{ value: "month", label: "近一月" },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={filterDuration}
|
||||
onChange={onFilterDurationChange}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: "all", label: "全部时长" },
|
||||
{ value: "short", label: "≤1分钟" },
|
||||
{ value: "medium", label: "1-3分钟" },
|
||||
{ value: "long", label: ">3分钟" },
|
||||
]}
|
||||
/>
|
||||
<Select
|
||||
value={filterProject}
|
||||
onChange={onFilterProjectChange}
|
||||
style={{ width: 140 }}
|
||||
options={[{ value: "all", label: "全部项目" }, ...projectOptions]}
|
||||
/>
|
||||
<Select
|
||||
value={filterReviewStatus}
|
||||
onChange={onFilterReviewStatusChange}
|
||||
style={{ width: 130 }}
|
||||
options={[
|
||||
{ value: "all", label: "全部复核" },
|
||||
{ value: "none", label: "未设置" },
|
||||
{ value: "pending_review", label: "待复核" },
|
||||
{ value: "approved", label: "已通过" },
|
||||
{ value: "rejected", label: "需修改" },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="xx-products-filters-right">
|
||||
<span
|
||||
style={{
|
||||
fontSize: "var(--font-size-sm)",
|
||||
color: "var(--text-tertiary)",
|
||||
}}
|
||||
>
|
||||
共 {resultCount} 个成片
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ProductFilterBar
|
||||
Reference in New Issue
Block a user