157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
import React from 'react';
|
||
import { Button, Typography } from 'antd';
|
||
import MatchSelector from './MatchSelector';
|
||
import { GAP, LEFT_WIDTH, ROW_HEIGHT } from './JinshujuMatchModal.types';
|
||
import type { JinshujuEntryRow, MatchDecision, StudentOption } from './JinshujuMatchModal.types';
|
||
|
||
const { Text } = Typography;
|
||
|
||
interface MatchStepProps {
|
||
entries: JinshujuEntryRow[];
|
||
studentOptions: StudentOption[];
|
||
getDecision: (serial: number) => MatchDecision | undefined;
|
||
onDecisionChange: (serial: number, d: MatchDecision) => void;
|
||
onClear: () => void;
|
||
leftRef: React.RefObject<HTMLDivElement | null>;
|
||
rightRef: React.RefObject<HTMLDivElement | null>;
|
||
onScroll: (source: 'left' | 'right') => void;
|
||
total: number;
|
||
matched: number;
|
||
}
|
||
|
||
const MatchStep: React.FC<MatchStepProps> = ({
|
||
entries,
|
||
studentOptions,
|
||
getDecision,
|
||
onDecisionChange,
|
||
onClear,
|
||
leftRef,
|
||
rightRef,
|
||
onScroll,
|
||
total,
|
||
matched,
|
||
}) => {
|
||
const svgHeight = entries.length * ROW_HEIGHT;
|
||
const lines: React.ReactNode[] = [];
|
||
entries.forEach((entry, i) => {
|
||
const y = i * ROW_HEIGHT + ROW_HEIGHT / 2;
|
||
const d = getDecision(entry.serialNumber);
|
||
const isMatched = d?.action === 'match';
|
||
const color = isMatched ? '#1677ff' : '#d9d9d9';
|
||
lines.push(
|
||
<line
|
||
key={entry.serialNumber}
|
||
x1={LEFT_WIDTH}
|
||
y1={y}
|
||
x2={LEFT_WIDTH + GAP}
|
||
y2={y}
|
||
stroke={color}
|
||
strokeWidth={isMatched ? 2 : 1}
|
||
strokeDasharray={isMatched ? undefined : '4 4'}
|
||
opacity={isMatched ? 0.7 : 0.3}
|
||
/>,
|
||
);
|
||
});
|
||
|
||
return (
|
||
<div style={{ position: 'relative' }}>
|
||
<div
|
||
style={{
|
||
marginBottom: 12,
|
||
display: 'flex',
|
||
justifyContent: 'space-between',
|
||
alignItems: 'center',
|
||
}}
|
||
>
|
||
<Text type="secondary">
|
||
共 {total} 条,已匹配 {matched} 条
|
||
</Text>
|
||
<Button size="small" onClick={onClear}>
|
||
清除全部匹配
|
||
</Button>
|
||
</div>
|
||
<div style={{ display: 'flex', position: 'relative' }}>
|
||
<svg
|
||
style={{
|
||
position: 'absolute',
|
||
top: 0,
|
||
left: 0,
|
||
width: LEFT_WIDTH + GAP,
|
||
height: svgHeight,
|
||
pointerEvents: 'none',
|
||
zIndex: 1,
|
||
}}
|
||
>
|
||
{lines}
|
||
</svg>
|
||
<div
|
||
ref={leftRef}
|
||
onScroll={() => onScroll('left')}
|
||
style={{ width: LEFT_WIDTH, maxHeight: 480, overflowY: 'auto', flexShrink: 0 }}
|
||
>
|
||
{entries.map((entry, i) => {
|
||
const d = getDecision(entry.serialNumber);
|
||
const isMatched = d?.action === 'match';
|
||
return (
|
||
<div
|
||
key={entry.serialNumber}
|
||
style={{
|
||
height: ROW_HEIGHT,
|
||
padding: '8px 12px',
|
||
borderBottom: '1px solid #f0f0f0',
|
||
display: 'flex',
|
||
flexDirection: 'column',
|
||
justifyContent: 'center',
|
||
background: isMatched ? '#f6ffed' : i % 2 === 0 ? '#fafafa' : '#fff',
|
||
borderLeft: isMatched ? '3px solid #1677ff' : '3px solid transparent',
|
||
}}
|
||
>
|
||
<Text strong style={{ fontSize: 13 }}>
|
||
{entry.name || <Text type="secondary">无姓名</Text>}
|
||
</Text>
|
||
{entry.phone && (
|
||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||
{entry.phone}
|
||
</Text>
|
||
)}
|
||
<Text type="secondary" style={{ fontSize: 11 }}>
|
||
#{entry.serialNumber}
|
||
</Text>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
<div style={{ width: GAP, flexShrink: 0 }} />
|
||
<div
|
||
ref={rightRef}
|
||
onScroll={() => onScroll('right')}
|
||
style={{ flex: 1, maxHeight: 480, overflowY: 'auto' }}
|
||
>
|
||
{entries.map((entry) => (
|
||
<div
|
||
key={entry.serialNumber}
|
||
style={{
|
||
height: ROW_HEIGHT,
|
||
padding: '8px 12px',
|
||
borderBottom: '1px solid #f0f0f0',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
}}
|
||
>
|
||
<MatchSelector
|
||
entry={entry}
|
||
decision={getDecision(entry.serialNumber)}
|
||
studentOptions={studentOptions}
|
||
onChange={(newD) => onDecisionChange(entry.serialNumber, newD)}
|
||
/>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default MatchStep;
|