feat(工作台): 细化计划进展工时证据

关键改动:

- 支持需求覆盖和调研方向开始工作记录

- 日报按计划记录开始时间和进度下次开始时间计算证据

- 更新版本编辑校验、项目展示和 workflow 说明

Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
Script Generator
2026-07-01 11:02:38 +08:00
parent e5ce2d221e
commit 8947aeaac7
18 changed files with 1125 additions and 94 deletions

View File

@@ -3,7 +3,7 @@
import { RouteGuard } from '@/components/auth/Guard';
import { useEffect, useMemo, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Search, FolderKanban, Package, ChevronDown, Plus, X, MoreHorizontal, Trash2 } from 'lucide-react';
import { Search, FolderKanban, Package, ChevronDown, Plus, X, MoreHorizontal, Trash2, Pencil } from 'lucide-react';
import { useProductStore } from '@/stores/useProductStore';
import { useRequirementStore } from '@/stores/useRequirementStore';
import { useVersionPlanStore } from '@/stores/useVersionPlanStore';
@@ -25,7 +25,7 @@ export default function ProjectsPage() {
function ProjectsPageContent() {
const router = useRouter();
const { overview, fetchOverview, createProject, deleteProject } = useProductStore();
const { overview, fetchOverview, createProject, updateProject, deleteProject } = useProductStore();
const { requirements, fetchRequirements } = useRequirementStore();
const { plans, fetchPlans } = useVersionPlanStore();
const { tasks: devTasks, fetchTasks: fetchDevTasks } = useDevTaskStore();
@@ -33,6 +33,7 @@ function ProjectsPageContent() {
const [search, setSearch] = useState('');
const [productFilter, setProductFilter] = useState<string>('all');
const [showForm, setShowForm] = useState(false);
const [editingProject, setEditingProject] = useState<ProjectWithContext | null>(null);
useEffect(() => { fetchOverview(); }, [fetchOverview]);
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
@@ -127,6 +128,7 @@ function ProjectsPageContent() {
proj={proj}
versionProgressMap={versionProgressMap}
onClick={() => router.push(`/projects/${proj.id}`)}
onEdit={() => setEditingProject(proj)}
onDelete={() => {
if (!confirm(`确认删除项目「${proj.name}」?此操作不可恢复。`)) return;
deleteProject(proj.productId, proj.id);
@@ -150,6 +152,18 @@ function ProjectsPageContent() {
}}
/>
)}
{editingProject && (
<EditProjectModal
project={editingProject}
overview={overview}
onClose={() => setEditingProject(null)}
onSubmit={async (name) => {
updateProject(editingProject.productId, editingProject.id, { name });
setEditingProject(null);
}}
/>
)}
</div>
);
}
@@ -160,13 +174,16 @@ function ProjectRow({
proj,
versionProgressMap,
onClick,
onEdit,
onDelete,
}: {
proj: ProjectWithContext;
versionProgressMap: Record<string, number>;
onClick: () => void;
onEdit: () => void;
onDelete: () => void;
}) {
const canEdit = useHasPermission('project:edit');
const canDelete = useHasPermission('project:delete');
const [menuOpen, setMenuOpen] = useState(false);
const activeVersions = proj.versions.filter((v) => (versionProgressMap[v.id] ?? 0) < 100);
@@ -200,7 +217,7 @@ function ProjectRow({
</span>
)}
</div>
{canDelete && (
{(canEdit || canDelete) && (
<div className="relative shrink-0" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => setMenuOpen(!menuOpen)}
@@ -213,19 +230,33 @@ function ProjectRow({
<>
<div className="fixed inset-0 z-30" onClick={() => setMenuOpen(false)} />
<div className="absolute right-0 top-full z-50 mt-1 min-w-[160px] rounded-lg border border-[var(--line)] bg-[var(--bg-card)] py-1 shadow-[var(--shadow-md)]">
<button
onClick={() => {
if (!canActuallyDelete) return;
setMenuOpen(false);
onDelete();
}}
disabled={!canActuallyDelete}
title={canActuallyDelete ? '' : `项目下还有 ${versionCount} 个版本,不可删除`}
className={`w-full flex items-center gap-2 px-3 py-1.5 text-left text-[12px] transition-colors ${canActuallyDelete ? 'text-red-600 hover:bg-red-50' : 'text-[var(--ink-muted)] cursor-not-allowed'}`}
>
<Trash2 size={14} />
</button>
{canEdit && (
<button
onClick={() => {
setMenuOpen(false);
onEdit();
}}
className="w-full flex items-center gap-2 px-3 py-1.5 text-left text-[12px] text-[var(--ink-soft)] transition-colors hover:bg-[var(--bg-hover)]"
>
<Pencil size={14} />
</button>
)}
{canDelete && (
<button
onClick={() => {
if (!canActuallyDelete) return;
setMenuOpen(false);
onDelete();
}}
disabled={!canActuallyDelete}
title={canActuallyDelete ? '' : `项目下还有 ${versionCount} 个版本,不可删除`}
className={`w-full flex items-center gap-2 px-3 py-1.5 text-left text-[12px] transition-colors ${canActuallyDelete ? 'text-red-600 hover:bg-red-50' : 'text-[var(--ink-muted)] cursor-not-allowed'}`}
>
<Trash2 size={14} />
</button>
)}
</div>
</>
)}
@@ -335,6 +366,94 @@ function EmptyState() {
);
}
/* ─── EditProjectModal ───────────────────────────────────────────── */
function EditProjectModal({
project,
overview,
onClose,
onSubmit,
}: {
project: ProjectWithContext;
overview: any[];
onClose: () => void;
onSubmit: (name: string) => Promise<void>;
}) {
const [name, setName] = useState(project.name);
const [submitting, setSubmitting] = useState(false);
const [duplicateError, setDuplicateError] = useState(false);
const handleSubmit = async () => {
const nextName = name.trim();
if (!nextName) return;
const product = overview.find((p: any) => p.id === project.productId);
const exists = product?.projects?.some(
(proj: any) => proj.id !== project.id && proj.name === nextName,
);
if (exists) {
setDuplicateError(true);
return;
}
setSubmitting(true);
try {
await onSubmit(nextName);
} finally {
setSubmitting(false);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="w-full max-w-md rounded-2xl bg-[var(--bg-card)] p-6 shadow-[var(--shadow-md)]">
<div className="mb-5 flex items-center justify-between">
<h2 className="text-base font-semibold text-[var(--ink)]"></h2>
<button type="button" onClick={onClose} className="text-[var(--ink-muted)] hover:text-[var(--ink)]">
<X size={18} />
</button>
</div>
<div>
<label className="mb-1.5 block text-[12px] font-medium text-[var(--ink-soft)]">
<span className="text-red-500">*</span>
</label>
<input
type="text"
value={name}
onChange={(e) => { setName(e.target.value); setDuplicateError(false); }}
className={`h-9 w-full rounded-lg border bg-[var(--bg)] px-3 text-[13px] text-[var(--ink)] placeholder:text-[var(--ink-muted)] focus:outline-none focus:ring-2 ${
duplicateError
? 'border-red-500 focus:border-red-500 focus:ring-red-100'
: 'border-[var(--line)] focus:border-[var(--accent)] focus:ring-[var(--accent-ring)]'
}`}
/>
{duplicateError && (
<p className="mt-1 text-[11px] text-red-500"></p>
)}
</div>
<div className="mt-6 flex items-center justify-end gap-2">
<button
type="button"
onClick={onClose}
className="h-8 rounded-lg border border-[var(--line)] bg-[var(--bg)] px-3 text-[13px] font-medium text-[var(--ink-soft)] hover:bg-[var(--bg-hover)]"
>
</button>
<button
type="button"
onClick={handleSubmit}
disabled={submitting || !name.trim() || name.trim() === project.name}
className="h-8 rounded-lg bg-[var(--accent)] px-3 text-[13px] font-medium text-white shadow-[var(--shadow-sm)] hover:bg-[var(--accent-hover)] disabled:cursor-not-allowed disabled:opacity-50"
>
{submitting ? '保存中…' : '保存'}
</button>
</div>
</div>
</div>
);
}
/* ─── CreateProjectModal ─────────────────────────────────────────── */
function CreateProjectModal({