feat: 支持 ITTO 明细功能

- 更新类型定义,支持 ProcessRef(字符串或对象)
- 添加 DetailItem 和 ProcessEntityUse 接口
- 为 P1.2(制定项目管理计划)添加工具明细示例
  - 数据收集:头脑风暴、核对单、焦点小组、访谈
  - 人际关系与团队技能:冲突管理、引导、会议管理
- 更新数据查询函数,支持新数据结构
- 更新前端展示,支持明细显示(带缩进和项目符号)
- 修复 ProcessGraphPage 类型错误

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>
This commit is contained in:
ittoview
2026-02-14 13:49:42 +00:00
parent 3c1451ca3f
commit 145e6e7549
5 changed files with 163 additions and 34 deletions

View File

@@ -16,6 +16,8 @@ import type {
Artifact,
ToolTechnique,
DataFlow,
ProcessRef,
ProcessEntityUse,
} from '../types/itto';
// 导出原始数据
@@ -64,14 +66,37 @@ processGroups.forEach(pg => {
);
});
// 工具函数:规范化 ProcessRef将字符串或对象统一处理
export function normalizeProcessRef(ref: ProcessRef): {
id: string;
detail?: ProcessEntityUse['detail'];
note?: string;
} {
if (typeof ref === 'string') {
return { id: ref };
}
return ref;
}
// 工具函数:从 ProcessRef 中提取 ID
export function extractId(ref: ProcessRef): string {
return typeof ref === 'string' ? ref : ref.id;
}
// 工具函数:检查 ProcessRef 数组中是否包含某个 ID
export function includesId(refs: ProcessRef[], targetId: string): boolean {
return refs.some(ref => extractId(ref) === targetId);
}
// 计算数据流向关系
export function computeDataFlows(): DataFlow[] {
const flows: DataFlow[] = [];
processes.forEach(sourceProcess => {
sourceProcess.outputs.forEach(outputId => {
sourceProcess.outputs.forEach(outputRef => {
const outputId = extractId(outputRef);
processes.forEach(targetProcess => {
if (targetProcess.id !== sourceProcess.id && targetProcess.inputs.includes(outputId)) {
if (targetProcess.id !== sourceProcess.id && includesId(targetProcess.inputs, outputId)) {
flows.push({
sourceProcessId: sourceProcess.id,
targetProcessId: targetProcess.id,
@@ -91,14 +116,14 @@ export function getArtifactUsage(artifactId: string): {
asOutput: Process[];
} {
return {
asInput: processes.filter(p => p.inputs.includes(artifactId)),
asOutput: processes.filter(p => p.outputs.includes(artifactId)),
asInput: processes.filter(p => includesId(p.inputs, artifactId)),
asOutput: processes.filter(p => includesId(p.outputs, artifactId)),
};
}
// 获取工具的使用情况
export function getToolUsage(toolId: string): Process[] {
return processes.filter(p => p.tools.includes(toolId));
return processes.filter(p => includesId(p.tools, toolId));
}
// 获取过程的完整信息(包含关联数据)
@@ -110,9 +135,21 @@ export function getProcessDetail(processId: string) {
...process,
knowledgeArea: knowledgeAreaMap.get(process.knowledgeAreaId),
processGroup: processGroupMap.get(process.processGroupId),
inputDetails: process.inputs.map(id => artifactMap.get(id)).filter(Boolean),
toolDetails: process.tools.map(id => toolMap.get(id)).filter(Boolean),
outputDetails: process.outputs.map(id => artifactMap.get(id)).filter(Boolean),
inputDetails: process.inputs.map(ref => {
const normalized = normalizeProcessRef(ref);
const artifact = artifactMap.get(normalized.id);
return artifact ? { ...artifact, detail: normalized.detail, note: normalized.note } : null;
}).filter(Boolean),
toolDetails: process.tools.map(ref => {
const normalized = normalizeProcessRef(ref);
const tool = toolMap.get(normalized.id);
return tool ? { ...tool, detail: normalized.detail, note: normalized.note } : null;
}).filter(Boolean),
outputDetails: process.outputs.map(ref => {
const normalized = normalizeProcessRef(ref);
const artifact = artifactMap.get(normalized.id);
return artifact ? { ...artifact, detail: normalized.detail, note: normalized.note } : null;
}).filter(Boolean),
};
}

View File

@@ -29,7 +29,27 @@
"processGroupId": "PG02",
"order": 2,
"inputs": ["A001", "A092", "A005", "A006"],
"tools": ["TT001", "TT002", "TT022", "TT032"],
"tools": [
"TT001",
{
"id": "TT002",
"detail": [
{ "label": "头脑风暴" },
{ "label": "核对单" },
{ "label": "焦点小组" },
{ "label": "访谈" }
]
},
{
"id": "TT022",
"detail": [
{ "label": "冲突管理" },
{ "label": "引导" },
{ "label": "会议管理" }
]
},
"TT032"
],
"outputs": ["A008"],
"w5h1": {
"who": "项目经理主导,团队参与",

View File

@@ -1,7 +1,7 @@
import { useParams, Link, useLocation, useNavigate } from 'react-router-dom'
import { motion, AnimatePresence } from 'framer-motion'
import { ArrowLeft, ArrowRight, FileText, Wrench, FileOutput, LayoutGrid, Workflow, User, Target, Clock, MapPin, HelpCircle, Cog, Eye, EyeOff } from 'lucide-react'
import { getProcessDetail, processes, artifactMap, toolMap } from '@/data'
import { getProcessDetail, processes } from '@/data'
import { useState, useEffect } from 'react'
// 5W1H图标和标签配置
@@ -237,12 +237,29 @@ export function ProcessDetailPage() {
transition={{ duration: 0.18 }}
className="divide-y divide-gray-100 dark:divide-gray-700"
>
{processDetail.inputs.map((inputId) => {
const artifact = artifactMap.get(inputId)
{processDetail.inputDetails?.map((inputDetail: any) => {
const hasDetail = inputDetail.detail && inputDetail.detail.length > 0
return (
<li key={inputId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{artifact?.name || inputId}</div>
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</div>}
<li key={inputDetail.id} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{inputDetail.name || inputDetail.id}</div>
{inputDetail.nameEn && <div className="text-xs text-gray-500 dark:text-gray-400">{inputDetail.nameEn}</div>}
{hasDetail && (
<div className="mt-2 pl-3 border-l-2 border-blue-200 dark:border-blue-700">
<ul className="space-y-1">
{inputDetail.detail.map((item: any, idx: number) => (
<li key={item.id || idx} className="text-xs text-gray-600 dark:text-gray-400 flex items-start gap-1.5">
<span className="text-blue-500 dark:text-blue-400 mt-0.5"></span>
<span>{item.label}</span>
</li>
))}
</ul>
</div>
)}
{inputDetail.note && (
<div className="mt-1.5 text-xs text-gray-500 dark:text-gray-400 italic">
💡 {inputDetail.note}
</div>
)}
</li>
)
})}
@@ -290,12 +307,29 @@ export function ProcessDetailPage() {
transition={{ duration: 0.18 }}
className="divide-y divide-gray-100 dark:divide-gray-700"
>
{processDetail.tools.map((toolId) => {
const tool = toolMap.get(toolId)
{processDetail.toolDetails?.map((toolDetail: any) => {
const hasDetail = toolDetail.detail && toolDetail.detail.length > 0
return (
<li key={toolId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{tool?.name || toolId}</div>
{tool && <div className="text-xs text-gray-500 dark:text-gray-400">{tool.nameEn}</div>}
<li key={toolDetail.id} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{toolDetail.name || toolDetail.id}</div>
{toolDetail.nameEn && <div className="text-xs text-gray-500 dark:text-gray-400">{toolDetail.nameEn}</div>}
{hasDetail && (
<div className="mt-2 pl-3 border-l-2 border-amber-200 dark:border-amber-700">
<ul className="space-y-1">
{toolDetail.detail.map((item: any, idx: number) => (
<li key={item.id || idx} className="text-xs text-gray-600 dark:text-gray-400 flex items-start gap-1.5">
<span className="text-amber-500 dark:text-amber-400 mt-0.5"></span>
<span>{item.label}</span>
</li>
))}
</ul>
</div>
)}
{toolDetail.note && (
<div className="mt-1.5 text-xs text-gray-500 dark:text-gray-400 italic">
💡 {toolDetail.note}
</div>
)}
</li>
)
})}
@@ -343,12 +377,29 @@ export function ProcessDetailPage() {
transition={{ duration: 0.18 }}
className="divide-y divide-gray-100 dark:divide-gray-700"
>
{processDetail.outputs.map((outputId) => {
const artifact = artifactMap.get(outputId)
{processDetail.outputDetails?.map((outputDetail: any) => {
const hasDetail = outputDetail.detail && outputDetail.detail.length > 0
return (
<li key={outputId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{artifact?.name || outputId}</div>
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</div>}
<li key={outputDetail.id} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
<div className="font-medium text-gray-900 dark:text-white text-sm">{outputDetail.name || outputDetail.id}</div>
{outputDetail.nameEn && <div className="text-xs text-gray-500 dark:text-gray-400">{outputDetail.nameEn}</div>}
{hasDetail && (
<div className="mt-2 pl-3 border-l-2 border-emerald-200 dark:border-emerald-700">
<ul className="space-y-1">
{outputDetail.detail.map((item: any, idx: number) => (
<li key={item.id || idx} className="text-xs text-gray-600 dark:text-gray-400 flex items-start gap-1.5">
<span className="text-emerald-500 dark:text-emerald-400 mt-0.5"></span>
<span>{item.label}</span>
</li>
))}
</ul>
</div>
)}
{outputDetail.note && (
<div className="mt-1.5 text-xs text-gray-500 dark:text-gray-400 italic">
💡 {outputDetail.note}
</div>
)}
</li>
)
})}

View File

@@ -18,6 +18,7 @@ import {
getProcessDetail,
getArtifactUsage,
getToolUsage,
extractId,
} from '@/data'
export function ProcessGraphPage() {
@@ -92,8 +93,8 @@ export function ProcessGraphPage() {
// 2. 添加工件节点和关系
const usedArtifacts = new Set<string>()
processes.forEach(p => {
p.inputs.forEach(id => usedArtifacts.add(id))
p.outputs.forEach(id => usedArtifacts.add(id))
p.inputs.forEach(ref => usedArtifacts.add(extractId(ref)))
p.outputs.forEach(ref => usedArtifacts.add(extractId(ref)))
})
artifacts.forEach(a => {
@@ -127,7 +128,7 @@ export function ProcessGraphPage() {
// 3. 添加工具节点和关系
const usedTools = new Set<string>()
processes.forEach(p => {
p.tools.forEach(id => usedTools.add(id))
p.tools.forEach(ref => usedTools.add(extractId(ref)))
})
tools.forEach(t => {
@@ -160,7 +161,8 @@ export function ProcessGraphPage() {
// 4. 构建边
processes.forEach(p => {
// 输入关系: Artifact -> Process
p.inputs.forEach(inputId => {
p.inputs.forEach(inputRef => {
const inputId = extractId(inputRef)
if (addedNodeIds.has(inputId)) {
edges.push({
source: inputId,
@@ -176,7 +178,8 @@ export function ProcessGraphPage() {
})
// 输出关系: Process -> Artifact
p.outputs.forEach(outputId => {
p.outputs.forEach(outputRef => {
const outputId = extractId(outputRef)
if (addedNodeIds.has(outputId)) {
edges.push({
source: p.id,
@@ -192,7 +195,8 @@ export function ProcessGraphPage() {
})
// 工具关系: Tool -> Process
p.tools.forEach(toolId => {
p.tools.forEach(toolRef => {
const toolId = extractId(toolRef)
if (addedNodeIds.has(toolId)) {
edges.push({
source: toolId,

View File

@@ -36,6 +36,23 @@ export interface Process5W1H {
how: string; // 如何执行(关键方法)
}
// 明细项
export interface DetailItem {
id?: string; // 可选:若明细在工具库里注册,可复用该 ID
label: string; // 明细名称
description?: string; // 明细描述
}
// 过程实体使用(支持明细)
export interface ProcessEntityUse {
id: string; // 实体ID工件/工具ID
detail?: DetailItem[]; // 明细列表
note?: string; // 针对此过程的补充说明
}
// 过程引用类型字符串ID 或 带明细的对象)
export type ProcessRef = string | ProcessEntityUse;
// 过程
export interface Process {
id: string; // 如 "P4.1"
@@ -45,9 +62,9 @@ export interface Process {
knowledgeAreaId: string; // 所属知识领域ID
processGroupId: string; // 所属过程组ID
order: number; // 在知识领域内的序号
inputs: string[]; // 输入工件ID列表
tools: string[]; // 工具与技术ID列表
outputs: string[]; // 输出工件ID列表
inputs: ProcessRef[]; // 输入工件ID列表(支持明细)
tools: ProcessRef[]; // 工具与技术ID列表(支持明细)
outputs: ProcessRef[]; // 输出工件ID列表(支持明细)
w5h1?: Process5W1H; // 5W1H记忆辅助信息
}