Files
gongxue-base/apps/admin/src/components/MatchStep.tsx

157 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;