reminder/resources/views/patients/index.blade.php

727 lines
26 KiB
PHP
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.

@extends('layouts.app')
@section('title', '患者列表 - 病例回访提醒系统')
@section('content')
<style>
/* 防止横向滚动 */
.card {
overflow-x: hidden;
}
/* ==================== PC端表格优化 ==================== */
.patient-table {
width: 100%;
border-collapse: collapse;
}
.patient-table thead {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
}
.patient-table th {
padding: 14px 12px;
text-align: left;
font-weight: 600;
font-size: 13px;
color: var(--color-text-secondary);
border-bottom: 2px solid var(--color-border);
white-space: nowrap;
}
.patient-table td {
padding: 16px 12px;
border-bottom: 1px solid var(--color-border-light);
vertical-align: middle;
}
.patient-table tbody tr:hover {
background: var(--color-bg-hover);
}
.patient-table tbody tr:last-child td {
border-bottom: none;
}
/* 患者信息单元格 */
.patient-cell {
display: flex;
align-items: center;
gap: 12px;
min-width: 140px;
}
.patient-cell-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
color: white;
flex-shrink: 0;
}
.patient-cell-avatar.male {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
}
.patient-cell-avatar.female {
background: linear-gradient(135deg, #ec4899 0%, #f472b6 100%);
}
.patient-cell-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.patient-cell-name {
font-weight: 600;
color: var(--color-text);
font-size: 14px;
}
.patient-cell-meta {
font-size: 12px;
color: var(--color-text-muted);
}
/* 诊断标签 */
.diagnosis-cell {
display: inline-flex;
align-items: center;
padding: 4px 10px;
background: var(--color-primary-light);
color: var(--color-primary);
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
/* 进度条 */
.progress-cell {
display: flex;
align-items: center;
gap: 10px;
min-width: 100px;
}
.progress-bar-mini {
flex: 1;
height: 6px;
background: var(--color-border);
border-radius: 3px;
overflow: hidden;
min-width: 50px;
}
.progress-bar-mini-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-success), #34d399);
border-radius: 3px;
}
.progress-text {
font-size: 13px;
color: var(--color-text-secondary);
font-weight: 500;
white-space: nowrap;
}
/* 电话链接 */
.phone-link {
color: var(--color-primary);
text-decoration: none;
font-weight: 500;
white-space: nowrap;
}
.phone-link:hover {
text-decoration: underline;
}
/* 备注 */
.remark-cell {
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--color-warning);
font-weight: 500;
}
/* 操作按钮组 */
.table-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
}
.table-actions .btn-icon {
width: 34px;
height: 34px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 8px;
border: 1px solid var(--color-border);
background: var(--color-bg-card);
cursor: pointer;
transition: all 0.2s;
}
.table-actions .btn-icon:hover {
background: var(--color-bg-hover);
}
.table-actions .btn-icon.success {
color: var(--color-success);
border-color: var(--color-success);
}
.table-actions .btn-icon.success:hover {
background: var(--color-success-light);
}
.table-actions .btn-icon.danger {
color: var(--color-danger);
border-color: var(--color-danger);
}
.table-actions .btn-icon.danger:hover {
background: var(--color-danger-light);
}
.table-actions .btn-icon svg {
width: 16px;
height: 16px;
}
/* ==================== 移动端卡片优化 ==================== */
.mobile-patient-list {
display: none;
}
.mobile-patient-card {
background: var(--color-bg-card);
border: 1px solid var(--color-border);
border-radius: 16px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
overflow: hidden;
}
.mobile-patient-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 16px;
}
.mobile-patient-info {
display: flex;
align-items: center;
gap: 14px;
}
.mobile-patient-avatar {
width: 50px;
height: 50px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 18px;
color: white;
flex-shrink: 0;
}
.mobile-patient-avatar.male {
background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
}
.mobile-patient-avatar.female {
background: linear-gradient(135deg, #ec4899 0%, #f472b6 100%);
}
.mobile-patient-name {
font-weight: 600;
font-size: 17px;
color: var(--color-text);
margin-bottom: 4px;
}
.mobile-patient-meta {
font-size: 14px;
color: var(--color-text-secondary);
}
.mobile-patient-body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 16px;
padding: 12px 14px;
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-radius: 12px;
overflow: hidden;
}
.mobile-info-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.mobile-info-label {
font-size: 12px;
color: var(--color-text-muted);
}
.mobile-info-value {
font-size: 14px;
color: var(--color-text);
font-weight: 500;
word-break: break-all;
overflow-wrap: break-word;
}
.mobile-info-value a {
color: var(--color-primary);
text-decoration: none;
word-break: break-all;
}
.mobile-patient-progress {
margin-bottom: 16px;
}
.mobile-progress-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.mobile-progress-label {
font-size: 13px;
color: var(--color-text-secondary);
}
.mobile-progress-value {
font-size: 13px;
font-weight: 600;
color: var(--color-text);
}
.mobile-progress-bar {
height: 8px;
background: var(--color-border);
border-radius: 4px;
overflow: hidden;
}
.mobile-progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--color-success), #34d399);
border-radius: 4px;
transition: width 0.3s ease;
}
.mobile-patient-remark {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
border-left: 3px solid var(--color-warning);
border-radius: 8px;
margin-bottom: 16px;
font-size: 14px;
color: #b45309;
font-weight: 500;
}
.mobile-patient-actions {
display: flex;
gap: 8px;
}
.mobile-patient-actions .btn {
flex: 1;
justify-content: center;
padding: 10px 12px;
font-size: 13px;
border-radius: 10px;
min-width: 0;
white-space: nowrap;
}
.mobile-patient-actions form {
flex: 1;
display: flex;
min-width: 0;
}
.mobile-patient-actions form .btn {
width: 100%;
}
.mobile-action-delete {
flex: 0 0 44px !important;
width: 44px !important;
height: 44px;
padding: 0 !important;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--color-danger) !important;
color: var(--color-danger) !important;
background: transparent !important;
}
.mobile-action-delete form {
flex: 0 0 auto !important;
}
/* 移动端显示控制 */
@media (max-width: 900px) {
.table-container {
display: none !important;
}
.mobile-patient-list {
display: block;
}
.custom-pagination {
flex-direction: column;
gap: 12px;
}
.pagination-info {
text-align: center;
font-size: 13px;
}
.pagination-links {
justify-content: center;
}
}
</style>
<div class="page-header">
<h1 class="page-title">患者列表</h1>
<p class="page-subtitle">管理所有患者信息,查看随访状态</p>
</div>
<div class="card">
<div class="card-header">
<div class="search-box">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<form action="{{ route('patients.index') }}" method="GET">
<input type="text" class="form-control" name="search" placeholder="搜索姓名、电话、地址..." value="{{ request('search') }}">
</form>
</div>
<div class="action-buttons">
<a href="{{ route('patients.import') }}" class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="17 8 12 3 7 8"/>
<line x1="12" y1="3" x2="12" y2="15"/>
</svg>
导入数据
</a>
</div>
</div>
@if($patients->isEmpty())
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
<div class="empty-state-title">暂无患者数据</div>
<p>请先导入患者数据,支持 Excel (.xlsx) CSV 格式</p>
<div style="margin-top: 20px;">
<a href="{{ route('patients.import') }}" class="btn btn-primary">导入患者数据</a>
</div>
</div>
@else
<!-- PC端表格 -->
<div class="table-container">
<table class="patient-table">
<thead>
<tr>
<th>患者信息</th>
<th>诊断</th>
<th>转诊日期</th>
<th>随访进度</th>
<th>状态</th>
<th>联系方式</th>
<th>备注</th>
<th style="text-align: right;">操作</th>
</tr>
</thead>
<tbody>
@foreach($patients as $patient)
@php
$total = count($patient->getFollowUpSchedule());
$completed = $patient->follow_up_count;
$status = $patient->getFollowUpStatus();
@endphp
<tr>
<td>
<div class="patient-cell">
<div class="patient-cell-avatar {{ $patient->gender == '女' ? 'female' : 'male' }}">
{{ mb_substr($patient->name, 0, 1) }}
</div>
<div class="patient-cell-info">
<span class="patient-cell-name">{{ $patient->name }}</span>
<span class="patient-cell-meta">{{ $patient->gender }} · {{ $patient->age }}</span>
</div>
</div>
</td>
<td>
<span class="diagnosis-cell">{{ $patient->getDiagnosisType() }}</span>
</td>
<td>{{ $patient->discharge_date->format('Y-m-d') }}</td>
<td>
<div class="progress-cell">
<div class="progress-bar-mini">
<div class="progress-bar-mini-fill" style="width: {{ ($completed / $total) * 100 }}%"></div>
</div>
<span class="progress-text">{{ $completed }}/{{ $total }}</span>
</div>
</td>
<td>
@if($patient->isCompleted())
<span class="badge badge-success">已完成</span>
@elseif(str_contains($status, '过期'))
<span class="badge badge-danger">{{ $status }}</span>
@elseif(str_contains($status, '今日'))
<span class="badge badge-warning">{{ $status }}</span>
@else
<span class="badge badge-info">{{ $status }}</span>
@endif
</td>
<td>
@if($patient->phone)
<a href="tel:{{ $patient->phone }}" class="phone-link">{{ $patient->phone }}</a>
@else
<span style="color: var(--color-text-muted);">-</span>
@endif
</td>
<td>
@if($patient->remark)
<span class="remark-cell" title="{{ $patient->remark }}">{{ $patient->remark }}</span>
@else
<span style="color: var(--color-text-muted);">-</span>
@endif
</td>
<td>
<div class="table-actions">
@if($patient->phone)
<a href="tel:{{ $patient->phone }}" class="btn-icon" title="拨打电话">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
</a>
@endif
@if(!$patient->isCompleted())
<form action="{{ route('patients.follow-up', $patient) }}" method="POST" style="display: inline;">
@csrf
<button type="submit" class="btn-icon success" title="标记已随访" onclick="return confirm('确认标记 {{ $patient->name }} 完成第{{ $patient->getNextFollowUpNumber() }}次随访?')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
</button>
</form>
@endif
<form action="{{ route('patients.destroy', $patient) }}" method="POST" style="display: inline;">
@csrf
@method('DELETE')
<button type="submit" class="btn-icon danger" title="删除" onclick="return confirm('确认删除患者 {{ $patient->name }}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- 移动端卡片列表 -->
<div class="mobile-patient-list">
@foreach($patients as $patient)
@php
$total = count($patient->getFollowUpSchedule());
$completed = $patient->follow_up_count;
$status = $patient->getFollowUpStatus();
@endphp
<div class="mobile-patient-card">
<div class="mobile-patient-header">
<div class="mobile-patient-info">
<div class="mobile-patient-avatar {{ $patient->gender == '女' ? 'female' : 'male' }}">
{{ mb_substr($patient->name, 0, 1) }}
</div>
<div>
<div class="mobile-patient-name">{{ $patient->name }}</div>
<div class="mobile-patient-meta">{{ $patient->gender }} · {{ $patient->age }} · {{ $patient->getDiagnosisType() }}</div>
</div>
</div>
@if($patient->isCompleted())
<span class="badge badge-success">已完成</span>
@elseif(str_contains($status, '过期'))
<span class="badge badge-danger">{{ $status }}</span>
@elseif(str_contains($status, '今日'))
<span class="badge badge-warning">{{ $status }}</span>
@else
<span class="badge badge-info">{{ $status }}</span>
@endif
</div>
<div class="mobile-patient-body">
<div class="mobile-info-item">
<span class="mobile-info-label">转诊日期</span>
<span class="mobile-info-value">{{ $patient->discharge_date->format('Y-m-d') }}</span>
</div>
<div class="mobile-info-item">
<span class="mobile-info-label">联系方式</span>
<span class="mobile-info-value">
@if($patient->phone)
<a href="tel:{{ $patient->phone }}">{{ $patient->phone }}</a>
@else
-
@endif
</span>
</div>
<div class="mobile-info-item" style="grid-column: span 2;">
<span class="mobile-info-label">地址</span>
<span class="mobile-info-value">{{ $patient->address ?: '-' }}</span>
</div>
</div>
@if($patient->remark)
<div class="mobile-patient-remark">
<span>📝</span>
<span>{{ $patient->remark }}</span>
</div>
@endif
<div class="mobile-patient-progress">
<div class="mobile-progress-header">
<span class="mobile-progress-label">随访进度</span>
<span class="mobile-progress-value">{{ $completed }}/{{ $total }} </span>
</div>
<div class="mobile-progress-bar">
<div class="mobile-progress-fill" style="width: {{ ($completed / $total) * 100 }}%"></div>
</div>
</div>
<div class="mobile-patient-actions">
@if($patient->phone)
<a href="tel:{{ $patient->phone }}" class="btn btn-outline">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/>
</svg>
电话
</a>
@else
<div></div>
@endif
@if(!$patient->isCompleted())
<form action="{{ route('patients.follow-up', $patient) }}" method="POST">
@csrf
<button type="submit" class="btn btn-primary" onclick="return confirm('确认标记 {{ $patient->name }} 完成第{{ $patient->getNextFollowUpNumber() }}次随访?')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<polyline points="20 6 9 17 4 12"/>
</svg>
已随访
</button>
</form>
@else
<div></div>
@endif
<form action="{{ route('patients.destroy', $patient) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-outline mobile-action-delete" onclick="return confirm('确认删除患者 {{ $patient->name }}')">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</form>
</div>
</div>
@endforeach
</div>
<!-- 分页 -->
@if($patients->hasPages())
<div class="custom-pagination">
<div class="pagination-info">
显示 {{ $patients->firstItem() }} - {{ $patients->lastItem() }} 条,共 {{ $patients->total() }}
</div>
<div class="pagination-links">
@if($patients->onFirstPage())
<span class="pagination-btn disabled">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<polyline points="15 18 9 12 15 6"/>
</svg>
</span>
@else
<a href="{{ $patients->previousPageUrl() }}" class="pagination-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<polyline points="15 18 9 12 15 6"/>
</svg>
</a>
@endif
@foreach($patients->getUrlRange(1, $patients->lastPage()) as $page => $url)
@if($page == $patients->currentPage())
<span class="pagination-btn active">{{ $page }}</span>
@else
<a href="{{ $url }}" class="pagination-btn">{{ $page }}</a>
@endif
@endforeach
@if($patients->hasMorePages())
<a href="{{ $patients->nextPageUrl() }}" class="pagination-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<polyline points="9 18 15 12 9 6"/>
</svg>
</a>
@else
<span class="pagination-btn disabled">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16">
<polyline points="9 18 15 12 9 6"/>
</svg>
</span>
@endif
</div>
</div>
@endif
@endif
</div>
@endsection