refactor(data): 收口关系表运行时数据源
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
Some checks failed
Deploy Production / Build, push, deploy, verify (push) Has been cancelled
- 移除已迁移业务 AppData 运行时 fallback,改走领域 API 和关系表快读 - 补齐需求产品负责人、版本计划任务 JSON 和成员 username 回填迁移 - 统一治理字典入口,并补充 AI provider、数据源契约和领域服务测试 Co-Authored-By: Codex GPT-5 <codex@openai.com>
This commit is contained in:
@@ -25,13 +25,30 @@ const KIND_LABEL: Record<GovernanceKind, string> = {
|
||||
requirement_source: '需求来源',
|
||||
};
|
||||
|
||||
const TASK_CATEGORY_GROUPS = [
|
||||
{ value: 'development', label: '开发' },
|
||||
{ value: 'testing', label: '测试' },
|
||||
{ value: 'implementation', label: '实施' },
|
||||
{ value: 'other', label: '其他' },
|
||||
];
|
||||
|
||||
const SOURCE_TYPE_GROUPS = [
|
||||
{ value: 'customer', label: '客户' },
|
||||
{ value: 'internal', label: '内部' },
|
||||
{ value: 'operation', label: '运营' },
|
||||
{ value: 'aftersale', label: '售后' },
|
||||
{ value: 'market', label: '市场' },
|
||||
{ value: 'competitor', label: '竞品' },
|
||||
{ value: 'management', label: '管理层' },
|
||||
];
|
||||
|
||||
function GovernancePageInner() {
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const role = useMemberStore((s) => s.roles.find((item) => item.id === user?.roleId));
|
||||
const [kind, setKind] = useState<GovernanceKind>('task_category');
|
||||
const [items, setItems] = useState<GovernanceItem[]>([]);
|
||||
const [name, setName] = useState('');
|
||||
const [group, setGroup] = useState('other');
|
||||
const [group, setGroup] = useState(defaultGroupForKind('task_category'));
|
||||
const [exportText, setExportText] = useState('');
|
||||
const actorId = user?.id ?? '';
|
||||
const permissions = role?.permissions ?? [];
|
||||
@@ -45,9 +62,13 @@ function GovernancePageInner() {
|
||||
void reload().catch(() => setItems([]));
|
||||
}, [kind]);
|
||||
|
||||
useEffect(() => {
|
||||
setGroup(defaultGroupForKind(kind));
|
||||
}, [kind]);
|
||||
|
||||
const create = async () => {
|
||||
if (!actorId || !name.trim()) return;
|
||||
await api.post('/governance/dictionaries', { actorId, permissions, kind, name: name.trim(), group });
|
||||
await api.post('/governance/dictionaries', { actorId, permissions, kind, name: name.trim(), group: groupForPayload(kind, group) });
|
||||
setName('');
|
||||
await reload();
|
||||
};
|
||||
@@ -99,9 +120,15 @@ function GovernancePageInner() {
|
||||
</div>
|
||||
|
||||
<section className="rounded-lg border border-[var(--line)] bg-[var(--bg-card)]">
|
||||
<div className="grid grid-cols-[1fr_140px_auto] gap-2 border-b border-[var(--line)] p-3">
|
||||
<div className={`grid gap-2 border-b border-[var(--line)] p-3 ${groupOptionsForKind(kind) ? 'grid-cols-[1fr_140px_auto]' : 'grid-cols-[1fr_auto]'}`}>
|
||||
<input value={name} onChange={(event) => setName(event.target.value)} placeholder={`新增${KIND_LABEL[kind]}`} className="h-8 rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
<input value={group} onChange={(event) => setGroup(event.target.value)} placeholder="分组" className="h-8 rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none" />
|
||||
{groupOptionsForKind(kind) && (
|
||||
<select value={group} onChange={(event) => setGroup(event.target.value)} className="h-8 rounded-md border border-[var(--line)] bg-[var(--bg)] px-3 text-[12px] focus:border-[var(--accent)] focus:outline-none">
|
||||
{groupOptionsForKind(kind)?.map((option) => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button onClick={create} disabled={!name.trim()} className="inline-flex h-8 items-center gap-1 rounded-md bg-[var(--accent)] px-3 text-[12px] font-medium text-white disabled:opacity-50">
|
||||
<Plus className="h-3.5 w-3.5" /> 添加
|
||||
</button>
|
||||
@@ -143,6 +170,22 @@ function GovernancePageInner() {
|
||||
);
|
||||
}
|
||||
|
||||
function groupOptionsForKind(kind: GovernanceKind) {
|
||||
if (kind === 'task_category') return TASK_CATEGORY_GROUPS;
|
||||
if (kind === 'requirement_source') return SOURCE_TYPE_GROUPS;
|
||||
return null;
|
||||
}
|
||||
|
||||
function defaultGroupForKind(kind: GovernanceKind) {
|
||||
if (kind === 'task_category') return 'development';
|
||||
if (kind === 'requirement_source') return 'customer';
|
||||
return '';
|
||||
}
|
||||
|
||||
function groupForPayload(kind: GovernanceKind, group: string) {
|
||||
return groupOptionsForKind(kind) ? group : undefined;
|
||||
}
|
||||
|
||||
export default function GovernancePage() {
|
||||
return (
|
||||
<RouteGuard permission="governance:manage">
|
||||
|
||||
@@ -47,7 +47,6 @@ function OvertimePageContent() {
|
||||
const [showReasonDrawer, setShowReasonDrawer] = useState(false);
|
||||
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
||||
useEffect(() => { fetchRecords(); }, [fetchRecords]);
|
||||
useEffect(() => { fetchMembers(); }, [fetchMembers]);
|
||||
|
||||
@@ -327,6 +326,7 @@ function OvertimePageContent() {
|
||||
versions={allVersions}
|
||||
reasons={reasons}
|
||||
requirements={requirements.map((r) => ({ id: r.id, title: r.title, versionId: r.versionId }))}
|
||||
onVersionChange={(version) => fetchRequirements({ productId: version.productId, versionId: version.id })}
|
||||
onClose={() => setShowModal(false)}
|
||||
onSubmit={(data) => {
|
||||
createRecord(data as any);
|
||||
@@ -408,13 +408,14 @@ function DepartmentButton({ active, label, count, hours, depth = 0, icon, onClic
|
||||
);
|
||||
}
|
||||
|
||||
function OvertimeModal({ defaultPerson, products, projects, versions, reasons, requirements, onClose, onSubmit }: {
|
||||
function OvertimeModal({ defaultPerson, products, projects, versions, reasons, requirements, onVersionChange, onClose, onSubmit }: {
|
||||
defaultPerson: string;
|
||||
products: { id: string; name: string }[];
|
||||
projects: { id: string; name: string; productId: string }[];
|
||||
versions: { id: string; name: string; projectId?: string }[];
|
||||
versions: { id: string; name: string; productId: string; projectId?: string }[];
|
||||
reasons: { id: string; name: string }[];
|
||||
requirements: { id: string; title: string; versionId?: string }[];
|
||||
onVersionChange: (version: { id: string; productId: string }) => void;
|
||||
onClose: () => void;
|
||||
onSubmit: (data: any) => void;
|
||||
}) {
|
||||
@@ -469,7 +470,18 @@ function OvertimeModal({ defaultPerson, products, projects, versions, reasons, r
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-[12px] font-medium text-[var(--ink-soft)] mb-1 block">版本 *</label>
|
||||
<select value={versionId} onChange={(e) => setVersionId(e.target.value)} required className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none">
|
||||
<select
|
||||
value={versionId}
|
||||
onChange={(e) => {
|
||||
const nextVersionId = e.target.value;
|
||||
setVersionId(nextVersionId);
|
||||
setRequirementId('');
|
||||
const version = versions.find((item) => item.id === nextVersionId);
|
||||
if (version) onVersionChange(version);
|
||||
}}
|
||||
required
|
||||
className="h-9 w-full rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] focus:border-[var(--accent)] focus:outline-none"
|
||||
>
|
||||
<option value="">选择版本</option>
|
||||
{filteredVersions.map((v) => <option key={v.id} value={v.id}>{v.name}</option>)}
|
||||
</select>
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function ProductDetailPage() {
|
||||
|
||||
useEffect(() => {
|
||||
fetchProduct(productId);
|
||||
fetchRequirements();
|
||||
fetchRequirements({ productId });
|
||||
fetchRecords();
|
||||
}, [productId, fetchProduct, fetchRequirements, fetchRecords]);
|
||||
|
||||
|
||||
@@ -332,14 +332,21 @@ export default function ProjectDetailPage() {
|
||||
const [showAnalysisDrawer, setShowAnalysisDrawer] = useState(false);
|
||||
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
||||
useEffect(() => { fetchRecords(); }, [fetchRecords]);
|
||||
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
||||
useEffect(() => { fetchDevTasks(); }, [fetchDevTasks]);
|
||||
useEffect(() => { fetchTestCases(); }, [fetchTestCases]);
|
||||
|
||||
const project = useMemo(() => getProjectDetail(overview, projectId), [overview, projectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!project) return;
|
||||
void fetchRequirements({ productId: project.productId, projectId });
|
||||
project.versions.forEach((version) => {
|
||||
void fetchPlans({ versionId: version.id });
|
||||
void fetchDevTasks({ versionId: version.id });
|
||||
void fetchTestCases({ versionId: version.id });
|
||||
void fetchBugs({ versionId: version.id });
|
||||
});
|
||||
}, [fetchBugs, fetchDevTasks, fetchPlans, fetchRequirements, fetchTestCases, project, projectId]);
|
||||
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const currentUserName = user?.name || '';
|
||||
const { roles } = useMemberStore();
|
||||
|
||||
@@ -35,15 +35,23 @@ function ProjectsPageContent() {
|
||||
const [showForm, setShowForm] = useState(false);
|
||||
const [editingProject, setEditingProject] = useState<ProjectWithContext | null>(null);
|
||||
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
||||
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
||||
useEffect(() => { fetchDevTasks(); }, [fetchDevTasks]);
|
||||
useEffect(() => { fetchTestCases(); }, [fetchTestCases]);
|
||||
|
||||
const allProjects = useMemo(() => flattenProjects(overview), [overview]);
|
||||
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
|
||||
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
useEffect(() => {
|
||||
overview.forEach((product) => {
|
||||
void fetchRequirements({ productId: product.id });
|
||||
});
|
||||
}, [fetchRequirements, overview]);
|
||||
useEffect(() => {
|
||||
allVersions.forEach((version) => {
|
||||
void fetchPlans({ versionId: version.id });
|
||||
void fetchDevTasks({ versionId: version.id });
|
||||
void fetchTestCases({ versionId: version.id });
|
||||
});
|
||||
}, [allVersions, fetchDevTasks, fetchPlans, fetchTestCases]);
|
||||
|
||||
const versionProgressMap = useMemo(
|
||||
() => buildVersionProgressMap(allVersions, plans, requirements, devTasks, testCases),
|
||||
[allVersions, plans, requirements, devTasks, testCases],
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { RouteGuard } from '@/components/auth/Guard';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
Search,
|
||||
Plus,
|
||||
Lightbulb,
|
||||
ArrowUp,
|
||||
ArrowDown,
|
||||
ChevronDown,
|
||||
@@ -42,7 +41,6 @@ import { getRequirementVersionSelectionPatch } from '@/lib/requirement-version-l
|
||||
import { Pagination } from '@/components/Pagination';
|
||||
import { RequirementModal } from '@/components/requirement/RequirementModal';
|
||||
import { RequirementDetail } from '@/components/requirement/RequirementDetail';
|
||||
import { DictDrawer, SourceDrawer } from '@/components/requirement/DictDrawer';
|
||||
import { FilterSelect } from '@/components/FilterSelect';
|
||||
import { buildV22RequirementQuery } from '@/lib/requirement-v22-query';
|
||||
import { loadV22RequirementsPage } from '@/lib/v22-api';
|
||||
@@ -199,9 +197,6 @@ function RequirementsPageContent() {
|
||||
const {
|
||||
requirements, fetchRequirements, createRequirement, updateRequirement, deleteRequirement,
|
||||
sourceTargets, types, platforms,
|
||||
addSourceTarget, updateSourceTarget, deleteSourceTarget,
|
||||
addType, updateType, deleteType,
|
||||
addPlatform, updatePlatform, deletePlatform,
|
||||
loaded: requirementsLoaded,
|
||||
} = useRequirementStore();
|
||||
const { overview, fetchOverview } = useProductStore();
|
||||
@@ -233,7 +228,6 @@ function RequirementsPageContent() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [editingReq, setEditingReq] = useState<Requirement | null>(null);
|
||||
const [viewingReq, setViewingReq] = useState<Requirement | null>(null);
|
||||
const [drawerType, setDrawerType] = useState<null | 'source' | 'type' | 'platform'>(null);
|
||||
const [rejectingReq, setRejectingReq] = useState<Requirement | null>(null);
|
||||
const [rejectReason, setRejectReason] = useState('');
|
||||
const [dateSort, setDateSort] = useState<RequirementDateSort>('desc');
|
||||
@@ -274,6 +268,23 @@ function RequirementsPageContent() {
|
||||
if (selectedScope.type === 'project') return allVersions.filter((v) => v.projectId === selectedScope.projectId);
|
||||
return allVersions;
|
||||
}, [allVersions, selectedScope]);
|
||||
const hydrateRequirementStoreFallback = useCallback(async () => {
|
||||
const pendingLoads: Array<Promise<void>> = [];
|
||||
if (selectedScope.type === 'product') {
|
||||
pendingLoads.push(fetchRequirements({ productId: selectedScope.productId }));
|
||||
} else if (selectedScope.type === 'project') {
|
||||
const project = allProjects.find((item) => item.id === selectedScope.projectId);
|
||||
if (project) pendingLoads.push(fetchRequirements({ productId: project.productId, projectId: selectedScope.projectId }));
|
||||
} else {
|
||||
for (const product of overview) {
|
||||
pendingLoads.push(fetchRequirements({ productId: product.id }));
|
||||
}
|
||||
}
|
||||
for (const version of scopedVersions) {
|
||||
pendingLoads.push(fetchDevTasks({ versionId: version.id }));
|
||||
}
|
||||
await Promise.all(pendingLoads);
|
||||
}, [allProjects, fetchDevTasks, fetchRequirements, overview, scopedVersions, selectedScope]);
|
||||
|
||||
const filterResetKey = useMemo(() => JSON.stringify({
|
||||
selectedScopeKey,
|
||||
@@ -319,9 +330,8 @@ function RequirementsPageContent() {
|
||||
|
||||
useEffect(() => {
|
||||
if (v22RequirementQuery && !v22RequirementsFailed) return;
|
||||
fetchRequirements();
|
||||
fetchDevTasks();
|
||||
}, [fetchDevTasks, fetchRequirements, v22RequirementQuery, v22RequirementsFailed]);
|
||||
void hydrateRequirementStoreFallback();
|
||||
}, [hydrateRequirementStoreFallback, v22RequirementQuery, v22RequirementsFailed]);
|
||||
|
||||
useEffect(() => {
|
||||
setV22RequirementsFailed(false);
|
||||
@@ -354,8 +364,7 @@ function RequirementsPageContent() {
|
||||
setV22Requirements([]);
|
||||
setV22NextCursor(undefined);
|
||||
setV22RequirementsFailed(true);
|
||||
fetchRequirements();
|
||||
fetchDevTasks();
|
||||
void hydrateRequirementStoreFallback();
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setV22RequirementsLoading(false);
|
||||
@@ -364,7 +373,7 @@ function RequirementsPageContent() {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [fetchDevTasks, fetchRequirements, page, v22RequirementQuery]);
|
||||
}, [hydrateRequirementStoreFallback, page, v22RequirementQuery]);
|
||||
|
||||
const selectedScopeTitle = useMemo(() => {
|
||||
if (selectedScope.type === 'product') {
|
||||
@@ -472,7 +481,7 @@ function RequirementsPageContent() {
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
await fetchRequirements();
|
||||
await hydrateRequirementStoreFallback();
|
||||
setEditingReq(null);
|
||||
setShowModal(true);
|
||||
};
|
||||
@@ -480,10 +489,6 @@ function RequirementsPageContent() {
|
||||
const handleSelectScope = (scope: RequirementScopeSelection) => {
|
||||
setAutoScopeSelected(true);
|
||||
setSelectedScope(scope);
|
||||
if (scope.type === 'all') {
|
||||
fetchRequirements();
|
||||
fetchDevTasks();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -552,13 +557,6 @@ function RequirementsPageContent() {
|
||||
<p className="mt-0.5 text-[11px] text-[var(--ink-muted)]">{selectedScopeMeta}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => { fetchRequirements(); setDrawerType('source'); }}
|
||||
className="flex h-8 items-center gap-1.5 rounded-lg border border-[var(--line)] bg-[var(--bg-card)] px-3 text-[13px] font-medium text-[var(--ink-soft)] hover:border-[var(--accent)] hover:text-[var(--accent)] transition-colors"
|
||||
>
|
||||
<Lightbulb className="h-3.5 w-3.5" strokeWidth={2} />
|
||||
来源管理
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="flex h-8 items-center gap-1.5 rounded-lg bg-[var(--accent)] px-3 text-[13px] font-medium text-white shadow-[var(--shadow-sm)] hover:bg-[var(--accent-hover)] transition-colors"
|
||||
@@ -888,41 +886,6 @@ function RequirementsPageContent() {
|
||||
setShowModal(false);
|
||||
setEditingReq(null);
|
||||
}}
|
||||
onOpenDrawer={(type) => setDrawerType(type)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Drawer */}
|
||||
{drawerType === 'source' && (
|
||||
<SourceDrawer
|
||||
open={true}
|
||||
items={sourceTargets}
|
||||
onClose={() => setDrawerType(null)}
|
||||
onAdd={addSourceTarget}
|
||||
onUpdate={updateSourceTarget}
|
||||
onDelete={deleteSourceTarget}
|
||||
/>
|
||||
)}
|
||||
{drawerType === 'type' && (
|
||||
<DictDrawer
|
||||
open={true}
|
||||
title="需求类型管理"
|
||||
items={types}
|
||||
onClose={() => setDrawerType(null)}
|
||||
onAdd={addType}
|
||||
onUpdate={updateType}
|
||||
onDelete={deleteType}
|
||||
/>
|
||||
)}
|
||||
{drawerType === 'platform' && (
|
||||
<DictDrawer
|
||||
open={true}
|
||||
title="支持端管理"
|
||||
items={platforms}
|
||||
onClose={() => setDrawerType(null)}
|
||||
onAdd={addPlatform}
|
||||
onUpdate={updatePlatform}
|
||||
onDelete={deletePlatform}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { AlertTriangle, Calendar, Check, ChevronLeft, Clock, FileText, Link2, Pencil, Search, Settings, Sparkles, UserPlus, X } from 'lucide-react';
|
||||
import { useProductStore } from '@/stores/useProductStore';
|
||||
@@ -142,6 +142,7 @@ export default function VersionDetailPage() {
|
||||
() => allMembers.map((member) => ({
|
||||
id: member.id,
|
||||
name: member.name,
|
||||
username: member.username,
|
||||
departmentName: departments.find((department) => department.id === member.departmentId)?.name,
|
||||
})),
|
||||
[allMembers, departments],
|
||||
@@ -163,6 +164,12 @@ export default function VersionDetailPage() {
|
||||
const [showAnalysisDrawer, setShowAnalysisDrawer] = useState(false);
|
||||
const [recommendationDataReady, setRecommendationDataReady] = useState(false);
|
||||
const [v22Scope, setV22Scope] = useState<VersionDataScope | null>(null);
|
||||
const version = useMemo(() => getVersionDetail(overview, versionId), [overview, versionId]);
|
||||
const fetchScopedRequirements = useCallback((force = false) => {
|
||||
if (!version) return Promise.resolve();
|
||||
if (force) return fetchRequirements({ productId: version.productId, versionId, force: true });
|
||||
return fetchRequirements({ productId: version.productId, versionId });
|
||||
}, [fetchRequirements, version, versionId]);
|
||||
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
useEffect(() => {
|
||||
@@ -182,43 +189,44 @@ export default function VersionDetailPage() {
|
||||
fetchRecords();
|
||||
return;
|
||||
}
|
||||
if (!version) return;
|
||||
if (activeTab === 'requirements') {
|
||||
fetchRequirements();
|
||||
fetchDevTasks();
|
||||
fetchScopedRequirements();
|
||||
fetchDevTasks({ versionId });
|
||||
return;
|
||||
}
|
||||
if (activeTab === 'research' || activeTab === 'product' || activeTab === 'ui') {
|
||||
fetchRequirements();
|
||||
fetchPlans();
|
||||
fetchScopedRequirements();
|
||||
fetchPlans({ versionId });
|
||||
return;
|
||||
}
|
||||
if (activeTab === 'tasks') {
|
||||
fetchRequirements();
|
||||
fetchDevTasks();
|
||||
fetchScopedRequirements();
|
||||
fetchDevTasks({ versionId });
|
||||
return;
|
||||
}
|
||||
if (activeTab === 'testcases') {
|
||||
fetchRequirements();
|
||||
fetchDevTasks();
|
||||
fetchTestCases();
|
||||
fetchBugs();
|
||||
fetchScopedRequirements();
|
||||
fetchDevTasks({ versionId });
|
||||
fetchTestCases({ versionId });
|
||||
fetchBugs({ versionId });
|
||||
return;
|
||||
}
|
||||
if (activeTab === 'bugs') {
|
||||
fetchRequirements();
|
||||
fetchTestCases();
|
||||
fetchBugs();
|
||||
fetchScopedRequirements();
|
||||
fetchTestCases({ versionId });
|
||||
fetchBugs({ versionId });
|
||||
}
|
||||
}, [activeTab, fetchBugs, fetchDevTasks, fetchPlans, fetchRecords, fetchRequirements, fetchTestCases]);
|
||||
}, [activeTab, fetchBugs, fetchDevTasks, fetchPlans, fetchRecords, fetchScopedRequirements, fetchTestCases, version, versionId]);
|
||||
useEffect(() => {
|
||||
if (!showRecommendModal) return;
|
||||
fetchRecords();
|
||||
fetchRequirements();
|
||||
fetchPlans();
|
||||
fetchDevTasks();
|
||||
fetchTestCases();
|
||||
fetchBugs();
|
||||
}, [fetchBugs, fetchDevTasks, fetchPlans, fetchRecords, fetchRequirements, fetchTestCases, showRecommendModal]);
|
||||
fetchScopedRequirements(true);
|
||||
fetchPlans({ versionId });
|
||||
fetchDevTasks({ versionId });
|
||||
fetchTestCases({ versionId });
|
||||
fetchBugs({ versionId });
|
||||
}, [fetchBugs, fetchDevTasks, fetchPlans, fetchRecords, fetchScopedRequirements, fetchTestCases, showRecommendModal, versionId]);
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
setRecommendationDataReady(false);
|
||||
@@ -228,8 +236,7 @@ export default function VersionDetailPage() {
|
||||
return () => { active = false; };
|
||||
}, [fetchMembers, fetchCategories]);
|
||||
|
||||
const version = useMemo(() => getVersionDetail(overview, versionId), [overview, versionId]);
|
||||
const appDataVersionScope = useMemo(
|
||||
const storeVersionScope = useMemo(
|
||||
() => version
|
||||
? buildVersionDataScope({
|
||||
versionId: version.id,
|
||||
@@ -250,19 +257,20 @@ export default function VersionDetailPage() {
|
||||
const testCaseWriteReady = requirementsLoaded && devTasksLoaded && testCasesLoaded && bugsLoaded;
|
||||
const bugWriteReady = requirementsLoaded && testCasesLoaded && bugsLoaded;
|
||||
const loadAllVersionStores = () => {
|
||||
if (!version) return;
|
||||
fetchRecords();
|
||||
fetchRequirements();
|
||||
fetchPlans();
|
||||
fetchDevTasks();
|
||||
fetchTestCases();
|
||||
fetchBugs();
|
||||
fetchRequirements({ productId: version.productId, versionId, force: true });
|
||||
fetchPlans({ versionId });
|
||||
fetchDevTasks({ versionId });
|
||||
fetchTestCases({ versionId });
|
||||
fetchBugs({ versionId });
|
||||
};
|
||||
const versionScope = useMemo(
|
||||
() => selectVersionDataScope({
|
||||
appDataScope: appDataVersionScope,
|
||||
v22Scope: versionWriteStoresReady ? null : v22Scope,
|
||||
storeScope: storeVersionScope,
|
||||
v22Scope,
|
||||
}),
|
||||
[appDataVersionScope, v22Scope, versionWriteStoresReady],
|
||||
[storeVersionScope, v22Scope],
|
||||
);
|
||||
const releaseProgress = versionScope
|
||||
? calcScopedVersionProgress(versionScope.plans, versionScope.devTasks, versionScope.testCases)
|
||||
@@ -327,15 +335,12 @@ export default function VersionDetailPage() {
|
||||
.some((permission) => canWriteVersionPermission(permission));
|
||||
|
||||
const scopedVersionData = versionScope!;
|
||||
const appDataScopedVersionData = appDataVersionScope ?? scopedVersionData;
|
||||
const displayRequirements = requirementsLoaded ? requirements : scopedVersionData.requirements;
|
||||
const displayPlans = plansLoaded ? appDataScopedVersionData.plans : scopedVersionData.plans;
|
||||
const displayDevTasks = devTasksLoaded ? appDataScopedVersionData.devTasks : scopedVersionData.devTasks;
|
||||
const displayTestCases = testCasesLoaded ? appDataScopedVersionData.testCases : scopedVersionData.testCases;
|
||||
const displayBugs = bugsLoaded ? appDataScopedVersionData.bugs : scopedVersionData.bugs;
|
||||
const displayRequirementIds = requirementsLoaded
|
||||
? appDataScopedVersionData.requirementIds
|
||||
: scopedVersionData.requirementIds;
|
||||
const displayRequirements = scopedVersionData.requirements;
|
||||
const displayPlans = scopedVersionData.plans;
|
||||
const displayDevTasks = scopedVersionData.devTasks;
|
||||
const displayTestCases = scopedVersionData.testCases;
|
||||
const displayBugs = scopedVersionData.bugs;
|
||||
const displayRequirementIds = scopedVersionData.requirementIds;
|
||||
const memberRecommendationGroups = showRecommendModal && recommendationDataReady ? (() => {
|
||||
const currentSystemParticipation = new Map<string, number>();
|
||||
overview.forEach((product) => {
|
||||
@@ -1066,8 +1071,10 @@ export default function VersionDetailPage() {
|
||||
version={version}
|
||||
versionDeadline={version.expectedReleaseDate ?? undefined}
|
||||
currentUserName={user?.name ?? ''}
|
||||
currentUserReference={user?.username ?? user?.id ?? user?.name ?? ''}
|
||||
planType={pt}
|
||||
versionMembers={version.members ?? []}
|
||||
allMembers={memberCandidates}
|
||||
linkedRequirements={versionLinkedReqs}
|
||||
allRequirements={displayRequirements}
|
||||
readOnly={versionReadonly || !planWriteReady || !canWriteVersionPermission(PLAN_MANAGE_PERMISSION[pt])}
|
||||
|
||||
@@ -94,16 +94,9 @@ function VersionsPageContent() {
|
||||
useEffect(() => { fetchOverview(); }, [fetchOverview]);
|
||||
|
||||
const { requirements, fetchRequirements, updateRequirement } = useRequirementStore();
|
||||
useEffect(() => { fetchRequirements(); }, [fetchRequirements]);
|
||||
|
||||
const { plans, fetchPlans } = useVersionPlanStore();
|
||||
useEffect(() => { fetchPlans(); }, [fetchPlans]);
|
||||
|
||||
const { tasks: devTasks, fetchTasks: fetchDevTasks } = useDevTaskStore();
|
||||
useEffect(() => { fetchDevTasks(); }, [fetchDevTasks]);
|
||||
|
||||
const { testCases, fetchTestCases } = useTestCaseStore();
|
||||
useEffect(() => { fetchTestCases(); }, [fetchTestCases]);
|
||||
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const currentUserName = user?.name || '';
|
||||
@@ -125,6 +118,14 @@ function VersionsPageContent() {
|
||||
if (!v.members || v.members.length === 0) return true;
|
||||
return v.members.some((m) => m.name === currentUserName);
|
||||
}), [allVersionsRaw, currentUserName, isSuperAdmin]);
|
||||
useEffect(() => {
|
||||
for (const version of allVersions) {
|
||||
void fetchRequirements({ productId: version.productId, versionId: version.id });
|
||||
void fetchPlans({ versionId: version.id });
|
||||
void fetchDevTasks({ versionId: version.id });
|
||||
void fetchTestCases({ versionId: version.id });
|
||||
}
|
||||
}, [allVersions, fetchDevTasks, fetchPlans, fetchRequirements, fetchTestCases]);
|
||||
const versionTree = useMemo(() => buildVersionScopeTree(allVersions, treeKeyword), [allVersions, treeKeyword]);
|
||||
|
||||
// Compute overall progress per version from actual data
|
||||
|
||||
@@ -32,7 +32,7 @@ import { loadV22WorkspaceData, type V22WorkspaceData } from '@/lib/v22-api';
|
||||
import { selectWorkspaceCollections } from '@/lib/workspace-v22-source';
|
||||
|
||||
type TabKey = 'all' | 'plan_research' | 'plan_product' | 'plan_ui' | 'devTask' | 'testCase' | 'bug';
|
||||
type WorkspaceVersionContext = { id: string; name: string; productName: string; projectName: string; status: VersionStatus };
|
||||
type WorkspaceVersionContext = { id: string; name: string; productId: string; productName: string; projectName: string; status: VersionStatus };
|
||||
type TreeVersion = { id: string; name: string; status: VersionStatus; pendingCount: number };
|
||||
type ProductTree = Map<string, { name: string; projects: Map<string, { name: string; versions: TreeVersion[] }> }>;
|
||||
|
||||
@@ -56,11 +56,11 @@ const PLAN_STATUS_LABEL: Record<string, string> = { pending: '未开始', in_pro
|
||||
export default function WorkspacePage() {
|
||||
const router = useRouter();
|
||||
const { overview, fetchOverview } = useProductStore();
|
||||
const { plans, fetchPlans, loaded: plansLoaded } = useVersionPlanStore();
|
||||
const { requirements, fetchRequirements, loaded: requirementsLoaded } = useRequirementStore();
|
||||
const { tasks: devTasks, fetchTasks, loaded: devTasksLoaded } = useDevTaskStore();
|
||||
const { testCases, fetchTestCases, loaded: testCasesLoaded } = useTestCaseStore();
|
||||
const { bugs, fetchBugs, loaded: bugsLoaded } = useBugStore();
|
||||
const { plans, fetchPlans } = useVersionPlanStore();
|
||||
const { requirements, fetchRequirements } = useRequirementStore();
|
||||
const { tasks: devTasks, fetchTasks } = useDevTaskStore();
|
||||
const { testCases, fetchTestCases } = useTestCaseStore();
|
||||
const { bugs, fetchBugs } = useBugStore();
|
||||
const { worklogs, fetchWorklogs } = useTaskWorklogStore();
|
||||
const { activities, fetchActivities } = useWorkActivityStore();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
@@ -81,6 +81,16 @@ export default function WorkspacePage() {
|
||||
const userName = user?.name ?? '';
|
||||
const workspaceUserKey = userId || userName;
|
||||
const workspaceUserRefs = useMemo(() => [userName, userId].filter(Boolean), [userId, userName]);
|
||||
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
|
||||
const hydrateWorkspaceStoreFallback = useCallback(() => {
|
||||
for (const version of allVersions) {
|
||||
void fetchRequirements({ productId: version.productId, versionId: version.id });
|
||||
void fetchPlans({ versionId: version.id });
|
||||
void fetchTasks({ versionId: version.id });
|
||||
void fetchTestCases({ versionId: version.id });
|
||||
void fetchBugs({ versionId: version.id });
|
||||
}
|
||||
}, [allVersions, fetchBugs, fetchPlans, fetchRequirements, fetchTasks, fetchTestCases]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceUserKey.trim()) {
|
||||
@@ -112,26 +122,19 @@ export default function WorkspacePage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!workspaceUserKey.trim() || !v22WorkspaceFailed) return;
|
||||
void fetchPlans();
|
||||
void fetchRequirements();
|
||||
void fetchTasks();
|
||||
void fetchTestCases();
|
||||
void fetchBugs();
|
||||
}, [
|
||||
fetchBugs,
|
||||
fetchPlans,
|
||||
fetchRequirements,
|
||||
fetchTasks,
|
||||
fetchTestCases,
|
||||
v22WorkspaceFailed,
|
||||
workspaceUserKey,
|
||||
]);
|
||||
|
||||
const allVersions = useMemo(() => flattenVersions(overview), [overview]);
|
||||
hydrateWorkspaceStoreFallback();
|
||||
}, [hydrateWorkspaceStoreFallback, v22WorkspaceFailed, workspaceUserKey]);
|
||||
|
||||
const versionMap = useMemo(() => {
|
||||
const map = new Map<string, WorkspaceVersionContext>();
|
||||
allVersions.forEach((v) => map.set(v.id, { id: v.id, name: v.name, productName: v.productName, projectName: v.projectName, status: v.status }));
|
||||
allVersions.forEach((v) => map.set(v.id, {
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
productId: v.productId,
|
||||
productName: v.productName,
|
||||
projectName: v.projectName,
|
||||
status: v.status,
|
||||
}));
|
||||
return map;
|
||||
}, [allVersions]);
|
||||
|
||||
@@ -146,7 +149,7 @@ export default function WorkspacePage() {
|
||||
v22Loaded: v22WorkspaceLoaded,
|
||||
v22Failed: v22WorkspaceFailed,
|
||||
v22Data: v22WorkspaceData,
|
||||
appData: { plans, devTasks, testCases, bugs },
|
||||
storeData: { plans, devTasks, testCases, bugs },
|
||||
}),
|
||||
[bugs, devTasks, plans, testCases, v22WorkspaceData, v22WorkspaceFailed, v22WorkspaceLoaded],
|
||||
);
|
||||
@@ -227,25 +230,24 @@ export default function WorkspacePage() {
|
||||
const drawerReadOnly = drawerVersionStatus ? isVersionReadonly(drawerVersionStatus) : false;
|
||||
const ensureWorkspaceDrawerStores = useCallback(async (item: WorkItem) => {
|
||||
const pendingLoads: Array<Promise<void>> = [];
|
||||
if (!requirementsLoaded) pendingLoads.push(fetchRequirements());
|
||||
if ((item.type === 'plan_research' || item.type === 'plan_product' || item.type === 'plan_ui') && !plansLoaded) {
|
||||
pendingLoads.push(fetchPlans());
|
||||
const versionContext = versionMap.get(item.versionId);
|
||||
if (versionContext) {
|
||||
pendingLoads.push(fetchRequirements({ productId: versionContext.productId, versionId: item.versionId }));
|
||||
}
|
||||
if (item.type === 'devTask' && !devTasksLoaded) pendingLoads.push(fetchTasks());
|
||||
if (item.type === 'testCase' && !testCasesLoaded) pendingLoads.push(fetchTestCases());
|
||||
if (item.type === 'bug' && !bugsLoaded) pendingLoads.push(fetchBugs());
|
||||
if (item.type === 'plan_research' || item.type === 'plan_product' || item.type === 'plan_ui') {
|
||||
pendingLoads.push(fetchPlans({ versionId: item.versionId }));
|
||||
}
|
||||
if (item.type === 'devTask') pendingLoads.push(fetchTasks({ versionId: item.versionId }));
|
||||
if (item.type === 'testCase') pendingLoads.push(fetchTestCases({ versionId: item.versionId }));
|
||||
if (item.type === 'bug') pendingLoads.push(fetchBugs({ versionId: item.versionId }));
|
||||
await Promise.all(pendingLoads);
|
||||
}, [
|
||||
bugsLoaded,
|
||||
devTasksLoaded,
|
||||
fetchBugs,
|
||||
fetchPlans,
|
||||
fetchRequirements,
|
||||
fetchTasks,
|
||||
fetchTestCases,
|
||||
plansLoaded,
|
||||
requirementsLoaded,
|
||||
testCasesLoaded,
|
||||
versionMap,
|
||||
fetchBugs,
|
||||
]);
|
||||
const openWorkItemDrawer = useCallback(async (item: WorkItem) => {
|
||||
await ensureWorkspaceDrawerStores(item);
|
||||
|
||||
@@ -9,15 +9,12 @@ import { useXiaobaoWarningRisks } from '@/hooks/useXiaobaoWarningRisks';
|
||||
import { useAuthStore } from '@/stores/useAuthStore';
|
||||
import { useXiaobaoWarningReadStore } from '@/stores/useXiaobaoWarningReadStore';
|
||||
import type { XiaobaoRiskLevel, XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
|
||||
import { buildRiskInsightSignature, findPreviousRiskSnapshot, requestRiskInsight, shouldRequestRiskInsightWithRequestGate } from '@/lib/xiaobao-risk-ai';
|
||||
import { buildRiskSignature, findLatestDailySnapshot, shouldSaveRiskSnapshot } from '@/lib/xiaobao-risk-trend';
|
||||
import { attachXiaobaoRiskSuggestion, buildXiaobaoRiskInsightPendingKey } from '@/lib/xiaobao-risk-suggestion';
|
||||
import { attachXiaobaoRiskSuggestion } from '@/lib/xiaobao-risk-suggestion';
|
||||
import {
|
||||
filterXiaobaoRiskWarnings,
|
||||
formatRemainingWork,
|
||||
isXiaobaoWarningUpdated,
|
||||
shouldSkipXiaobaoRiskInsightRequestForReadState,
|
||||
sanitizeRiskInsight,
|
||||
} from '@/lib/xiaobao-warning-view';
|
||||
import { formatDateTime } from '@/lib/format';
|
||||
|
||||
@@ -57,19 +54,13 @@ function XiaobaoWarningContent() {
|
||||
insights,
|
||||
pendingInsightKeys,
|
||||
insightRequestAttempts,
|
||||
riskDataLoaded,
|
||||
saveSnapshot,
|
||||
saveInsight,
|
||||
beginInsightUpdate,
|
||||
finishInsightUpdate,
|
||||
today,
|
||||
} = useXiaobaoWarningRisks({ loadRiskCache: true });
|
||||
const { readStates, readStateLoaded, fetchReadStates, markRiskRead } = useXiaobaoWarningReadStore();
|
||||
const [selectedRiskId, setSelectedRiskId] = useState<string | null>(null);
|
||||
const [selectedProductId, setSelectedProductId] = useState('');
|
||||
const [selectedProjectId, setSelectedProjectId] = useState('');
|
||||
const savedSnapshotKeysRef = useRef(new Set<string>());
|
||||
const requestedInsightKeysRef = useRef(new Set<string>());
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.id) fetchReadStates();
|
||||
@@ -89,52 +80,6 @@ function XiaobaoWarningContent() {
|
||||
});
|
||||
}, [risks, saveSnapshot, snapshots]);
|
||||
|
||||
useEffect(() => {
|
||||
risks.forEach((risk) => {
|
||||
if (shouldSkipXiaobaoRiskInsightRequestForReadState(risk, readStates, user?.id, readStateLoaded)) return;
|
||||
const previous = findPreviousRiskSnapshot(snapshots, risk.versionId, today);
|
||||
const key = buildXiaobaoRiskInsightPendingKey(risk);
|
||||
if (!shouldRequestRiskInsightWithRequestGate({
|
||||
riskCacheLoaded: riskDataLoaded,
|
||||
cache: insights,
|
||||
current: risk,
|
||||
previous,
|
||||
lastRequestedAt: insightRequestAttempts[key],
|
||||
})) return;
|
||||
const signature = buildRiskInsightSignature(risk);
|
||||
if (pendingInsightKeys.includes(key)) return;
|
||||
if (requestedInsightKeysRef.current.has(key)) return;
|
||||
requestedInsightKeysRef.current.add(key);
|
||||
beginInsightUpdate(key);
|
||||
requestRiskInsight(risk).then((response) => {
|
||||
if (!response.ok) return;
|
||||
return saveInsight({
|
||||
versionId: risk.versionId,
|
||||
riskSignature: signature,
|
||||
insight: sanitizeRiskInsight(response.result),
|
||||
generatedAt: new Date().toISOString(),
|
||||
providerInfo: { model: response.meta.model },
|
||||
}).catch(() => {});
|
||||
}).catch(() => {}).finally(() => {
|
||||
finishInsightUpdate(key);
|
||||
});
|
||||
});
|
||||
}, [
|
||||
beginInsightUpdate,
|
||||
finishInsightUpdate,
|
||||
insights,
|
||||
insightRequestAttempts,
|
||||
pendingInsightKeys,
|
||||
readStateLoaded,
|
||||
readStates,
|
||||
riskDataLoaded,
|
||||
risks,
|
||||
saveInsight,
|
||||
snapshots,
|
||||
today,
|
||||
user?.id,
|
||||
]);
|
||||
|
||||
const risksWithInsight = useMemo(() => risks.map((risk) => attachXiaobaoRiskSuggestion(risk, {
|
||||
insights,
|
||||
pendingInsightKeys,
|
||||
|
||||
Reference in New Issue
Block a user