'use client'; import { useEffect, useMemo, useState } from 'react'; import { Search, Tag, Plus, X, ChevronDown } from 'lucide-react'; import { useProductStore } from '@/stores/useProductStore'; import { flattenVersions, flattenProjects } from '@/lib/derive'; import type { VersionWithContext } from '@/lib/derive'; import { VersionStatus, VERSION_STATUS_LABEL, VERSION_STATUS_DOT } from '@/lib/version-status'; const STATUS_TABS: { key: VersionStatus | 'all'; label: string }[] = [ { key: 'all', label: '全部' }, { key: 'developing', label: '开发中' }, { key: 'planned', label: '规划中' }, { key: 'released', label: '已发布' }, ]; function parseVersionNumber(name: string): number[] { const match = name.match(/V([\d.]+)$/i); if (!match) return [0]; return match[1].split('.').map(Number); } function compareVersionsDesc(a: string, b: string): number { const va = parseVersionNumber(a); const vb = parseVersionNumber(b); const len = Math.max(va.length, vb.length); for (let i = 0; i < len; i++) { const na = va[i] ?? 0; const nb = vb[i] ?? 0; if (nb !== na) return nb - na; } return 0; } function sortVersions(versions: VersionWithContext[]): VersionWithContext[] { return [...versions].sort((a, b) => { const projCmp = a.projectName.localeCompare(b.projectName, 'zh-CN'); if (projCmp !== 0) return projCmp; return compareVersionsDesc(a.name, b.name); }); } export default function VersionsPage() { const { overview, fetchOverview, createVersion } = useProductStore(); const [search, setSearch] = useState(''); const [statusTab, setStatusTab] = useState('all'); const [projectFilter, setProjectFilter] = useState('all'); const [projectDropdownOpen, setProjectDropdownOpen] = useState(false); const [showModal, setShowModal] = useState(false); useEffect(() => { fetchOverview(); }, [fetchOverview]); const allVersions = useMemo(() => flattenVersions(overview), [overview]); const allProjects = useMemo(() => flattenProjects(overview), [overview]); const filtered = useMemo(() => { const list = allVersions.filter((v) => { if (statusTab !== 'all' && v.status !== statusTab) return false; if (projectFilter !== 'all' && v.projectName !== projectFilter) return false; if (search && !v.name.toLowerCase().includes(search.toLowerCase())) return false; return true; }); return sortVersions(list); }, [allVersions, search, statusTab, projectFilter]); return (

版本

{allVersions.length}
{/* Filters */}
setSearch(e.target.value)} placeholder="搜索版本号" className="h-8 w-64 rounded-lg border border-[var(--line)] bg-[var(--bg)] pl-8 pr-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)]" />
{STATUS_TABS.map((tab) => ( ))}
{projectDropdownOpen && (
{allProjects.map((p) => ( ))}
)}
开发中 规划中 已发布
{/* Content */}
{filtered.length === 0 ? (

没有找到匹配的版本

尝试调整筛选条件

) : (
{filtered.map((v) => )}
版本号 状态 项目 产品 创建时间
)}
{showModal && setShowModal(false)} onCreate={createVersion} />}
); } function VersionRow({ version }: { version: VersionWithContext }) { const dotCls = VERSION_STATUS_DOT[version.status]; return (
{version.name}
{VERSION_STATUS_LABEL[version.status]} {version.projectName} {version.productName} {new Date(version.createdAt).toLocaleDateString('zh-CN')} ); } type IterationType = 'major' | 'minor' | 'patch'; interface NewVersionModalProps { overview: any[]; onClose: () => void; onCreate: (productId: string, data: { name: string; status: VersionStatus }) => void; } function getNextVersion(existingVersions: any[], projectName: string, type: IterationType): string { const projectVersions = existingVersions .filter((v: any) => v.name.startsWith(projectName + 'V')) .map((v: any) => { const match = v.name.match(/V([\d.]+)$/i); return match ? match[1].split('.').map(Number) : null; }) .filter(Boolean) as number[][]; if (projectVersions.length === 0) { if (type === 'major') return '1.0'; if (type === 'minor') return '0.1'; return '0.0.1'; } // Find the max version projectVersions.sort((a, b) => { const len = Math.max(a.length, b.length); for (let i = 0; i < len; i++) { const diff = (b[i] ?? 0) - (a[i] ?? 0); if (diff !== 0) return diff; } return 0; }); const latest = projectVersions[0]; if (type === 'major') { return `${(latest[0] ?? 0) + 1}.0`; } else if (type === 'minor') { return `${latest[0] ?? 0}.${(latest[1] ?? 0) + 1}`; } else { const major = latest[0] ?? 0; const minor = latest[1] ?? 0; const patch = latest[2] ?? 0; return `${major}.${minor}.${patch + 1}`; } } function NewVersionModal({ overview, onClose, onCreate }: NewVersionModalProps) { const [productId, setProductId] = useState(''); const [projectId, setProjectId] = useState(''); const [projectName, setProjectName] = useState(''); const [iterationType, setIterationType] = useState(''); const [versionNumber, setVersionNumber] = useState(''); const [status, setStatus] = useState('planned'); const [submitting, setSubmitting] = useState(false); const selectedProduct = overview.find((p: any) => p.id === productId); const projects = selectedProduct?.projects ?? []; const existingVersions = selectedProduct?.versions ?? []; // When product changes, reset project const handleProductChange = (pid: string) => { setProductId(pid); setProjectId(''); setProjectName(''); setIterationType(''); setVersionNumber(''); }; // When project changes const handleProjectChange = (projId: string) => { const proj = projects.find((p: any) => p.id === projId); setProjectId(projId); setProjectName(proj?.name ?? ''); setIterationType(''); setVersionNumber(''); }; // When iteration type changes, auto-generate version number const handleIterationChange = (type: IterationType) => { setIterationType(type); if (projectName) { const next = getNextVersion(existingVersions, projectName, type); setVersionNumber(next); } }; const handleVersionInput = (val: string) => { // Only allow digits and dots const cleaned = val.replace(/[^\d.]/g, ''); setVersionNumber(cleaned); }; const canSubmit = productId && projectId && versionNumber && !submitting; const handleSubmit = async () => { if (!canSubmit) return; setSubmitting(true); try { await onCreate(productId, { name: `${projectName}V${versionNumber}`, status }); onClose(); } catch (e) { setSubmitting(false); } }; return (
e.stopPropagation()}>

新建版本

{/* 产品选择 */}
{/* 项目选择 */}
{/* 迭代类型 */}
{([ { key: 'major' as const, label: '大版本', hint: 'X.0' }, { key: 'minor' as const, label: '中版本', hint: '1.X' }, { key: 'patch' as const, label: '小版本', hint: '1.2.X' }, ]).map((opt) => ( ))}
{/* 版本号 */}
handleVersionInput(e.target.value)} placeholder="1.0" className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] tabular-nums text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:border-[var(--accent)] focus:outline-none focus:ring-2 focus:ring-[var(--accent-ring)]" /> {projectName && versionNumber && (

将创建:{projectName}V{versionNumber}

)}
{/* 状态 */}
{([ { key: 'planned' as VersionStatus, label: '规划中' }, { key: 'developing' as VersionStatus, label: '开发中' }, ]).map((opt) => ( ))}
); }