'use client' import { logError as _ulogError } from '@/lib/logging/core' import { useState, useEffect, useCallback } from 'react' import { useSession } from 'next-auth/react' import { useTranslations } from 'next-intl' import Navbar from '@/components/Navbar' import ConfirmDialog from '@/components/ConfirmDialog' import TaskStatusInline from '@/components/task/TaskStatusInline' import { resolveTaskPresentationState } from '@/lib/task/presentation' import { AppIcon, IconGradientDefs } from '@/components/ui/icons' import { shouldGuideToModelSetup } from '@/lib/workspace/model-setup' import { Link, useRouter } from '@/i18n/navigation' import { apiFetch } from '@/lib/api-fetch' interface ProjectStats { episodes: number images: number videos: number panels: number firstEpisodePreview: string | null } interface Project { id: string name: string description: string | null createdAt: string updatedAt: string totalCost?: number // 项目总费用(CNY) stats?: ProjectStats } interface Pagination { page: number pageSize: number total: number totalPages: number } const PAGE_SIZE = 7 // 加上新建项目按钮正好8个,4列布局下2行 const DEFAULT_BILLING_CURRENCY = 'CNY' function formatProjectCost(amount: number, currency = DEFAULT_BILLING_CURRENCY): string { if (currency === 'USD') return `$${amount.toFixed(2)}` return `¥${amount.toFixed(2)}` } export default function WorkspacePage() { const { data: session, status } = useSession() const router = useRouter() const [projects, setProjects] = useState([]) const [loading, setLoading] = useState(true) const [showCreateModal, setShowCreateModal] = useState(false) const [createLoading, setCreateLoading] = useState(false) const [formData, setFormData] = useState({ name: '', description: '' }) const [editingProject, setEditingProject] = useState(null) const [showEditModal, setShowEditModal] = useState(false) const [editFormData, setEditFormData] = useState({ name: '', description: '' }) const [deletingProjectId, setDeletingProjectId] = useState(null) const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) const [projectToDelete, setProjectToDelete] = useState(null) // 分页和搜索状态 const [pagination, setPagination] = useState({ page: 1, pageSize: PAGE_SIZE, total: 0, totalPages: 0 }) const [searchQuery, setSearchQuery] = useState('') const [searchInput, setSearchInput] = useState('') const [modelNotConfigured, setModelNotConfigured] = useState(false) const t = useTranslations('workspace') const tc = useTranslations('common') // 检查用户是否已登录 useEffect(() => { if (status === 'loading') return if (!session) { router.push({ pathname: '/auth/signin' }) return } }, [session, status, router]) // 获取项目列表 const fetchProjects = useCallback(async (page: number = 1, search: string = '') => { try { setLoading(true) const params = new URLSearchParams({ page: page.toString(), pageSize: PAGE_SIZE.toString() }) if (search.trim()) { params.set('search', search.trim()) } const response = await apiFetch(`/api/projects?${params}`) if (response.ok) { const data = await response.json() setProjects(data.projects) setPagination(data.pagination) } } catch (error) { _ulogError('获取项目失败:', error) } finally { setLoading(false) } }, []) // 初始加载和搜索/分页变化时重新获取 useEffect(() => { if (session) { fetchProjects(pagination.page, searchQuery) } }, [session, pagination.page, searchQuery, fetchProjects]) // 搜索处理 const handleSearch = () => { setSearchQuery(searchInput) setPagination(prev => ({ ...prev, page: 1 })) } // 打开新建项目弹窗并检测模型配置 const openCreateModal = useCallback(() => { setShowCreateModal(true) // 异步检测模型配置状态 void (async () => { try { const res = await apiFetch('/api/user-preference') if (res.ok) { const payload: unknown = await res.json() setModelNotConfigured(shouldGuideToModelSetup(payload)) } } catch { // 忽略检测失败 } })() }, []) // 分页处理 const handlePageChange = (newPage: number) => { setPagination(prev => ({ ...prev, page: newPage })) } const handleCreateProject = async (e: React.FormEvent) => { e.preventDefault() if (!formData.name.trim()) return setCreateLoading(true) try { const response = await apiFetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) }) if (response.ok) { let shouldOpenModelSetup = true const preferenceResponse = await apiFetch('/api/user-preference') if (preferenceResponse.ok) { const preferencePayload: unknown = await preferenceResponse.json() shouldOpenModelSetup = shouldGuideToModelSetup(preferencePayload) } else { _ulogError('获取用户偏好失败:', { status: preferenceResponse.status }) } // 创建成功后刷新第一页 setSearchQuery('') setSearchInput('') setPagination(prev => ({ ...prev, page: 1 })) void fetchProjects(1, '') setShowCreateModal(false) setFormData({ name: '', description: '' }) if (shouldOpenModelSetup) { alert(t('analysisModelRequiredAfterCreate')) router.push({ pathname: '/profile' }) } } else { alert(t('createFailed')) } } catch (error) { _ulogError('创建项目失败:', error) alert(t('createFailed')) } finally { setCreateLoading(false) } } const formatDate = (dateString: string) => { const date = new Date(dateString) // 转换为北京时间 (UTC+8) const beijingTime = new Date(date.getTime() + 8 * 60 * 60 * 1000) return beijingTime.toLocaleDateString('zh-CN', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Shanghai' }) } const handleEditProject = async (e: React.FormEvent) => { e.preventDefault() if (!editingProject || !editFormData.name.trim()) return setCreateLoading(true) try { const response = await apiFetch(`/api/projects/${editingProject.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(editFormData) }) if (response.ok) { const data = await response.json() setProjects(projects.map(p => p.id === editingProject.id ? data.project : p)) setShowEditModal(false) setEditingProject(null) setEditFormData({ name: '', description: '' }) } else { alert(t('updateFailed')) } } catch { alert(t('updateFailed')) } finally { setCreateLoading(false) } } const handleDeleteProject = async () => { if (!projectToDelete) return setDeletingProjectId(projectToDelete.id) setShowDeleteConfirm(false) try { const response = await apiFetch(`/api/projects/${projectToDelete.id}`, { method: 'DELETE' }) if (response.ok) { // 删除成功后重新获取当前页 fetchProjects(pagination.page, searchQuery) } else { alert(t('deleteFailed')) } } catch { alert(t('deleteFailed')) } finally { setDeletingProjectId(null) setProjectToDelete(null) } } const openDeleteConfirm = (project: Project, e: React.MouseEvent) => { e.preventDefault() // 阻止 Link 导航 e.stopPropagation() setProjectToDelete(project) setShowDeleteConfirm(true) } const cancelDelete = () => { setShowDeleteConfirm(false) setProjectToDelete(null) } const openEditModal = (project: Project, e: React.MouseEvent) => { e.preventDefault() // 阻止 Link 导航 e.stopPropagation() setEditingProject(project) setEditFormData({ name: project.name, description: project.description || '' }) setShowEditModal(true) } if (status === 'loading' || !session) { return (
{tc('loading')}
) } return (
{/* Header - 统一导航栏 */} {/* Main Content */}

{t('title')}

{t('subtitle')}

{/* 搜索框 */}
setSearchInput(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleSearch()} placeholder={t('searchPlaceholder')} className="glass-input-base w-64 px-3 py-2" /> {searchQuery && ( )}
{/* Projects Grid */}
{/* New Project Card */}
openCreateModal()} className="glass-surface p-6 cursor-pointer group flex items-center justify-center bg-gradient-to-br from-blue-500/5 via-cyan-500/5 to-blue-600/5 hover:from-blue-500/10 hover:via-cyan-500/10 hover:to-blue-600/10 transition-all duration-300" >
{t('newProject')}
{/* Project Cards */} {loading ? ( // Loading skeleton Array.from({ length: 3 }).map((_, index) => (
)) ) : ( projects.map((project) => ( {/* 悬停光效 */}
{/* 操作按钮 */}
{/* 标题 */}

{project.name}

{/* 描述:优先用户描述,fallback 到第一集故事 */} {(project.description || project.stats?.firstEpisodePreview) && (

{project.description || project.stats?.firstEpisodePreview}

)} {/* 统计信息 - 整行统一渐变 */} {project.stats && (project.stats.episodes > 0 || project.stats.images > 0 || project.stats.videos > 0) ? (
{/* 共享渐变定义 */}
) : (
{t('noContent')}
)} {/* 底部信息 */}
{formatDate(project.updatedAt)}
{project.totalCost !== undefined && project.totalCost > 0 && ( {formatProjectCost(project.totalCost)} )}
)) )}
{/* Empty State */} {!loading && projects.length === 0 && (

{searchQuery ? t('noResults') : t('noProjects')}

{searchQuery ? t('noResultsDesc') : t('noProjectsDesc')}

{!searchQuery && ( )}
)} {/* 分页控件 */} {!loading && pagination.totalPages > 1 && (
{/* 页码按钮 */} {Array.from({ length: pagination.totalPages }, (_, i) => i + 1) .filter(page => { // 显示第一页、最后一页、当前页及其前后两页 return page === 1 || page === pagination.totalPages || Math.abs(page - pagination.page) <= 2 }) .map((page, index, array) => ( {/* 显示省略号 */} {index > 0 && array[index - 1] !== page - 1 && ( ... )} ))} {t('totalProjects', { count: pagination.total })}
)}
{/* Create Project Modal - 简化版,只有名称和描述 */} {showCreateModal && (

{t('createProject')}

{modelNotConfigured && (
{t('modelNotConfigured.before')} setShowCreateModal(false)} > {t('modelNotConfigured.link')} {t('modelNotConfigured.after')}
)}
setFormData({ ...formData, name: e.target.value })} className="glass-input-base w-full px-3 py-2" placeholder={t('projectNamePlaceholder')} maxLength={100} required autoFocus />