feat: 实现需求管理、加班记录、成员/角色管理模块
- 需求模块:完整 CRUD、状态流转(采纳/拒绝/关闭)、详情抽屉、产品→项目级联选择 - 加班记录:产品→项目→版本三级联动、月份筛选(MonthPicker)、CSV 导出 - 成员管理:左右布局(部门树+成员列表)、手机号脱敏、初始密码自动生成及规则设置 - 角色管理:卡片列表、系统角色保护、CRUD - 通用组件:FilterSelect 下拉、MonthPicker 月份选择器、Pagination 分页 - 样式统一:状态标签加 border、日期输入现代化、筛选组件风格一致 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ChevronLeft, Pencil, Trash2 } from 'lucide-react';
|
||||
import { RequirementStatus } from '@ftb/shared';
|
||||
import type { RequirementStatus } from '@/lib/requirement';
|
||||
import { useProductStore } from '@/stores/useProductStore';
|
||||
import { useRequirementStore } from '@/stores/useRequirementStore';
|
||||
import { RequirementTable } from '@/components/product/RequirementTable';
|
||||
@@ -24,38 +24,37 @@ export default function ProductDetailPage() {
|
||||
const { currentProduct, fetchProduct, updateProduct, deleteProduct } = useProductStore();
|
||||
const {
|
||||
requirements,
|
||||
statusFilter,
|
||||
fetchRequirements,
|
||||
createRequirement,
|
||||
updateRequirement,
|
||||
updateStatus,
|
||||
deleteRequirement,
|
||||
setStatusFilter,
|
||||
} = useRequirementStore();
|
||||
|
||||
const [showReqForm, setShowReqForm] = useState(false);
|
||||
const [editingReq, setEditingReq] = useState<any>(null);
|
||||
const [editingProduct, setEditingProduct] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<'requirements' | 'projects' | 'versions'>('requirements');
|
||||
const [reqStatusFilter, setReqStatusFilter] = useState<RequirementStatus | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchProduct(productId);
|
||||
fetchRequirements(productId);
|
||||
fetchRequirements();
|
||||
}, [productId, fetchProduct, fetchRequirements]);
|
||||
|
||||
const productRequirements = useMemo(() => {
|
||||
let list = requirements.filter((r) => r.productId === productId);
|
||||
if (reqStatusFilter) list = list.filter((r) => r.status === reqStatusFilter);
|
||||
return list;
|
||||
}, [requirements, productId, reqStatusFilter]);
|
||||
|
||||
const handleFilterChange = (status: RequirementStatus | null) => {
|
||||
setStatusFilter(status);
|
||||
fetchRequirements(productId, status || undefined);
|
||||
setReqStatusFilter(status);
|
||||
};
|
||||
|
||||
const handleCreateReq = async (data: { title: string; description: string; priority: number }) => {
|
||||
await createRequirement(productId, { ...data, creatorId: 'temp-user-id' });
|
||||
setShowReqForm(false);
|
||||
};
|
||||
|
||||
const handleUpdateReq = async (data: { title: string; description: string; priority: number }) => {
|
||||
if (editingReq) {
|
||||
await updateRequirement(productId, editingReq.id, data);
|
||||
setEditingReq(null);
|
||||
}
|
||||
};
|
||||
@@ -81,7 +80,7 @@ export default function ProductDetailPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => router.push('/products')}
|
||||
className="flex h-7 items-center gap-1 rounded-md px-1.5 text-[12.5px] text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]"
|
||||
className="flex h-7 items-center gap-1 rounded-md px-1.5 text-[12px] text-[var(--ink-muted)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]"
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
产品
|
||||
@@ -94,14 +93,14 @@ export default function ProductDetailPage() {
|
||||
<div className="flex gap-1.5">
|
||||
<button
|
||||
onClick={() => setEditingProduct(true)}
|
||||
className="flex h-8 items-center gap-1 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-2.5 text-[12.5px] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]"
|
||||
className="flex h-8 items-center gap-1 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-2.5 text-[12px] text-[var(--ink-soft)] hover:bg-[var(--bg-subtle)] hover:text-[var(--ink)]"
|
||||
>
|
||||
<Pencil className="h-3 w-3" strokeWidth={2} />
|
||||
编辑
|
||||
</button>
|
||||
<button
|
||||
onClick={handleDeleteProduct}
|
||||
className="flex h-8 items-center gap-1 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-2.5 text-[12.5px] text-[var(--ink-soft)] hover:border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
className="flex h-8 items-center gap-1 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-2.5 text-[12px] text-[var(--ink-soft)] hover:border-red-200 hover:bg-red-50 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" strokeWidth={2} />
|
||||
删除
|
||||
@@ -137,7 +136,7 @@ export default function ProductDetailPage() {
|
||||
<div className="p-5">
|
||||
{editingProduct && (
|
||||
<div className="mb-5 rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] p-6 shadow-[var(--shadow-sm)]">
|
||||
<h3 className="mb-4 text-[14px] font-semibold text-[var(--ink)]">编辑产品</h3>
|
||||
<h3 className="mb-4 text-[13px] font-semibold text-[var(--ink)]">编辑产品</h3>
|
||||
<ProductForm
|
||||
initialData={{ name: currentProduct.name, description: currentProduct.description }}
|
||||
onSubmit={async (data) => {
|
||||
@@ -154,7 +153,7 @@ export default function ProductDetailPage() {
|
||||
<>
|
||||
{(showReqForm || editingReq) && (
|
||||
<div className="mb-5 rounded-2xl border border-[var(--line)] bg-[var(--bg-card)] p-6 shadow-[var(--shadow-sm)]">
|
||||
<h3 className="mb-4 text-[14px] font-semibold text-[var(--ink)]">
|
||||
<h3 className="mb-4 text-[13px] font-semibold text-[var(--ink)]">
|
||||
{editingReq ? '编辑需求' : '新建需求'}
|
||||
</h3>
|
||||
<RequirementForm
|
||||
@@ -168,12 +167,12 @@ export default function ProductDetailPage() {
|
||||
</div>
|
||||
)}
|
||||
<RequirementTable
|
||||
requirements={requirements as any}
|
||||
statusFilter={statusFilter}
|
||||
requirements={productRequirements as any}
|
||||
statusFilter={reqStatusFilter}
|
||||
onFilterChange={handleFilterChange}
|
||||
onEdit={(req) => setEditingReq(req)}
|
||||
onStatusChange={(id, status) => updateStatus(productId, id, status)}
|
||||
onDelete={(id) => deleteRequirement(productId, id)}
|
||||
onStatusChange={() => {}}
|
||||
onDelete={(id) => deleteRequirement(id)}
|
||||
onCreate={() => setShowReqForm(true)}
|
||||
/>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user