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:
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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": "项目经理主导,团队参与",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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记忆辅助信息
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user