373 lines
12 KiB
TypeScript
373 lines
12 KiB
TypeScript
/**
|
|
* 项目列表页面 - 采用卡片网格布局
|
|
*
|
|
* 设计理念:
|
|
* - 不使用传统列表,而是卡片网格展示
|
|
* - 每个项目卡片显示项目状态和进度
|
|
* - 支持快速操作:继续编辑、查看详情、删除
|
|
*/
|
|
import { useEffect, useState } from 'react'
|
|
import { useNavigate } from 'react-router-dom'
|
|
import { Button, Space, Tag, Card, message, Empty, Row, Col, Progress, Tooltip, Badge } from 'antd'
|
|
import {
|
|
PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,
|
|
ClockCircleOutlined, CheckCircleOutlined, LoadingOutlined,
|
|
FileTextOutlined, SettingOutlined
|
|
} from '@ant-design/icons'
|
|
import { useProjectStore } from '@/stores/projectStore'
|
|
import { SeriesProject } from '@/services/projectService'
|
|
import dayjs from 'dayjs'
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
import 'dayjs/locale/zh-cn'
|
|
|
|
dayjs.extend(relativeTime)
|
|
dayjs.locale('zh-cn')
|
|
|
|
type ProjectStatus = 'draft' | 'in_progress' | 'completed'
|
|
|
|
// 项目状态配置
|
|
const STATUS_CONFIG: Record<ProjectStatus, {
|
|
text: string
|
|
color: string
|
|
icon: React.ReactNode
|
|
}> = {
|
|
draft: {
|
|
text: '草稿',
|
|
color: 'default',
|
|
icon: <FileTextOutlined />
|
|
},
|
|
in_progress: {
|
|
text: '创作中',
|
|
color: 'processing',
|
|
icon: <LoadingOutlined />
|
|
},
|
|
completed: {
|
|
text: '已完成',
|
|
color: 'success',
|
|
icon: <CheckCircleOutlined />
|
|
}
|
|
}
|
|
|
|
// 计算项目完成度
|
|
const calculateCompletion = (project: SeriesProject): number => {
|
|
let completed = 0
|
|
let total = 3
|
|
|
|
// 检查世界观设定
|
|
if (project.globalContext?.worldSetting) completed += 1
|
|
|
|
// 检查人物设定
|
|
if (project.globalContext?.characterProfiles &&
|
|
Object.keys(project.globalContext.characterProfiles).length > 0) {
|
|
completed += 1
|
|
}
|
|
|
|
// 检查大纲
|
|
if (project.globalContext?.overallOutline) completed += 1
|
|
|
|
// 检查剧集完成度
|
|
if (project.totalEpisodes && project.episodes) {
|
|
total = 3 + project.totalEpisodes
|
|
completed += project.episodes.length
|
|
}
|
|
|
|
return Math.min(100, Math.round((completed / total) * 100))
|
|
}
|
|
|
|
// 获取项目状态
|
|
const getProjectStatus = (project: SeriesProject): ProjectStatus => {
|
|
const completion = calculateCompletion(project)
|
|
|
|
if (completion === 0) return 'draft'
|
|
if (completion >= 100) return 'completed'
|
|
return 'in_progress'
|
|
}
|
|
|
|
// 项目卡片组件
|
|
const ProjectCard = ({ project, onEdit, onDelete, onView }: {
|
|
project: SeriesProject
|
|
onEdit: (id: string) => void
|
|
onDelete: (id: string) => void
|
|
onView: (id: string) => void
|
|
}) => {
|
|
const status = getProjectStatus(project)
|
|
const statusConfig = STATUS_CONFIG[status]
|
|
const completion = calculateCompletion(project)
|
|
|
|
return (
|
|
<Badge.Ribbon
|
|
text={statusConfig.text}
|
|
color={statusConfig.color}
|
|
>
|
|
<Card
|
|
hoverable
|
|
style={{ height: '100%' }}
|
|
bodyStyle={{ display: 'flex', flexDirection: 'column', height: '100%' }}
|
|
>
|
|
{/* 项目标题 */}
|
|
<div style={{ marginBottom: '12px' }}>
|
|
<h3 style={{ margin: 0, fontSize: '16px', fontWeight: 600 }}>
|
|
{project.name}
|
|
</h3>
|
|
<div style={{ display: 'flex', alignItems: 'center', marginTop: '4px', gap: '4px' }}>
|
|
<Tag color="blue" icon={<FileTextOutlined />}>
|
|
{project.totalEpisodes || 0} 集
|
|
</Tag>
|
|
<Tag color={statusConfig.color} icon={statusConfig.icon}>
|
|
{statusConfig.text}
|
|
</Tag>
|
|
</div>
|
|
</div>
|
|
|
|
{/* 项目描述/内容预览 */}
|
|
<div style={{ flex: 1, marginBottom: '16px' }}>
|
|
{project.globalContext?.overallOutline ? (
|
|
<div
|
|
style={{
|
|
fontSize: '12px',
|
|
color: '#666',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 3,
|
|
WebkitBoxOrient: 'vertical'
|
|
}}
|
|
>
|
|
{project.globalContext.overallOutline}
|
|
</div>
|
|
) : project.globalContext?.worldSetting ? (
|
|
<div
|
|
style={{
|
|
fontSize: '12px',
|
|
color: '#666',
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
display: '-webkit-box',
|
|
WebkitLineClamp: 3,
|
|
WebkitBoxOrient: 'vertical'
|
|
}}
|
|
>
|
|
{project.globalContext.worldSetting}
|
|
</div>
|
|
) : (
|
|
<div style={{ fontSize: '12px', color: '#999' }}>
|
|
暂无内容描述
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* 进度条 */}
|
|
<div style={{ marginBottom: '16px' }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '4px' }}>
|
|
<span style={{ fontSize: '12px', color: '#666' }}>完成度</span>
|
|
<span style={{ fontSize: '12px', color: '#1677ff', fontWeight: 600 }}>
|
|
{completion}%
|
|
</span>
|
|
</div>
|
|
<Progress
|
|
percent={completion}
|
|
size="small"
|
|
status={completion === 100 ? 'success' : 'active'}
|
|
/>
|
|
</div>
|
|
|
|
{/* 时间信息 */}
|
|
<div style={{ fontSize: '12px', color: '#999', marginBottom: '16px' }}>
|
|
<ClockCircleOutlined style={{ marginRight: '4px' }} />
|
|
{dayjs(project.createdAt).fromNow()}
|
|
</div>
|
|
|
|
{/* 操作按钮 */}
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
{status === 'draft' || status === 'in_progress' ? (
|
|
<Button
|
|
type="primary"
|
|
size="small"
|
|
icon={<EditOutlined />}
|
|
onClick={() => onEdit(project.id)}
|
|
style={{ flex: 1 }}
|
|
>
|
|
继续编辑
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
type="default"
|
|
size="small"
|
|
icon={<EyeOutlined />}
|
|
onClick={() => onView(project.id)}
|
|
style={{ flex: 1 }}
|
|
>
|
|
查看详情
|
|
</Button>
|
|
)}
|
|
<Tooltip title="删除">
|
|
<Button
|
|
danger
|
|
size="small"
|
|
icon={<DeleteOutlined />}
|
|
onClick={() => onDelete(project.id)}
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
</Card>
|
|
</Badge.Ribbon>
|
|
)
|
|
}
|
|
|
|
export const ProjectList = () => {
|
|
const navigate = useNavigate()
|
|
const { projects, loading, fetchProjects, deleteProject } = useProjectStore()
|
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
|
|
|
useEffect(() => {
|
|
fetchProjects()
|
|
}, [])
|
|
|
|
const handleDelete = async (id: string) => {
|
|
try {
|
|
await deleteProject(id)
|
|
message.success('项目已删除')
|
|
} catch (error) {
|
|
message.error(`删除失败: ${(error as Error).message}`)
|
|
}
|
|
}
|
|
|
|
const handleContinueEdit = (id: string) => {
|
|
// 跳转到项目详情页继续编辑
|
|
navigate(`/projects/${id}`)
|
|
}
|
|
|
|
const handleView = (id: string) => {
|
|
navigate(`/projects/${id}`)
|
|
}
|
|
|
|
// 按状态分组项目
|
|
const draftProjects = projects.filter(p => getProjectStatus(p) === 'draft')
|
|
const inProgressProjects = projects.filter(p => getProjectStatus(p) === 'in_progress')
|
|
const completedProjects = projects.filter(p => getProjectStatus(p) === 'completed')
|
|
|
|
return (
|
|
<div style={{ padding: '24px' }}>
|
|
{/* 头部 */}
|
|
<div style={{ marginBottom: '24px', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
<div>
|
|
<h1 style={{ margin: 0, fontSize: '24px', fontWeight: 600 }}>我的项目</h1>
|
|
<p style={{ margin: '4px 0 0 0', color: '#666' }}>
|
|
共 {projects.length} 个项目
|
|
{inProgressProjects.length > 0 && ` · ${inProgressProjects.length} 个创作中`}
|
|
{completedProjects.length > 0 && ` · ${completedProjects.length} 个已完成`}
|
|
</p>
|
|
</div>
|
|
<Space>
|
|
<Button
|
|
icon={<SettingOutlined />}
|
|
onClick={() => setViewMode(viewMode === 'grid' ? 'list' : 'grid')}
|
|
>
|
|
{viewMode === 'grid' ? '列表视图' : '网格视图'}
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
size="large"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => navigate('/projects/progressive')}
|
|
>
|
|
创建新项目
|
|
</Button>
|
|
</Space>
|
|
</div>
|
|
|
|
{/* 项目列表 */}
|
|
{loading ? (
|
|
<div style={{ textAlign: 'center', padding: '100px 0' }}>
|
|
<LoadingOutlined style={{ fontSize: '48px', color: '#1677ff' }} />
|
|
<p style={{ marginTop: '16px', color: '#666' }}>加载中...</p>
|
|
</div>
|
|
) : projects.length === 0 ? (
|
|
<Empty
|
|
style={{ padding: '100px 0' }}
|
|
description={
|
|
<div>
|
|
<p style={{ fontSize: '16px', marginBottom: '8px' }}>暂无项目</p>
|
|
<p style={{ color: '#999' }}>创建您的第一个项目开始创作</p>
|
|
</div>
|
|
}
|
|
>
|
|
<Button
|
|
type="primary"
|
|
size="large"
|
|
icon={<PlusOutlined />}
|
|
onClick={() => navigate('/projects/progressive')}
|
|
>
|
|
创建项目
|
|
</Button>
|
|
</Empty>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '32px' }}>
|
|
{/* 创作中的项目 */}
|
|
{inProgressProjects.length > 0 && (
|
|
<div>
|
|
<h2 style={{ marginBottom: '16px', fontSize: '18px', fontWeight: 600 }}>
|
|
创作中 ({inProgressProjects.length})
|
|
</h2>
|
|
<Row gutter={[16, 16]}>
|
|
{inProgressProjects.map(project => (
|
|
<Col key={project.id} xs={24} sm={12} md={8} lg={6}>
|
|
<ProjectCard
|
|
project={project}
|
|
onEdit={handleContinueEdit}
|
|
onView={handleView}
|
|
onDelete={handleDelete}
|
|
/>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</div>
|
|
)}
|
|
|
|
{/* 草稿 */}
|
|
{draftProjects.length > 0 && (
|
|
<div>
|
|
<h2 style={{ marginBottom: '16px', fontSize: '18px', fontWeight: 600 }}>
|
|
草稿 ({draftProjects.length})
|
|
</h2>
|
|
<Row gutter={[16, 16]}>
|
|
{draftProjects.map(project => (
|
|
<Col key={project.id} xs={24} sm={12} md={8} lg={6}>
|
|
<ProjectCard
|
|
project={project}
|
|
onEdit={handleContinueEdit}
|
|
onView={handleView}
|
|
onDelete={handleDelete}
|
|
/>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</div>
|
|
)}
|
|
|
|
{/* 已完成的项目 */}
|
|
{completedProjects.length > 0 && (
|
|
<div>
|
|
<h2 style={{ marginBottom: '16px', fontSize: '18px', fontWeight: 600 }}>
|
|
已完成 ({completedProjects.length})
|
|
</h2>
|
|
<Row gutter={[16, 16]}>
|
|
{completedProjects.map(project => (
|
|
<Col key={project.id} xs={24} sm={12} md={8} lg={6}>
|
|
<ProjectCard
|
|
project={project}
|
|
onEdit={handleContinueEdit}
|
|
onView={handleView}
|
|
onDelete={handleDelete}
|
|
/>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|