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,
|
Artifact,
|
||||||
ToolTechnique,
|
ToolTechnique,
|
||||||
DataFlow,
|
DataFlow,
|
||||||
|
ProcessRef,
|
||||||
|
ProcessEntityUse,
|
||||||
} from '../types/itto';
|
} 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[] {
|
export function computeDataFlows(): DataFlow[] {
|
||||||
const flows: DataFlow[] = [];
|
const flows: DataFlow[] = [];
|
||||||
|
|
||||||
processes.forEach(sourceProcess => {
|
processes.forEach(sourceProcess => {
|
||||||
sourceProcess.outputs.forEach(outputId => {
|
sourceProcess.outputs.forEach(outputRef => {
|
||||||
|
const outputId = extractId(outputRef);
|
||||||
processes.forEach(targetProcess => {
|
processes.forEach(targetProcess => {
|
||||||
if (targetProcess.id !== sourceProcess.id && targetProcess.inputs.includes(outputId)) {
|
if (targetProcess.id !== sourceProcess.id && includesId(targetProcess.inputs, outputId)) {
|
||||||
flows.push({
|
flows.push({
|
||||||
sourceProcessId: sourceProcess.id,
|
sourceProcessId: sourceProcess.id,
|
||||||
targetProcessId: targetProcess.id,
|
targetProcessId: targetProcess.id,
|
||||||
@@ -91,14 +116,14 @@ export function getArtifactUsage(artifactId: string): {
|
|||||||
asOutput: Process[];
|
asOutput: Process[];
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
asInput: processes.filter(p => p.inputs.includes(artifactId)),
|
asInput: processes.filter(p => includesId(p.inputs, artifactId)),
|
||||||
asOutput: processes.filter(p => p.outputs.includes(artifactId)),
|
asOutput: processes.filter(p => includesId(p.outputs, artifactId)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取工具的使用情况
|
// 获取工具的使用情况
|
||||||
export function getToolUsage(toolId: string): Process[] {
|
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,
|
...process,
|
||||||
knowledgeArea: knowledgeAreaMap.get(process.knowledgeAreaId),
|
knowledgeArea: knowledgeAreaMap.get(process.knowledgeAreaId),
|
||||||
processGroup: processGroupMap.get(process.processGroupId),
|
processGroup: processGroupMap.get(process.processGroupId),
|
||||||
inputDetails: process.inputs.map(id => artifactMap.get(id)).filter(Boolean),
|
inputDetails: process.inputs.map(ref => {
|
||||||
toolDetails: process.tools.map(id => toolMap.get(id)).filter(Boolean),
|
const normalized = normalizeProcessRef(ref);
|
||||||
outputDetails: process.outputs.map(id => artifactMap.get(id)).filter(Boolean),
|
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",
|
"processGroupId": "PG02",
|
||||||
"order": 2,
|
"order": 2,
|
||||||
"inputs": ["A001", "A092", "A005", "A006"],
|
"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"],
|
"outputs": ["A008"],
|
||||||
"w5h1": {
|
"w5h1": {
|
||||||
"who": "项目经理主导,团队参与",
|
"who": "项目经理主导,团队参与",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useParams, Link, useLocation, useNavigate } from 'react-router-dom'
|
import { useParams, Link, useLocation, useNavigate } from 'react-router-dom'
|
||||||
import { motion, AnimatePresence } from 'framer-motion'
|
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 { 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'
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
// 5W1H图标和标签配置
|
// 5W1H图标和标签配置
|
||||||
@@ -237,12 +237,29 @@ export function ProcessDetailPage() {
|
|||||||
transition={{ duration: 0.18 }}
|
transition={{ duration: 0.18 }}
|
||||||
className="divide-y divide-gray-100 dark:divide-gray-700"
|
className="divide-y divide-gray-100 dark:divide-gray-700"
|
||||||
>
|
>
|
||||||
{processDetail.inputs.map((inputId) => {
|
{processDetail.inputDetails?.map((inputDetail: any) => {
|
||||||
const artifact = artifactMap.get(inputId)
|
const hasDetail = inputDetail.detail && inputDetail.detail.length > 0
|
||||||
return (
|
return (
|
||||||
<li key={inputId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
<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">{artifact?.name || inputId}</div>
|
<div className="font-medium text-gray-900 dark:text-white text-sm">{inputDetail.name || inputDetail.id}</div>
|
||||||
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</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>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -290,12 +307,29 @@ export function ProcessDetailPage() {
|
|||||||
transition={{ duration: 0.18 }}
|
transition={{ duration: 0.18 }}
|
||||||
className="divide-y divide-gray-100 dark:divide-gray-700"
|
className="divide-y divide-gray-100 dark:divide-gray-700"
|
||||||
>
|
>
|
||||||
{processDetail.tools.map((toolId) => {
|
{processDetail.toolDetails?.map((toolDetail: any) => {
|
||||||
const tool = toolMap.get(toolId)
|
const hasDetail = toolDetail.detail && toolDetail.detail.length > 0
|
||||||
return (
|
return (
|
||||||
<li key={toolId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
<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">{tool?.name || toolId}</div>
|
<div className="font-medium text-gray-900 dark:text-white text-sm">{toolDetail.name || toolDetail.id}</div>
|
||||||
{tool && <div className="text-xs text-gray-500 dark:text-gray-400">{tool.nameEn}</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>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -343,12 +377,29 @@ export function ProcessDetailPage() {
|
|||||||
transition={{ duration: 0.18 }}
|
transition={{ duration: 0.18 }}
|
||||||
className="divide-y divide-gray-100 dark:divide-gray-700"
|
className="divide-y divide-gray-100 dark:divide-gray-700"
|
||||||
>
|
>
|
||||||
{processDetail.outputs.map((outputId) => {
|
{processDetail.outputDetails?.map((outputDetail: any) => {
|
||||||
const artifact = artifactMap.get(outputId)
|
const hasDetail = outputDetail.detail && outputDetail.detail.length > 0
|
||||||
return (
|
return (
|
||||||
<li key={outputId} className="px-3 py-2 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
<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">{artifact?.name || outputId}</div>
|
<div className="font-medium text-gray-900 dark:text-white text-sm">{outputDetail.name || outputDetail.id}</div>
|
||||||
{artifact && <div className="text-xs text-gray-500 dark:text-gray-400">{artifact.nameEn}</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>
|
</li>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
getProcessDetail,
|
getProcessDetail,
|
||||||
getArtifactUsage,
|
getArtifactUsage,
|
||||||
getToolUsage,
|
getToolUsage,
|
||||||
|
extractId,
|
||||||
} from '@/data'
|
} from '@/data'
|
||||||
|
|
||||||
export function ProcessGraphPage() {
|
export function ProcessGraphPage() {
|
||||||
@@ -92,8 +93,8 @@ export function ProcessGraphPage() {
|
|||||||
// 2. 添加工件节点和关系
|
// 2. 添加工件节点和关系
|
||||||
const usedArtifacts = new Set<string>()
|
const usedArtifacts = new Set<string>()
|
||||||
processes.forEach(p => {
|
processes.forEach(p => {
|
||||||
p.inputs.forEach(id => usedArtifacts.add(id))
|
p.inputs.forEach(ref => usedArtifacts.add(extractId(ref)))
|
||||||
p.outputs.forEach(id => usedArtifacts.add(id))
|
p.outputs.forEach(ref => usedArtifacts.add(extractId(ref)))
|
||||||
})
|
})
|
||||||
|
|
||||||
artifacts.forEach(a => {
|
artifacts.forEach(a => {
|
||||||
@@ -127,7 +128,7 @@ export function ProcessGraphPage() {
|
|||||||
// 3. 添加工具节点和关系
|
// 3. 添加工具节点和关系
|
||||||
const usedTools = new Set<string>()
|
const usedTools = new Set<string>()
|
||||||
processes.forEach(p => {
|
processes.forEach(p => {
|
||||||
p.tools.forEach(id => usedTools.add(id))
|
p.tools.forEach(ref => usedTools.add(extractId(ref)))
|
||||||
})
|
})
|
||||||
|
|
||||||
tools.forEach(t => {
|
tools.forEach(t => {
|
||||||
@@ -160,7 +161,8 @@ export function ProcessGraphPage() {
|
|||||||
// 4. 构建边
|
// 4. 构建边
|
||||||
processes.forEach(p => {
|
processes.forEach(p => {
|
||||||
// 输入关系: Artifact -> Process
|
// 输入关系: Artifact -> Process
|
||||||
p.inputs.forEach(inputId => {
|
p.inputs.forEach(inputRef => {
|
||||||
|
const inputId = extractId(inputRef)
|
||||||
if (addedNodeIds.has(inputId)) {
|
if (addedNodeIds.has(inputId)) {
|
||||||
edges.push({
|
edges.push({
|
||||||
source: inputId,
|
source: inputId,
|
||||||
@@ -176,7 +178,8 @@ export function ProcessGraphPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 输出关系: Process -> Artifact
|
// 输出关系: Process -> Artifact
|
||||||
p.outputs.forEach(outputId => {
|
p.outputs.forEach(outputRef => {
|
||||||
|
const outputId = extractId(outputRef)
|
||||||
if (addedNodeIds.has(outputId)) {
|
if (addedNodeIds.has(outputId)) {
|
||||||
edges.push({
|
edges.push({
|
||||||
source: p.id,
|
source: p.id,
|
||||||
@@ -192,7 +195,8 @@ export function ProcessGraphPage() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 工具关系: Tool -> Process
|
// 工具关系: Tool -> Process
|
||||||
p.tools.forEach(toolId => {
|
p.tools.forEach(toolRef => {
|
||||||
|
const toolId = extractId(toolRef)
|
||||||
if (addedNodeIds.has(toolId)) {
|
if (addedNodeIds.has(toolId)) {
|
||||||
edges.push({
|
edges.push({
|
||||||
source: toolId,
|
source: toolId,
|
||||||
|
|||||||
@@ -36,6 +36,23 @@ export interface Process5W1H {
|
|||||||
how: string; // 如何执行(关键方法)
|
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 {
|
export interface Process {
|
||||||
id: string; // 如 "P4.1"
|
id: string; // 如 "P4.1"
|
||||||
@@ -45,9 +62,9 @@ export interface Process {
|
|||||||
knowledgeAreaId: string; // 所属知识领域ID
|
knowledgeAreaId: string; // 所属知识领域ID
|
||||||
processGroupId: string; // 所属过程组ID
|
processGroupId: string; // 所属过程组ID
|
||||||
order: number; // 在知识领域内的序号
|
order: number; // 在知识领域内的序号
|
||||||
inputs: string[]; // 输入工件ID列表
|
inputs: ProcessRef[]; // 输入工件ID列表(支持明细)
|
||||||
tools: string[]; // 工具与技术ID列表
|
tools: ProcessRef[]; // 工具与技术ID列表(支持明细)
|
||||||
outputs: string[]; // 输出工件ID列表
|
outputs: ProcessRef[]; // 输出工件ID列表(支持明细)
|
||||||
w5h1?: Process5W1H; // 5W1H记忆辅助信息
|
w5h1?: Process5W1H; // 5W1H记忆辅助信息
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user