在提供的代码中,日期选择器回显问题可能是由于 `moment` 对象处理不当或者数据传递问题导致的。以下是修改后的完整代码,主要对日期格式化和数据处理进行了检查和修正:
```jsx
import React, { useState, useRef, useImperativeHandle, forwardRef, useEffect } from 'react';
import { Form, Input, Select, DatePicker, Segmented, Button, Row, Col, message, Radio, Modal } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import './CustomCheckboxStyle.css';
import { formatCustomDateTime } from '@/components/formatDateTime';
import { postStudent_archives, postStudent_archives_info } from '@/api/auth';
import moment from 'moment';
import { useOSS } from '@/hooks/useOSS';
// 定义接口数据类型
interface ExamRecord {
id?: string;
file_url: string;
file_name: string;
}
interface TargetScores {
toefl?: {
reading_score?: string | number | null;
listening_score?: string | number | null;
writing_score?: string | number | null;
speaking_score?: string | number | null;
achieved_date?: string | null;
};
ielts?: {
reading_score?: string | number | null;
listening_score?: string | number | null;
writing_score?: string | number | null;
speaking_score?: string | number | null;
achieved_date?: string | null;
};
}
interface StudentArchiveData {
start_date?: string | null;
end_date?: string | null;
first_english_date?: string | null;
is_school?: boolean | null;
study_abroad_purpose?: string | null;
use_textbook?: string[];
target?: TargetScores;
past_exam_record_data?: ExamRecord[];
}
const { Option } = Select;
export interface ChildMethods {
handleSave: () => void;
}
const StudentProfileSection = forwardRef<ChildMethods>((props, ref) => {
// 状态管理
const [examType, setExamType] = useState<string>('托福');
const { uploadFile, getFileUrl } = useOSS();
const [form] = Form.useForm();
const fileInputRef = useRef<HTMLInputElement>(null);
const [reselectIndex, setReselectIndex] = useState<number | null>(null); // 记录正在重新选择的文件索引
// 文件管理状态
const [selectedFiles, setSelectedFiles] = useState<(ExamRecord & { isNew?: boolean; isUpdated?: boolean })[]>([]);
const [updateFiles, setUpdateFiles] = useState<ExamRecord[]>([]);
const [deleteFiles, setDeleteFiles] = useState<string[]>([]); // 只存储要删除的ID
// 添加教材
const [textbook, setTextbook] = useState<string | null>(null);
const [textbookVisible, setTextbookVisible] = useState(false);
const purposeOptions = [
{ label: "入学", value: "enter_school" },
{ label: "毕业", value: "graduation" },
{ label: "职业", value: "profession" },
{ label: "移民", value: "immigration" },
{ label: "兴趣", value: "interest" },
];
// 保存表单数据到后端
const handleSave = async () => {
try {
const values = await form.validateFields();
const processedValues: any = { ...values };
processedValues.add_past_exam_record_data = selectedFiles
.filter(file => file.isNew && !file.isUpdated)
.map(({ id, isNew, isUpdated, ...rest }) => rest);
processedValues.update_past_exam_record_data = updateFiles;
processedValues.delete_past_exam_record_data = deleteFiles;
if (processedValues.target?.toefl?.achieved_date) {
processedValues.target.toefl.achieved_date = formatCustomDateTime(
processedValues.target.toefl.achieved_date
);
}
if (processedValues.target?.ielts?.achieved_date) {
processedValues.target.ielts.achieved_date = formatCustomDateTime(
processedValues.target.ielts.achieved_date
);
}
if (processedValues.target) {
if (processedValues.target.toefl) {
console.log('托福');
}
if (processedValues.target.ielts) {
console.log('雅思');
}
}
if (processedValues.first_english_date) {
processedValues.first_english_date = formatCustomDateTime(
processedValues.first_english_date
);
}
if (processedValues.learning_cycle && Array.isArray(processedValues.learning_cycle)) {
const [startDate, endDate] = processedValues.learning_cycle;
if (startDate) {
processedValues.start_date = formatCustomDateTime(startDate);
}
if (endDate) {
const endDateWithEndTime = new Date(endDate);
endDateWithEndTime.setHours(23, 59, 59, 999);
processedValues.end_date = formatCustomDateTime(endDateWithEndTime);
}
delete processedValues.learning_cycle;
}
// 提交数据
const response = await postStudent_archives(processedValues);
console.log('接口响应:', response);
console.log('提交的数据:', processedValues);
if (response.code === 0) {
// 重置文件状态
setSelectedFiles(prev => prev.map(file => ({
...file,
isNew: false,
isUpdated: false
})));
setUpdateFiles([]);
setDeleteFiles([]);
}
} catch (error: any) {
console.error('保存表单失败:', error);
}
};
// 暴露方法给父组件
useImperativeHandle(ref, () => ({
handleSave
}));
const getDetail = async () => {
try {
const response = await postStudent_archives_info();
if (response.code === 0) {
const data = (response?.data || {}) as StudentArchiveData;
const formValues: Record<string, any> = {};
if (data.is_school !== undefined && data.is_school !== null) {
formValues.is_school = data.is_school;
}
if (data.study_abroad_purpose) {
formValues.study_abroad_purpose = data.study_abroad_purpose;
}
if (data.use_textbook && Array.isArray(data.use_textbook)) {
formValues.use_textbook = data.use_textbook;
}
if (data.first_english_date) {
formValues.first_english_date = moment(data.first_english_date, 'YYYY-MM-DD');
}
if (data.start_date || data.end_date) {
formValues.learning_cycle = [
data.start_date ? moment(data.start_date, 'YYYY-MM-DD') : null,
data.end_date ? moment(data.end_date, 'YYYY-MM-DD') : null
];
}
if (data.target) {
formValues.target = {};
if (data.target.toefl) {
formValues.target.toefl = {
...data.target.toefl,
achieved_date: data.target.toefl.achieved_date
? moment(data.target.toefl.achieved_date, 'YYYY-MM-DD')
: null
};
// 根据回显数据设置 examType
if (!data.target.ielts) {
setExamType('托福');
}
}
if (data.target.ielts) {
formValues.target.ielts = {
...data.target.ielts,
achieved_date: data.target.ielts.achieved_date
? moment(data.target.ielts.achieved_date, 'YYYY-MM-DD')
: null
};
setExamType('雅思');
}
}
// 过往考试记录回显
if (data.past_exam_record_data && Array.isArray(data.past_exam_record_data)) {
setSelectedFiles(data.past_exam_record_data.map(file => ({
...file,
isNew: false,
isUpdated: false
})));
}
// 填充表单
await form.setFieldsValue(formValues);
message.success('数据加载成功!');
}
} catch (error: any) {
console.error('获取详情失败:', error);
} finally {
message.destroy();
}
};
useEffect(() => {
getDetail();
}, []);
// 文件删除处理
const handleDeleteFile = (index: number) => {
const deletedFile = selectedFiles[index];
if (deletedFile.id) {
setUpdateFiles(prev => prev.filter(file => file.id !== deletedFile.id));
setDeleteFiles(prev => [...prev.filter(id => id !== deletedFile.id), deletedFile.id!]);
}
setSelectedFiles(prev => prev.filter((_, i) => i !== index));
};
const handleChooseFile = () => {
fileInputRef.current?.click();
};
const handleReselectFile = (index: number) => {
setReselectIndex(index);
fileInputRef.current?.click();
};
// 处理文件上传和重新选择
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const files = e.target.files;
if (!files || files.length === 0) return;
const fileArray = Array.from(files);
message.loading('正在处理文件');
try {
// 重新选择文件
if (reselectIndex !== null) {
const file = fileArray[0]; // 重新选择只处理单个文件
const result = await uploadFile(file, 'materials');
const originalFile = selectedFiles[reselectIndex];
const updatedFile = {
...originalFile,
file_url: result.Key,
file_name: file.name,
isUpdated: !!originalFile.id, // 有id的文件才算更新
isNew: !originalFile.id // 无id的文件保持新增状态
};
// 更新 selectedFiles
const newFiles = [...selectedFiles];
newFiles[reselectIndex] = updatedFile;
setSelectedFiles(newFiles);
// 已保存文件加入 updateFiles
if (updatedFile.id) {
const filteredUpdates = updateFiles.filter(f => f.id !== updatedFile.id);
setUpdateFiles([...filteredUpdates, {
id: updatedFile.id,
file_url: updatedFile.file_url,
file_name: updatedFile.file_name
}]);
// 确保删除列表中移除
setDeleteFiles(prev => prev.filter(id => id !== updatedFile.id));
}
message.success('文件已更新!');
setReselectIndex(null);
}
// 新增文件
else {
const newFiles: (ExamRecord & { isNew: boolean; isUpdated?: boolean })[] = [];
for (const file of fileArray) {
const result = await uploadFile(file, 'materials');
newFiles.push({
file_url: result.Key,
file_name: file.name,
isNew: true
});
}
setSelectedFiles(prev => [...prev, ...newFiles]);
message.success(`成功上传 ${newFiles.length} 个文件!`);
}
} catch (error: any) {
console.error('文件处理失败:', error);
message.error(`处理失败:${error.message || '文件处理异常'}`);
} finally {
message.destroy();
if (e.target) e.target.value = '';
}
};
return (
<>
<Form form={form} layout="vertical">
<div className="flex items-center mt-12 mb-6">
<div className="w-1 h-5 bg-[#4A3C8C] rounded-full mr-3"></div>
<span className="text-xl font-medium text-[#333333]">
个人学习目标
</span>
</div>
<Form.Item noStyle>
<Segmented
options={["托福", "雅思"]}
onChange={(value) => setExamType(value as string)}
style={{ marginBottom: 24 }}
size="large"
value={examType}
/>
</Form.Item>
{/* 托福目标分数 */}
{examType === '托福' && (
<>
<Form.Item label="目标达成日期" name={['target', 'toefl', 'achieved_date']}>
<DatePicker
className="w-full md:w-64"
size="large"
// showTime
format="YYYY-MM-DD"
/>
</Form.Item>
<div className="flex flex-wrap gap-5 mt-6">
<div className="flex-1">
<Form.Item label="阅读目标分数" name={['target', 'toefl', 'reading_score']}>
<Input type="number" placeholder="请输入分数" size="large" />
</Form.Item>
</div>
<div className="flex-1">
<Form.Item label="听力目标分数" name={['target', 'toefl', 'listening_score']}>
<Input type="number" placeholder="请输入分数" size="large" />
</Form.Item>
</div>
<div className="flex-1">
<Form.Item label="写作目标分数" name={['target', 'toefl', 'writing_score']}>
<Input type="number" placeholder="请输入分数" size="large" />
</Form.Item>
</div>
<div className="flex-1">
<Form.Item label="口语目标分数" name={['target', 'toefl', 'speaking_score']}>
<Input type="number" placeholder="请输入分数" size="large" />
</Form.Item>
</div>
</div>
</>
)}
{/* 雅思目标分数 */}
{examType === '雅思' && (
<>
<Form.Item label="目标达成日期" name={['target', 'ielts', 'achieved_date']}>
<DatePicker
className="w-full md:w-64"
size="large"
// showTime={{ format: 'HH:mm:ss' }}
format="YYYY-MM-DD"
/>
</Form.Item>
<div className="flex flex-wrap gap-5 mt-6">
<div className="flex-1">
<Form.Item label="阅读目标分数" name={['target', 'ielts', 'reading_score']}>
<Input type="number" step="0.5" placeholder="请输入分数" size="large" />
</Form.Item>
</div>
<div className="flex-1">
<Form.Item label="听力目标分数" name={['target', 'ielts', 'listening_score']}>
<Input type="number" step="0.5" placeholder="请输入分数" size="large" />
</Form.Item>
</div>
<div className="flex-1">
<Form.Item label="写作目标分数" name={['target', 'ielts', 'writing_score']}>
<Input type="number" step="0.5" placeholder="请输入分数" size="large" />
</Form.Item>
</div>
<div className="flex-1">
<Form.Item label="口语目标分数" name={['target', 'ielts', 'speaking_score']}>
<Input type="number" step="0.5" placeholder="请输入分数" size="large" />
</Form.Item>
</div>
</div>
</>
)}
<div className="flex items-center mt-8 mb-6">
<div className="w-1 h-5 bg-[#4A3C8C] rounded-full mr-3"></div>
<span className="text-xl font-medium text-[#333333]">
过往考试记录
</span>
</div>
<div className="flex overflow-x-auto pb-4 -mx-2 px-2">
<div className="flex flex-nowrap gap-4">
{selectedFiles.map((item, index) => (
<div className="flex-shrink-0 relative" key={index}>
<div className='w-[194px] h-[194px] border-2 rounded-[8px] flex items-center justify-center bg-gray-50 relative'>
<span className="text-gray-400">{item.file_name || `材料${index + 1}`}</span>
<div className="absolute inset-0 bg-black bg-opacity-50 rounded-[8px] flex flex-col items-center justify-center gap-2 opacity-0 hover:opacity-100 transition-opacity">
<Button
type="default"
size="small"
onClick={async () => {
if (item.file_url) {
const fileExtension = item.file_name.split