diff --git a/apps/admin/src/components/JinshujuMatchModal.tsx b/apps/admin/src/components/JinshujuMatchModal.tsx new file mode 100644 index 0000000..1dddaa8 --- /dev/null +++ b/apps/admin/src/components/JinshujuMatchModal.tsx @@ -0,0 +1,565 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Button, Form, Input, Modal, Popconfirm, Select, Spin, Steps, Tag, Typography } from 'antd'; +import { + CloudUploadOutlined, + DeleteOutlined, + EditOutlined, + LinkOutlined, + PlusOutlined, + SaveOutlined, + SearchOutlined, +} from '@ant-design/icons'; +import api from '../api'; +import { message } from '../ui/app-message'; + +const { Text } = Typography; + +// ── Types ── + +interface JinshujuEntryRow { + serialNumber: number; + name: string; + phone: string | null; + suggestedStudent: { + id: number; + name: string; + phone: string | null; + studentNo: string | null; + } | null; +} + +interface StudentOption { + id: number; + name: string; + phone: string | null; + studentNo: string | null; +} + +interface PreviewResponse { + success: boolean; + entries: JinshujuEntryRow[]; + students: StudentOption[]; +} + +interface MatchRule { + id: number; + name: string; + formToken: string; + mappings: Record; + createdAt: string; +} + +interface JinshujuFormField { + key: string; + label: string; + type: string; +} + +type MatchDecision = + | { action: 'match'; matchStudentId: number } + | { action: 'create'; createName: string; createPhone: string } + | { action: 'skip' }; + +// ── Constants ── + +const ROW_HEIGHT = 72; +const LEFT_WIDTH = 260; +const GAP = 80; + +const STUDENT_FIELDS = [ + { key: 'name', label: '姓名' }, + { key: 'phone', label: '手机号' }, + { key: 'idNumber', label: '身份证号' }, + { key: 'gender', label: '性别' }, + { key: 'ethnicity', label: '民族' }, + { key: 'emergencyContact', label: '紧急联系人' }, + { key: 'emergencyPhone', label: '紧急联系电话' }, + { key: 'studentNo', label: '学号' }, +]; + +// ── MatchSelector sub-component ── + +interface MatchSelectorProps { + entry: JinshujuEntryRow; + decision: MatchDecision | undefined; + studentOptions: StudentOption[]; + onChange: (d: MatchDecision) => void; +} + +const MatchSelector: React.FC = ({ entry, decision, studentOptions, onChange }) => { + const action = decision?.action ?? 'skip'; + + if (action === 'match') { + const matchD = decision as { action: 'match'; matchStudentId: number }; + const matchedStudent = studentOptions.find((s) => s.id === matchD.matchStudentId); + return ( +
+ }>已匹配 + + {matchedStudent?.name ?? '未知'} + {matchedStudent?.studentNo && ( + + ({matchedStudent.studentNo}) + + )} + + +
+ ); + } + + if (action === 'create') { + const createD = decision as { action: 'create'; createName: string; createPhone: string }; + return ( +
+ }>将新建 + onChange({ action: 'create', createName: e.target.value, createPhone: createD.createPhone })} /> + onChange({ action: 'create', createName: createD.createName, createPhone: e.target.value })} /> + +
+ ); + } + + return ( +
+ setName(e.target.value)} + style={{ marginBottom: 12 }} /> + 选择金数据字段映射到学生资料 + {STUDENT_FIELDS.map((sf) => ( +
+ {sf.label} + + + + + + + + + + + ); + + const renderRuleStep = () => ( +
+
+ 选择匹配规则 + + 已连接表单「{formName}」,共 {formFields.length} 个字段。选择已有规则或新建字段映射。 + +
+ +
+