'use client'; import { useId } from 'react'; import { getLessonDiagrams, getModuleDiagrams, type DiagramEdge, type DiagramNode, type DiagramPoint, type TeachingDiagram, } from './diagram-catalog'; import './teaching-diagrams.css'; export { hasLessonDiagrams } from './diagram-catalog'; const NODE_WIDTH = 210; const NODE_HEIGHT = 104; // Wide glyphs need more space than Latin glyphs. Never clip educational text. function linesFor(text: string, maxUnits: number) { const lines: string[] = []; let line = ''; let units = 0; for (const character of text) { const width = /[\u0020-\u007e]/.test(character) ? 1 : 2; if (units + width > maxUnits && line) { lines.push(line.trim()); line = ''; units = 0; } line += character; units += width; } if (line) lines.push(line.trim()); return lines; } function port( node: DiagramNode, side: NonNullable, ): DiagramPoint { if (side === 'left') return [node.x, node.y + NODE_HEIGHT / 2]; if (side === 'right') return [node.x + NODE_WIDTH, node.y + NODE_HEIGHT / 2]; if (side === 'top') return [node.x + NODE_WIDTH / 2, node.y]; return [node.x + NODE_WIDTH / 2, node.y + NODE_HEIGHT]; } function edgeGeometry(edge: DiagramEdge, nodes: DiagramNode[]) { const source = nodes.find((node) => node.id === edge.from)!; const target = nodes.find((node) => node.id === edge.to)!; const sameRow = source.y === target.y; const sameColumn = source.x === target.x; const right = target.x > source.x; const down = target.y > source.y; const fromSide = edge.fromPort ?? (sameRow ? (right ? 'right' : 'left') : down ? 'bottom' : 'top'); const toSide = edge.toPort ?? (sameRow ? (right ? 'left' : 'right') : down ? 'top' : 'bottom'); const start = port(source, fromSide); const end = port(target, toSide); const middleY = (start[1] + end[1]) / 2; const via = edge.via ?? (sameRow || sameColumn ? [] : ([ [start[0], middleY], [end[0], middleY], ] as DiagramPoint[])); const points: DiagramPoint[] = [start, ...via, end]; const path = points .map(([x, y], index) => `${index ? 'L' : 'M'} ${x} ${y}`) .join(' '); const labelAt = edge.labelAt ?? (sameRow || sameColumn ? ([(start[0] + end[0]) / 2, (start[1] + end[1]) / 2] as DiagramPoint) : ([(start[0] + end[0]) / 2, middleY] as DiagramPoint)); const vertical = sameColumn && !edge.via; return { path, labelAt, vertical }; } function DiagramFigure({ diagram, index, }: { diagram: TeachingDiagram; index: number; }) { const reactId = useId().replace(/:/g, ''); const instanceId = `teaching-${diagram.id}-${reactId}`; const titleId = `${instanceId}-title`; const descriptionId = `${instanceId}-description`; const height = diagram.rows.length * 185 + 60; return (
图解 {String(index + 1).padStart(2, '0')}

{diagram.title}

{diagram.summary}

    {diagram.rows.map((label, row) => (
  1. 区域 {row + 1} {label}
  2. ))}
{/* oxlint-disable jsx-a11y/no-noninteractive-tabindex -- Keyboard users must be able to focus and scroll the full-size diagram. */}
{/* oxlint-disable jsx-a11y/prefer-tag-over-role -- Inline SVG needs image semantics and native title/desc labels. */} {/* oxlint-enable jsx-a11y/no-noninteractive-tabindex, jsx-a11y/prefer-tag-over-role */} {diagram.title} {diagram.summary}{' '} {diagram.edges .map( (edge) => `${diagram.nodes.find((node) => node.id === edge.from)?.title},${edge.label},到${diagram.nodes.find((node) => node.id === edge.to)?.title}。`, ) .join(' ')}{' '} 完整步骤与边界说明见图下文字。 {(['sync', 'async', 'risk'] as const).map((kind) => ( ))} {diagram.rows.map((label, row) => ( 区域 {row + 1} ))} {diagram.edges.map((edge, edgeIndex) => { const geometry = edgeGeometry(edge, diagram.nodes); return ( ); })} {diagram.nodes.map((node) => { const titleLines = linesFor(node.title, 24); const detailLines = linesFor(node.detail, 29); return ( {titleLines.map((line, lineIndex) => ( {line} ))} {detailLines.map((line, lineIndex) => ( {line} ))} ); })} {diagram.edges.map((edge, edgeIndex) => { const { labelAt, vertical } = edgeGeometry(edge, diagram.nodes); const labelLines = linesFor(edge.label, vertical ? 20 : 17); const x = labelAt[0] + (vertical ? 10 : 0); const y = labelAt[1] - (vertical ? (labelLines.length - 1) * 7 : 10 + (labelLines.length - 1) * 14); return ( {labelLines.map((line, lineIndex) => ( {line} ))} ); })}