userPatients(); // 搜索功能 if ($request->filled('search')) { $search = $request->input('search'); $query->where(function ($q) use ($search) { $q->where('name', 'like', "%{$search}%") ->orWhere('phone', 'like', "%{$search}%") ->orWhere('address', 'like', "%{$search}%"); }); } // 诊断筛选 if ($request->filled('diagnosis')) { $query->where('diagnosis', 'like', "%{$request->input('diagnosis')}%"); } $patients = $query->orderBy('discharge_date', 'desc')->paginate(20); return view('patients.index', compact('patients')); } /** * 显示需要随访提醒的患者 */ public function reminders(Request $request) { $patients = $this->userPatients()->get(); // 筛选需要随访的患者 $reminders = $patients->filter(function ($patient) { return !$patient->isCompleted(); })->map(function ($patient) { $nextDate = $patient->getNextFollowUpDate(); return [ 'patient' => $patient, 'next_follow_up_date' => $nextDate, 'next_follow_up_number' => $patient->getNextFollowUpNumber(), 'status' => $patient->getFollowUpStatus(), 'needs_attention' => $patient->needsFollowUp(), 'days_until' => $nextDate ? Carbon::today()->diffInDays($nextDate, false) : null, ]; })->sortBy('days_until'); // 筛选类型 $filter = $request->input('filter', 'all'); if ($filter === 'overdue') { $reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] < 0); } elseif ($filter === 'today') { $reminders = $reminders->filter(fn($r) => $r['days_until'] === 0); } elseif ($filter === 'upcoming') { $reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] > 0 && $r['days_until'] <= 7); } // 统计数据 $stats = [ 'total' => $this->userPatients()->count(), 'overdue' => $patients->filter(fn($p) => !$p->isCompleted() && $p->getNextFollowUpDate()?->lt(Carbon::today()))->count(), 'today' => $patients->filter(fn($p) => !$p->isCompleted() && $p->getNextFollowUpDate()?->isToday())->count(), 'upcoming' => $patients->filter(function ($p) { if ($p->isCompleted()) return false; $next = $p->getNextFollowUpDate(); if (!$next) return false; $diff = Carbon::today()->diffInDays($next, false); return $diff > 0 && $diff <= 7; })->count(), 'completed' => $patients->filter(fn($p) => $p->isCompleted())->count(), ]; return view('patients.reminders', compact('reminders', 'stats', 'filter')); } /** * 显示导入页面 */ public function showImport() { return view('patients.import'); } /** * 处理Excel导入 */ public function import(Request $request) { $request->validate([ 'file' => 'required|file|mimes:xlsx,xls,csv|max:10240', ]); $file = $request->file('file'); $path = $file->getRealPath(); try { $data = $this->parseExcel($path, $file->getClientOriginalExtension()); $imported = 0; $errors = []; $headerSkipped = false; foreach ($data as $index => $row) { try { // 跳过空行 if (empty($row) || !is_array($row)) { continue; } $firstCell = trim((string)($row[0] ?? '')); // 跳过表头行(第一行或包含"姓名"的行) if (!$headerSkipped && ($index === 0 || mb_strpos($firstCell, '姓名') !== false)) { $headerSkipped = true; continue; } // 跳过空行 if (empty($firstCell)) { continue; } // 解析数据 $patientData = $this->parseRow($row); if ($patientData) { // 关联到当前用户 $patientData['user_id'] = Auth::id(); Patient::create($patientData); $imported++; } } catch (\Exception $e) { $errors[] = "第 " . ($index + 1) . " 行导入失败: " . $e->getMessage(); } } $message = "成功导入 {$imported} 条记录。"; if (count($errors) > 0) { $message .= " " . count($errors) . " 条记录导入失败。"; } return redirect()->route('patients.index') ->with('success', $message) ->with('errors', $errors); } catch (\Exception $e) { return back()->withErrors(['file' => '文件解析失败: ' . $e->getMessage()]); } } /** * 解析Excel/CSV文件 (使用 xlswriter 扩展) */ private function parseExcel(string $path, string $extension): array { $data = []; if ($extension === 'csv') { // 检测文件编码并转换 $content = file_get_contents($path); $encoding = mb_detect_encoding($content, ['UTF-8', 'GBK', 'GB2312', 'BIG5'], true); if ($encoding && $encoding !== 'UTF-8') { $content = mb_convert_encoding($content, 'UTF-8', $encoding); $tempPath = sys_get_temp_dir() . '/converted_' . time() . '.csv'; file_put_contents($tempPath, $content); $path = $tempPath; } $handle = fopen($path, 'r'); // 跳过 BOM $bom = fread($handle, 3); if ($bom !== "\xEF\xBB\xBF") { rewind($handle); } while (($row = fgetcsv($handle)) !== false) { $data[] = $row; } fclose($handle); // 清理临时文件 if (isset($tempPath) && file_exists($tempPath)) { @unlink($tempPath); } } elseif ($extension === 'xls') { // .xls 是旧版 Excel 格式,xlswriter 不支持读取 // 尝试使用 PhpSpreadsheet 或提示用户转换格式 if (class_exists(\PhpOffice\PhpSpreadsheet\IOFactory::class)) { $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($path); $worksheet = $spreadsheet->getActiveSheet(); foreach ($worksheet->getRowIterator() as $row) { $rowData = []; $cellIterator = $row->getCellIterator(); $cellIterator->setIterateOnlyExistingCells(false); foreach ($cellIterator as $cell) { $rowData[] = $cell->getValue(); } $data[] = $rowData; } } else { throw new \Exception('不支持 .xls 格式(Excel 97-2003)。请将文件另存为 .xlsx 格式(Excel 2007+)或 .csv 格式后重新上传。操作方法:在 Excel 中打开文件 → 文件 → 另存为 → 选择"Excel 工作簿 (*.xlsx)"或"CSV UTF-8"'); } } else { // 使用 xlswriter 扩展解析 xlsx if (!class_exists(\Vtiful\Kernel\Excel::class)) { throw new \Exception('xlswriter 扩展未安装,请安装后重试'); } $config = ['path' => sys_get_temp_dir()]; $excel = new \Vtiful\Kernel\Excel($config); // 复制文件到临时目录 $tempFile = sys_get_temp_dir() . '/' . uniqid('import_') . '.' . $extension; copy($path, $tempFile); // 读取Excel数据 $data = $excel->openFile(basename($tempFile)) ->openSheet() ->getSheetData(); // 如果没有读取到数据,可能是格式问题 if (empty($data)) { throw new \Exception('无法读取 Excel 文件内容。请确保文件是有效的 .xlsx 格式,或尝试另存为 .csv 格式后重新上传。'); } // 释放 Excel 对象 unset($excel); // 清理临时文件 (使用 @ 抑制 Windows 上的文件锁定警告) if (file_exists($tempFile)) { @unlink($tempFile); } } return $data; } /** * 解析单行数据 */ private function parseRow(array $row): ?array { // 期望的列顺序: 姓名, 性别, 年龄, 出院诊断, 转诊时间, 户籍地址, 联系方式, 备注 if (count($row) < 5) { return null; } $name = trim((string)($row[0] ?? '')); if (empty($name) || mb_strpos($name, '姓名') !== false) { return null; } // 解析日期 $dateValue = $row[4] ?? ''; $dischargeDate = $this->parseDate($dateValue); if (!$dischargeDate) { throw new \Exception("日期格式无效: {$dateValue}"); } // 处理年龄(可能是数字或字符串) $age = $row[2] ?? 0; if (is_string($age)) { $age = (int)preg_replace('/[^0-9]/', '', $age); } // 处理电话号码(可能是科学计数法或数字) $phone = $row[6] ?? ''; if (is_numeric($phone)) { $phone = number_format($phone, 0, '', ''); } return [ 'name' => $name, 'gender' => trim((string)($row[1] ?? '未知')), 'age' => (int)$age, 'diagnosis' => trim((string)($row[3] ?? '')), 'discharge_date' => $dischargeDate, 'address' => trim((string)($row[5] ?? '')), 'phone' => trim((string)$phone), 'remark' => trim((string)($row[7] ?? '')), 'follow_up_count' => 0, ]; } /** * 解析日期 */ private function parseDate($value): ?string { if (empty($value)) { return null; } $value = trim((string)$value); // Excel 日期序列号(纯数字且大于25569) if (is_numeric($value) && (float)$value > 25569 && (float)$value < 50000) { // Excel日期是从1900年1月1日开始的天数 // 25569 是 1970-01-01 的Excel序列号 $unixTimestamp = ((float)$value - 25569) * 86400; return date('Y-m-d', (int)$unixTimestamp); } // 字符串格式日期 - 各种格式 $formats = [ 'Y.m.d', // 2025.12.01 'Y-m-d', // 2025-12-01 'Y/m/d', // 2025/12/01 'Y.n.j', // 2025.1.1 (无前导零) 'Y-n-j', // 2025-1-1 'Y/n/j', // 2025/1/1 'd/m/Y', // 01/12/2025 'm/d/Y', // 12/01/2025 ]; foreach ($formats as $format) { $date = \DateTime::createFromFormat($format, $value); if ($date) { // 验证日期是否有效 $errors = \DateTime::getLastErrors(); if (empty($errors['warning_count']) && empty($errors['error_count'])) { return $date->format('Y-m-d'); } } } // 尝试通过 strtotime(处理更多格式) $timestamp = strtotime(str_replace('.', '-', $value)); if ($timestamp && $timestamp > 0) { return date('Y-m-d', $timestamp); } return null; } /** * 导出提醒列表 (使用 xlswriter 生成 Excel) */ public function export(Request $request) { $filter = $request->input('filter', 'all'); $patients = $this->userPatients()->get(); // 筛选需要随访的患者 $reminders = $patients->filter(function ($patient) { return !$patient->isCompleted(); })->map(function ($patient) { $nextDate = $patient->getNextFollowUpDate(); return [ 'patient' => $patient, 'next_follow_up_date' => $nextDate, 'next_follow_up_number' => $patient->getNextFollowUpNumber(), 'status' => $patient->getFollowUpStatus(), 'days_until' => $nextDate ? Carbon::today()->diffInDays($nextDate, false) : null, ]; })->sortBy('days_until'); // 根据筛选条件过滤 if ($filter === 'overdue') { $reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] < 0); } elseif ($filter === 'today') { $reminders = $reminders->filter(fn($r) => $r['days_until'] === 0); } elseif ($filter === 'upcoming') { $reminders = $reminders->filter(fn($r) => $r['days_until'] !== null && $r['days_until'] > 0 && $r['days_until'] <= 7); } // 使用 xlswriter 生成 Excel 文件 $config = ['path' => sys_get_temp_dir()]; $filename = '随访提醒_' . date('Y-m-d_His') . '.xlsx'; $excel = new \Vtiful\Kernel\Excel($config); // 创建文件并设置表头 $fileHandle = $excel->fileName($filename) ->header(['姓名', '性别', '年龄', '出院诊断', '转诊时间', '下次随访日期', '第几次随访', '状态', '户籍地址', '联系方式', '备注']); // 设置表头样式 $headerStyle = $fileHandle->getHandle(); $format = new \Vtiful\Kernel\Format($headerStyle); $boldFormat = $format->bold()->toResource(); // 添加数据行 $rows = []; foreach ($reminders as $reminder) { $p = $reminder['patient']; $rows[] = [ $p->name, $p->gender, $p->age, $p->diagnosis, $p->discharge_date->format('Y-m-d'), $reminder['next_follow_up_date']?->format('Y-m-d') ?? '', '第' . $reminder['next_follow_up_number'] . '次', $reminder['status'], $p->address ?? '', $p->phone ?? '', $p->remark ?? '', ]; } $fileHandle->data($rows)->output(); $filePath = $config['path'] . '/' . $filename; return Response::download($filePath, $filename, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ])->deleteFileAfterSend(true); } /** * 标记已随访 */ public function markFollowedUp(Patient $patient) { // 验证患者属于当前用户 if ($patient->user_id !== Auth::id()) { abort(403, '无权操作此患者'); } if (!$patient->isCompleted()) { $patient->follow_up_count += 1; $patient->last_follow_up_date = Carbon::today(); $patient->save(); } return back()->with('success', "{$patient->name} 已标记为完成第 {$patient->follow_up_count} 次随访"); } /** * 删除患者 */ public function destroy(Patient $patient) { // 验证患者属于当前用户 if ($patient->user_id !== Auth::id()) { abort(403, '无权操作此患者'); } $name = $patient->name; $patient->delete(); return back()->with('success', "已删除患者: {$name}"); } /** * 下载导入模板 (使用 xlswriter 生成 Excel) */ public function downloadTemplate() { $config = ['path' => sys_get_temp_dir()]; $filename = '导入模板.xlsx'; $excel = new \Vtiful\Kernel\Excel($config); // 创建文件 $fileHandle = $excel->fileName($filename) ->header(['姓名', '性别', '年龄', '出院诊断', '转诊时间', '户籍地址', '联系方式', '备注']); // 添加示例数据 $data = [ ['张三', '男', 65, '脑卒中', '2025.12.01', '北京市朝阳区', '13800138000', '常住'], ['李四', '女', 70, '慢性肾脏病', '2025.11.15', '上海市浦东新区', '13900139000', ''], ['王五', '男', 58, '心肌梗塞', '2025.10.20', '广州市天河区', '13700137000', ''], ]; $fileHandle->data($data)->output(); $filePath = $config['path'] . '/' . $filename; return Response::download($filePath, $filename, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ])->deleteFileAfterSend(true); } }