Python项目:智能医疗辅助诊断系统开发(5)

部署运行你感兴趣的模型镜像

源代码续

{% extends "layout.html" %}

{% block title %}注册 - 智能医疗辅助诊断系统{% endblock %}

{% block content %}
<div class="row justify-content-center">
    <div class="col-md-8 col-lg-7">
        <div class="card shadow">
            <div class="card-header bg-primary text-white text-center py-3">
                <h4 class="mb-0"><i class="fas fa-user-plus me-2"></i>用户注册</h4>
            </div>
            <div class="card-body p-4">
                <form method="post" action="{{ url_for('auth.register') }}">
                    <div class="mb-3">
                        <label for="role" class="form-label">账号类型</label>
                        <select class="form-select" id="role" name="role" required onchange="toggleRoleFields()">
                            <option value="" selected disabled>请选择账号类型</option>
                            <option value="doctor">医生</option>
                            <option value="patient">患者</option>
                        </select>
                    </div>
                    
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label for="username" class="form-label">用户名</label>
                            <input type="text" class="form-control" id="username" name="username" required>
                            <div class="form-text">用户名将用于登录系统</div>
                        </div>
                        <div class="col-md-6 mb-3">
                            <label for="email" class="form-label">电子邮箱</label>
                            <input type="email" class="form-control" id="email" name="email" required>
                        </div>
                    </div>
                    
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label for="password" class="form-label">密码</label>
                            <input type="password" class="form-control" id="password" name="password" required>
                        </div>
                        <div class="col-md-6 mb-3">
                            <label for="confirm_password" class="form-label">确认密码</label>
                            <input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
                        </div>
                    </div>
                    
                    <div class="mb-3">
                        <label for="name" class="form-label">姓名</label>
                        <input type="text" class="form-control" id="name" name="name" required>
                    </div>
                    
                    <!-- 医生特有字段 -->
                    <div id="doctor-fields" style="display: none;">
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label for="department" class="form-label">科室</label>
                                <select class="form-select" id="department" name="department">
                                    <option value="" selected disabled>请选择科室</option>
                                    <option value="内科">内科</option>
                                    <option value="外科">外科</option>
                                    <option value="妇产科">妇产科</option>
                                    <option value="儿科">儿科</option>
                                    <option value="眼科">眼科</option>
                                    <option value="耳鼻喉科">耳鼻喉科</option>
                                    <option value="口腔科">口腔科</option>
                                    <option value="皮肤科">皮肤科</option>
                                    <option value="神经科">神经科</option>
                                    <option value="精神科">精神科</option>
                                    <option value="肿瘤科">肿瘤科</option>
                                    <option value="放射科">放射科</option>
                                    <option value="检验科">检验科</option>
                                    <option value="其他">其他</option>
                                </select>
                            </div>
                            <div class="col-md-6 mb-3">
                                <label for="title" class="form-label">职称</label>
                                <select class="form-select" id="title" name="title">
                                    <option value="" selected disabled>请选择职称</option>
                                    <option value="主任医师">主任医师</option>
                                    <option value="副主任医师">副主任医师</option>
                                    <option value="主治医师">主治医师</option>
                                    <option value="住院医师">住院医师</option>
                                    <option value="医师">医师</option>
                                </select>
                            </div>
                        </div>
                    </div>
                    
                    <!-- 患者特有字段 -->
                    <div id="patient-fields" style="display: none;">
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label for="age" class="form-label">年龄</label>
                                <input type="number" class="form-control" id="age" name="age" min="0" max="120">
                            </div>
                            <div class="col-md-6 mb-3">
                                <label for="gender" class="form-label">性别</label>
                                <select class="form-select" id="gender" name="gender">
                                    <option value="" selected disabled>请选择性别</option>
                                    <option value="男">男</option>
                                    <option value="女">女</option>
                                    <option value="其他">其他</option>
                                </select>
                            </div>
                        </div>
                    </div>
                    
                    <div class="mb-3 form-check">
                        <input type="checkbox" class="form-check-input" id="agree" name="agree" required>
                        <label class="form-check-label" for="agree">我已阅读并同意<a href="#" class="text-decoration-none">用户协议</a>和<a href="#" class="text-decoration-none">隐私政策</a></label>
                    </div>
                    
                    <div class="d-grid gap-2">
                        <button type="submit" class="btn btn-primary btn-lg">注册</button>
                    </div>
                </form>
            </div>
            <div class="card-footer bg-light text-center py-3">
                已有账号? <a href="{{ url_for('auth.login') }}" class="text-decoration-none">立即登录</a>
            </div>
        </div>
    </div>
</div>

{% block scripts %}
<script>
    function toggleRoleFields() {
        const role = document.getElementById('role').value;
        const doctorFields = document.getElementById('doctor-fields');
        const patientFields = document.getElementById('patient-fields');
        
        if (role === 'doctor') {
            doctorFields.style.display = 'block';
            patientFields.style.display = 'none';
            
            // 设置医生字段为必填
            document.getElementById('department').required = true;
            document.getElementById('title').required = true;
            
            // 取消患者字段的必填
            document.getElementById('age').required = false;
            document.getElementById('gender').required = false;
        } else if (role === 'patient') {
            doctorFields.style.display = 'none';
            patientFields.style.display = 'block';
            
            // 取消医生字段的必填
            document.getElementById('department').required = false;
            document.getElementById('title').required = false;
            
            // 设置患者字段为必填
            document.getElementById('age').required = true;
            document.getElementById('gender').required = true;
        } else {
            doctorFields.style.display = 'none';
            patientFields.style.display = 'none';
        }
    }
    
    // 检查密码一致性
    document.getElementById('confirm_password').addEventListener('input', function() {
        const password = document.getElementById('password').value;
        const confirmPassword = this.value;
        
        if (password !== confirmPassword) {
            this.setCustomValidity('两次输入的密码不一致');
        } else {
            this.setCustomValidity('');
        }
    });
</script>
{% endblock %}
{% endblock %}

modules\user_interface\templates\doctor\dashboard.html

{% extends "layout.html" %}

{% block title %}医生控制面板 - 智能医疗辅助诊断系统{% endblock %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-6">
        <h1 class="h2 mb-0"><i class="fas fa-clinic-medical me-2"></i>医生控制面板</h1>
        <p class="text-muted">欢迎回来,{{ g.user.name }}。</p>
    </div>
    <div class="col-md-6 text-md-end">
        <a href="{{ url_for('new_case') }}" class="btn btn-primary">
            <i class="fas fa-plus-circle me-1"></i>新建病例
        </a>
    </div>
</div>

<!-- 统计卡片 -->
<div class="row mb-4">
    <div class="col-xl-3 col-md-6 mb-4">
        <div class="card border-left-primary shadow h-100 py-2">
            <div class="card-body">
                <div class="row no-gutters align-items-center">
                    <div class="col mr-2">
                        <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">总病例数</div>
                        <div class="h5 mb-0 font-weight-bold text-gray-800">{{ statistics.total_cases }}</div>
                    </div>
                    <div class="col-auto">
                        <i class="fas fa-folder fa-2x text-gray-300"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="col-xl-3 col-md-6 mb-4">
        <div class="card border-left-success shadow h-100 py-2">
            <div class="card-body">
                <div class="row no-gutters align-items-center">
                    <div class="col mr-2">
                        <div class="text-xs font-weight-bold text-success text-uppercase mb-1">今日完成</div>
                        <div class="h5 mb-0 font-weight-bold text-gray-800">{{ statistics.completed_today }}</div>
                    </div>
                    <div class="col-auto">
                        <i class="fas fa-calendar-check fa-2x text-gray-300"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="col-xl-3 col-md-6 mb-4">
        <div class="card border-left-warning shadow h-100 py-2">
            <div class="card-body">
                <div class="row no-gutters align-items-center">
                    <div class="col mr-2">
                        <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">待审核</div>
                        <div class="h5 mb-0 font-weight-bold text-gray-800">{{ statistics.pending_review }}</div>
                    </div>
                    <div class="col-auto">
                        <i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="col-xl-3 col-md-6 mb-4">
        <div class="card border-left-info shadow h-100 py-2">
            <div class="card-body">
                <div class="row no-gutters align-items-center">
                    <div class="col mr-2">
                        <div class="text-xs font-weight-bold text-info text-uppercase mb-1">系统准确率</div>
                        <div class="row no-gutters align-items-center">
                            <div class="col-auto">
                                <div class="h5 mb-0 mr-3 font-weight-bold text-gray-800">{{ (statistics.system_accuracy * 100)|round }}%</div>
                            </div>
                            <div class="col">
                                <div class="progress progress-sm mr-2">
                                    <div class="progress-bar bg-info" role="progressbar" style="width: {{ (statistics.system_accuracy * 100)|round }}%"></div>
                                </div>
                            </div>
                        </div>
                    </div>
                    <div class="col-auto">
                        <i class="fas fa-chart-line fa-2x text-gray-300"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 最近病例 -->
<div class="card shadow mb-4">
    <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
        <h6 class="m-0 font-weight-bold text-primary">最近病例</h6>
        <a href="{{ url_for('patient_list') }}" class="btn btn-sm btn-primary">
            查看全部
        </a>
    </div>
    <div class="card-body">
        <div class="table-responsive">
            <table class="table table-bordered table-hover" width="100%" cellspacing="0">
                <thead>
                    <tr>
                        <th>ID</th>
                        <th>患者姓名</th>
                        <th>年龄/性别</th>
                        <th>主要诊断</th>
                        <th>置信度</th>
                        <th>日期</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    {% for case in recent_cases %}
                    <tr>
                        <td>{{ case.id }}</td>
                        <td>{{ case.patient_name }}</td>
                        <td>{{ case.age }}岁/{{ case.gender }}</td>
                        <td>{{ case.primary_diagnosis }}</td>
                        <td>
                            <div class="progress" style="height: 20px;">
                                <div class="progress-bar 
                                    {% if case.confidence >= 0.8 %}
                                        bg-success
                                    {% elif case.confidence >= 0.6 %}
                                        bg-info
                                    {% elif case.confidence >= 0.4 %}
                                        bg-warning
                                    {% else %}
                                        bg-danger
                                    {% endif %}"
                                    role="progressbar" style="width: {{ (case.confidence * 100)|round }}%"
                                    aria-valuenow="{{ (case.confidence * 100)|round }}" aria-valuemin="0" aria-valuemax="100">
                                    {{ (case.confidence * 100)|round }}%
                                </div>
                            </div>
                        </td>
                        <td>{{ case.date }}</td>
                        <td>
                            <a href="{{ url_for('view_case', case_id=case.id) }}" class="btn btn-sm btn-primary">
                                <i class="fas fa-eye"></i> 查看
                            </a>
                        </td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div>
    </div>
</div>

<!-- 图表行 -->
<div class="row">
    <!-- 诊断分布图 -->
    <div class="col-xl-8 col-lg-7">
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">诊断分布</h6>
            </div>
            <div class="card-body">
                <div class="chart-area">
                    <canvas id="diagnosisChart"></canvas>
                </div>
            </div>
        </div>
    </div>

    <!-- 系统准确率趋势 -->
    <div class="col-xl-4 col-lg-5">
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">系统准确率趋势</h6>
            </div>
            <div class="card-body">
                <div class="chart-pie">
                    <canvas id="accuracyChart"></canvas>
                </div>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
    // 诊断分布图
    var ctx = document.getElementById("diagnosisChart");
    var myLineChart = new Chart(ctx, {
        type: 'bar',
        data: {
            labels: ["肺炎", "冠心病", "2型糖尿病", "高血压", "胃炎", "关节炎", "其他"],
            datasets: [{
                label: "诊断数量",
                backgroundColor: "rgba(78, 115, 223, 0.8)",
                borderColor: "rgba(78, 115, 223, 1)",
                data: [35, 29, 24, 20, 16, 12, 16],
            }],
        },
        options: {
            maintainAspectRatio: false,
            scales: {
                y: {
                    beginAtZero: true
                }
            }
        }
    });

    // 系统准确率趋势
    var ctx2 = document.getElementById("accuracyChart");
    var myPieChart = new Chart(ctx2, {
        type: 'line',
        data: {
            labels: ["1月", "2月", "3月", "4月", "5月"],
            datasets: [{
                label: "系统准确率",
                lineTension: 0.3,
                backgroundColor: "rgba(78, 115, 223, 0.05)",
                borderColor: "rgba(78, 115, 223, 1)",
                pointRadius: 3,
                pointBackgroundColor: "rgba(78, 115, 223, 1)",
                pointBorderColor: "rgba(78, 115, 223, 1)",
                pointHoverRadius: 3,
                pointHoverBackgroundColor: "rgba(78, 115, 223, 1)",
                pointHoverBorderColor: "rgba(78, 115, 223, 1)",
                pointHitRadius: 10,
                pointBorderWidth: 2,
                data: [0.75, 0.78, 0.82, 0.85, 0.87],
            }],
        },
        options: {
            maintainAspectRatio: false,
            scales: {
                y: {
                    min: 0.7,
                    max: 0.9
                }
            }
        }
    });
</script>
{% endblock %}

modules\user_interface\templates\doctor\new_case.html

{% extends "layout.html" %}

{% block title %}新建病例 - 智能医疗辅助诊断系统{% endblock %}

{% block styles %}
<style>
    .custom-file-upload {
        border: 1px dashed #ccc;
        display: inline-block;
        padding: 30px 20px;
        cursor: pointer;
        width: 100%;
        text-align: center;
        border-radius: 5px;
        transition: all 0.3s;
    }
    .custom-file-upload:hover {
        background-color: #f8f9fa;
        border-color: #adb5bd;
    }
    .preview-container {
        max-height: 200px;
        overflow: hidden;
        margin-top: 10px;
        text-align: center;
    }
    .preview-container img {
        max-height: 180px;
        max-width: 100%;
    }
    .file-info {
        margin-top: 10px;
        font-size: 0.9rem;
    }
</style>
{% endblock %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-6">
        <h1 class="h2 mb-0"><i class="fas fa-file-medical me-2"></i>新建病例</h1>
        <p class="text-muted">创建新的诊断病例,上传相关医疗数据进行AI辅助诊断。</p>
    </div>
</div>

<div class="card shadow mb-4">
    <div class="card-header py-3">
        <h6 class="m-0 font-weight-bold text-primary">病例信息</h6>
    </div>
    <div class="card-body">
        <form method="post" enctype="multipart/form-data" id="newCaseForm">
            <!-- 患者基本信息 -->
            <div class="mb-4">
                <h5 class="border-bottom pb-2"><i class="fas fa-user me-2"></i>患者基本信息</h5>
                <div class="row g-3">
                    <div class="col-md-4">
                        <label for="patient_name" class="form-label">姓名</label>
                        <input type="text" class="form-control" id="patient_name" name="patient_name" required>
                    </div>
                    <div class="col-md-4">
                        <label for="patient_age" class="form-label">年龄</label>
                        <input type="number" class="form-control" id="patient_age" name="patient_age" min="0" max="120" required>
                    </div>
                    <div class="col-md-4">
                        <label for="patient_gender" class="form-label">性别</label>
                        <select class="form-select" id="patient_gender" name="patient_gender" required>
                            <option value="" selected disabled>请选择</option>
                            <option value="男">男</option>
                            <option value="女">女</option>
                            <option value="其他">其他</option>
                        </select>
                    </div>
                    <div class="col-md-6">
                        <label for="patient_height" class="form-label">身高 (cm)</label>
                        <input type="number" class="form-control" id="patient_height" name="patient_height" min="0" max="250">
                    </div>
                    <div class="col-md-6">
                        <label for="patient_weight" class="form-label">体重 (kg)</label>
                        <input type="number" class="form-control" id="patient_weight" name="patient_weight" min="0" max="500">
                    </div>
                </div>
            </div>

            <!-- 病史信息 -->
            <div class="mb-4">
                <h5 class="border-bottom pb-2"><i class="fas fa-clipboard-list me-2"></i>病史信息</h5>
                <div class="row g-3">
                    <div class="col-md-6">
                        <label for="medical_history" class="form-label">既往病史</label>
                        <textarea class="form-control" id="medical_history" name="medical_history" rows="3" placeholder="请输入既往病史,多个病史请用逗号分隔"></textarea>
                        <div class="form-text">例如:高血压,2型糖尿病,冠心病</div>
                    </div>
                    <div class="col-md-6">
                        <label for="allergies" class="form-label">过敏史</label>
                        <textarea class="form-control" id="allergies" name="allergies" rows="3" placeholder="请输入过敏史,多个过敏原请用逗号分隔"></textarea>
                        <div class="form-text">例如:青霉素,磺胺类药物,海鲜</div>
                    </div>
                </div>
            </div>

            <!-- 医学影像上传 -->
            <div class="mb-4">
                <h5 class="border-bottom pb-2"><i class="fas fa-x-ray me-2"></i>医学影像</h5>
                <div class="row g-3">
                    <div class="col-md-6">
                        <label for="image_modality" class="form-label">影像类型</label>
                        <select class="form-select" id="image_modality" name="image_modality">
                            <option value="" selected disabled>请选择影像类型</option>
                            <option value="X光">X光</option>
                            <option value="CT">CT</option>
                            <option value="MRI">MRI</option>
                            <option value="超声">超声</option>
                            <option value="其他">其他</option>
                        </select>
                    </div>
                    <div class="col-md-6">
                        <label for="body_part" class="form-label">检查部位</label>
                        <select class="form-select" id="body_part" name="body_part">
                            <option value="" selected disabled>请选择检查部位</option>
                            <option value="头部">头部</option>
                            <option value="胸部">胸部</option>
                            <option value="腹部">腹部</option>
                            <option value="骨骼">骨骼</option>
                            <option value="四肢">四肢</option>
                            <option value="其他">其他</option>
                        </select>
                    </div>
                    <div class="col-12">
                        <label for="medical_image" class="form-label">上传医学影像</label>
                        <div class="custom-file-upload" id="imageUploadContainer">
                            <input type="file" class="form-control d-none" id="medical_image" name="medical_image" accept=".png,.jpg,.jpeg,.gif,.bmp,.dcm,.dicom" onchange="previewImage(this)">
                            <i class="fas fa-upload fa-2x mb-2"></i>
                            <p class="mb-0">点击或拖拽文件到此处上传</p>
                            <p class="text-muted small">支持 DICOM, JPG, PNG, BMP 格式</p>
                        </div>
                        <div class="preview-container d-none" id="imagePreviewContainer">
                            <img id="imagePreview" src="#" alt="预览图">
                            <div class="file-info" id="imageFileInfo"></div>
                        </div>
                    </div>
                    <div class="col-12">
                        <label for="image_findings" class="form-label">影像所见</label>
                        <textarea class="form-control" id="image_findings" name="image_findings" rows="3" placeholder="请输入影像所见,每行一个发现"></textarea>
                        <div class="form-text">在实际应用中,这部分将由医学影像分析模块自动生成</div>
                    </div>
                </div>
            </div>

            <!-- 临床文本上传 -->
            <div class="mb-4">
                <h5 class="border-bottom pb-2"><i class="fas fa-file-medical-alt me-2"></i>临床文本</h5>
                <div class="row g-3">
                    <div class="col-12">
                        <label for="clinical_notes" class="form-label">临床病历</label>
                        <textarea class="form-control" id="clinical_notes" name="clinical_notes" rows="5" placeholder="请输入临床病历内容"></textarea>
                    </div>
                    <div class="col-12">
                        <label for="clinical_text" class="form-label">上传临床文本文件</label>
                        <div class="custom-file-upload" id="textUploadContainer">
                            <input type="file" class="form-control d-none" id="clinical_text" name="clinical_text" accept=".txt,.pdf,.docx,.doc" onchange="previewText(this)">
                            <i class="fas fa-file-alt fa-2x mb-2"></i>
                            <p class="mb-0">点击或拖拽文件到此处上传</p>
                            <p class="text-muted small">支持 TXT, PDF, DOCX 格式</p>
                        </div>
                        <div class="file-info d-none" id="textFileInfo"></div>
                    </div>
                </div>
            </div>

            <!-- 实验室检查结果 -->
            <div class="mb-4">
                <h5 class="border-bottom pb-2"><i class="fas fa-flask me-2"></i>实验室检查结果</h5>
                <div class="row g-3">
                    <div class="col-12">
                        <label for="lab_results" class="form-label">上传实验室检查结果</label>
                        <div class="custom-file-upload" id="labUploadContainer">
                            <input type="file" class="form-control d-none" id="lab_results" name="lab_results" accept=".csv,.xlsx,.xls,.txt,.json" onchange="previewLab(this)">
                            <i class="fas fa-vial fa-2x mb-2"></i>
                            <p class="mb-0">点击或拖拽文件到此处上传</p>
                            <p class="text-muted small">支持 CSV, Excel, TXT, JSON 格式</p>
                        </div>
                        <div class="file-info d-none" id="labFileInfo"></div>
                    </div>
                </div>
            </div>

            <!-- 诊断选项 -->
            <div class="mb-4">
                <h5 class="border-bottom pb-2"><i class="fas fa-cog me-2"></i>诊断选项</h5>
                <div class="row g-3">
                    <div class="col-md-6">
                        <label for="report_format" class="form-label">报告格式</label>
                        <select class="form-select" id="report_format" name="report_format">
                            <option value="html" selected>HTML</option>
                            <option value="text">文本</option>
                            <option value="json">JSON</option>
                        </select>
                    </div>
                    <div class="col-md-6">
                        <label for="diagnosis_method" class="form-label">诊断方法</label>
                        <select class="form-select" id="diagnosis_method" name="diagnosis_method">
                            <option value="hybrid" selected>混合推理(推荐)</option>
                            <option value="rule_based">规则推理</option>
                            <option value="bayesian">贝叶斯推理</option>
                            <option value="case_based">案例推理</option>
                            <option value="deep_learning">深度学习</option>
                        </select>
                    </div>
                </div>
            </div>

            <!-- 提交按钮 -->
            <div class="d-grid gap-2 d-md-flex justify-content-md-end">
                <button type="button" class="btn btn-secondary me-md-2" onclick="window.location.href='{{ url_for('doctor_dashboard') }}'">取消</button>
                <button type="submit" class="btn btn-primary">
                    <i class="fas fa-stethoscope me-1"></i>生成诊断
                </button>
            </div>
        </form>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    // 图片上传预览
    function previewImage(input) {
        const imagePreviewContainer = document.getElementById('imagePreviewContainer');
        const imagePreview = document.getElementById('imagePreview');
        const imageFileInfo = document.getElementById('imageFileInfo');
        
        if (input.files && input.files[0]) {
            const file = input.files[0];
            const reader = new FileReader();
            
            reader.onload = function(e) {
                // 显示预览
                imagePreview.src = e.target.result;
                imagePreviewContainer.classList.remove('d-none');
                
                // 显示文件信息
                imageFileInfo.textContent = `文件名: ${file.name} | 大小: ${formatFileSize(file.size)} | 类型: ${file.type}`;
                imageFileInfo.classList.remove('d-none');
            }
            
            reader.readAsDataURL(file);
        }
    }
    
    // 文本文件预览
    function previewText(input) {
        const textFileInfo = document.getElementById('textFileInfo');
        
        if (input.files && input.files[0]) {
            const file = input.files[0];
            
            // 显示文件信息
            textFileInfo.textContent = `文件名: ${file.name} | 大小: ${formatFileSize(file.size)} | 类型: ${file.type}`;
            textFileInfo.classList.remove('d-none');
        }
    }
    
    // 实验室结果文件预览
    function previewLab(input) {
        const labFileInfo = document.getElementById('labFileInfo');
        
        if (input.files && input.files[0]) {
            const file = input.files[0];
            
            // 显示文件信息
            labFileInfo.textContent = `文件名: ${file.name} | 大小: ${formatFileSize(file.size)} | 类型: ${file.type}`;
            labFileInfo.classList.remove('d-none');
        }
    }
    
    // 格式化文件大小
    function formatFileSize(bytes) {
        if (bytes < 1024) {
            return bytes + ' B';
        } else if (bytes < 1048576) {
            return (bytes / 1024).toFixed(2) + ' KB';
        } else {
            return (bytes / 1048576).toFixed(2) + ' MB';
        }
    }
    
    // 拖拽上传
    document.addEventListener('DOMContentLoaded', function() {
        // 图片上传
        const imageUploadContainer = document.getElementById('imageUploadContainer');
        const imageInput = document.getElementById('medical_image');
        
        imageUploadContainer.addEventListener('click', function() {
            imageInput.click();
        });
        
        imageUploadContainer.addEventListener('dragover', function(e) {
            e.preventDefault();
            imageUploadContainer.style.backgroundColor = '#f8f9fa';
            imageUploadContainer.style.borderColor = '#adb5bd';
        });
        
        imageUploadContainer.addEventListener('dragleave', function(e) {
            e.preventDefault();
            imageUploadContainer.style.backgroundColor = '';
            imageUploadContainer.style.borderColor = '#ccc';
        });
        
        imageUploadContainer.addEventListener('drop', function(e) {
            e.preventDefault();
            imageUploadContainer.style.backgroundColor = '';
            imageUploadContainer.style.borderColor = '#ccc';
            
            if (e.dataTransfer.files.length) {
                imageInput.files = e.dataTransfer.files;
                previewImage(imageInput);
            }
        });
        
        // 文本上传
        const textUploadContainer = document.getElementById('textUploadContainer');
        const textInput = document.getElementById('clinical_text');
        
        textUploadContainer.addEventListener('click', function() {
            textInput.click();
        });
        
        textUploadContainer.addEventListener('dragover', function(e) {
            e.preventDefault();
            textUploadContainer.style.backgroundColor = '#f8f9fa';
            textUploadContainer.style.borderColor = '#adb5bd';
        });
        
        textUploadContainer.addEventListener('dragleave', function(e) {
            e.preventDefault();
            textUploadContainer.style.backgroundColor = '';
            textUploadContainer.style.borderColor = '#ccc';
        });
        
        textUploadContainer.addEventListener('drop', function(e) {
            e.preventDefault();
            textUploadContainer.style.backgroundColor = '';
            textUploadContainer.style.borderColor = '#ccc';
            
            if (e.dataTransfer.files.length) {
                textInput.files = e.dataTransfer.files;
                previewText(textInput);
            }
        });
        
        // 实验室结果上传
        const labUploadContainer = document.getElementById('labUploadContainer');
        const labInput = document.getElementById('lab_results');
        
        labUploadContainer.addEventListener('click', function() {
            labInput.click();
        });
        
        labUploadContainer.addEventListener('dragover', function(e) {
            e.preventDefault();
            labUploadContainer.style.backgroundColor = '#f8f9fa';
            labUploadContainer.style.borderColor = '#adb5bd';
        });
        
        labUploadContainer.addEventListener('dragleave', function(e) {
            e.preventDefault();
            labUploadContainer.style.backgroundColor = '';
            labUploadContainer.style.borderColor = '#ccc';
        });
        
        labUploadContainer.addEventListener('drop', function(e) {
            e.preventDefault();
            labUploadContainer.style.backgroundColor = '';
            labUploadContainer.style.borderColor = '#ccc';
            
            if (e.dataTransfer.files.length) {
                labInput.files = e.dataTransfer.files;
                previewLab(labInput);
            }
        });
    });
</script>
{% endblock %}

modules\user_interface\templates\doctor\view_case.html

{% extends "layout.html" %}

{% block title %}查看病例 - 智能医疗辅助诊断系统{% endblock %}

{% block styles %}
<style>
    .diagnosis-confidence {
        height: 30px;
        border-radius: 15px;
        overflow: hidden;
    }
    .diagnosis-item {
        border-left: 4px solid #4e73df;
        padding-left: 15px;
        margin-bottom: 15px;
    }
    .medical-image {
        max-height: 300px;
        max-width: 100%;
        border-radius: 5px;
    }
    .finding-item {
        border-left: 3px solid #1cc88a;
        padding-left: 10px;
        margin-bottom: 10px;
    }
    .nav-tabs .nav-link.active {
        font-weight: bold;
        border-bottom: 3px solid #4e73df;
    }
    .badge-highlight {
        background-color: rgba(78, 115, 223, 0.1);
        color: #4e73df;
        font-weight: normal;
        padding: 5px 10px;
        border-radius: 10px;
    }
    .timeline {
        position: relative;
        padding-left: 30px;
    }
    .timeline::before {
        content: '';
        position: absolute;
        left: 10px;
        top: 0;
        bottom: 0;
        width: 2px;
        background: #e3e6f0;
    }
    .timeline-item {
        position: relative;
        margin-bottom: 20px;
    }
    .timeline-item::before {
        content: '';
        position: absolute;
        left: -30px;
        top: 0;
        width: 20px;
        height: 20px;
        border-radius: 50%;
        background: #4e73df;
        border: 3px solid #fff;
    }
</style>
{% endblock %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-8">
        <h1 class="h2 mb-0">
            <i class="fas fa-file-medical me-2"></i>病例 #{{ case.id }}
            <span class="badge 
                {% if case.status == '已完成' %}
                    bg-success
                {% elif case.status == '处理中' %}
                    bg-primary
                {% elif case.status == '待复诊' %}
                    bg-warning
                {% else %}
                    bg-secondary
                {% endif %}">
                {{ case.status }}
            </span>
        </h1>
        <p class="text-muted">创建于 {{ case.created_at }} | 更新于 {{ case.updated_at }}</p>
    </div>
    <div class="col-md-4 text-md-end">
        <div class="btn-group">
            <a href="{{ url_for('edit_case', case_id=case.id) }}" class="btn btn-outline-primary">
                <i class="fas fa-edit me-1"></i>编辑
            </a>
            <a href="{{ url_for('print_case', case_id=case.id) }}" class="btn btn-outline-secondary" target="_blank">
                <i class="fas fa-print me-1"></i>打印
            </a>
            <button type="button" class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#shareModal">
                <i class="fas fa-share-alt me-1"></i>分享
            </button>
        </div>
    </div>
</div>

<!-- 主要内容区 -->
<div class="row">
    <!-- 左侧:患者信息和诊断结果 -->
    <div class="col-lg-8">
        <!-- 诊断结果卡片 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
                <h6 class="m-0 font-weight-bold text-primary">AI辅助诊断结果</h6>
                <span class="badge badge-highlight">
                    <i class="fas fa-robot me-1"></i>{{ case.diagnosis_method }}
                </span>
            </div>
            <div class="card-body">
                <!-- 主要诊断 -->
                <div class="mb-4">
                    <h5 class="font-weight-bold">主要诊断</h5>
                    <div class="diagnosis-item p-3 bg-light rounded">
                        <div class="d-flex justify-content-between align-items-center mb-2">
                            <h5 class="mb-0">{{ case.primary_diagnosis }}</h5>
                            <span class="badge bg-primary">{{ (case.confidence * 100)|round }}% 置信度</span>
                        </div>
                        <div class="progress diagnosis-confidence mb-2">
                            <div class="progress-bar bg-primary" role="progressbar" style="width: {{ (case.confidence * 100)|round }}%"></div>
                        </div>
                        <p class="mb-0">{{ case.diagnosis_description }}</p>
                    </div>
                </div>
                
                <!-- 鉴别诊断 -->
                <div class="mb-4">
                    <h5 class="font-weight-bold">鉴别诊断</h5>
                    <div class="row">
                        {% for diagnosis in case.differential_diagnoses %}
                        <div class="col-md-6 mb-3">
                            <div class="diagnosis-item p-3 bg-light rounded">
                                <div class="d-flex justify-content-between align-items-center mb-2">
                                    <h6 class="mb-0">{{ diagnosis.name }}</h6>
                                    <span class="badge bg-secondary">{{ (diagnosis.confidence * 100)|round }}%</span>
                                </div>
                                <div class="progress diagnosis-confidence" style="height: 10px;">
                                    <div class="progress-bar bg-secondary" role="progressbar" style="width: {{ (diagnosis.confidence * 100)|round }}%"></div>
                                </div>
                            </div>
                        </div>
                        {% endfor %}
                    </div>
                </div>
                
                <!-- 建议治疗方案 -->
                <div>
                    <h5 class="font-weight-bold">建议治疗方案</h5>
                    <div class="bg-light p-3 rounded">
                        <ul class="list-group list-group-flush">
                            {% for treatment in case.recommended_treatments %}
                            <li class="list-group-item bg-light">
                                <i class="fas fa-check-circle text-success me-2"></i>{{ treatment }}
                            </li>
                            {% endfor %}
                        </ul>
                        <div class="mt-3">
                            <small class="text-muted">
                                <i class="fas fa-info-circle me-1"></i>治疗建议仅供参考,请根据患者具体情况调整。
                            </small>
                        </div>
                    </div>
                </div>
            </div>
        </div>
        
        <!-- 诊断依据和详情 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <ul class="nav nav-tabs card-header-tabs" id="diagnosisDetailsTabs" role="tablist">
                    <li class="nav-item" role="presentation">
                        <button class="nav-link active" id="findings-tab" data-bs-toggle="tab" data-bs-target="#findings" type="button" role="tab">
                            <i class="fas fa-search me-1"></i>发现与依据
                        </button>
                    </li>
                    <li class="nav-item" role="presentation">
                        <button class="nav-link" id="images-tab" data-bs-toggle="tab" data-bs-target="#images" type="button" role="tab">
                            <i class="fas fa-x-ray me-1"></i>医学影像
                        </button>
                    </li>
                    <li class="nav-item" role="presentation">
                        <button class="nav-link" id="lab-tab" data-bs-toggle="tab" data-bs-target="#lab" type="button" role="tab">
                            <i class="fas fa-flask me-1"></i>实验室结果
                        </button>
                    </li>
                    <li class="nav-item" role="presentation">
                        <button class="nav-link" id="notes-tab" data-bs-toggle="tab" data-bs-target="#notes" type="button" role="tab">
                            <i class="fas fa-clipboard me-1"></i>临床记录
                        </button>
                    </li>
                </ul>
            </div>
            <div class="card-body">
                <div class="tab-content" id="diagnosisDetailsTabContent">
                    <!-- 发现与依据 -->
                    <div class="tab-pane fade show active" id="findings" role="tabpanel">
                        <h5 class="card-title mb-3">关键发现</h5>
                        <div class="mb-4">
                            {% for finding in case.key_findings %}
                            <div class="finding-item mb-3">
                                <div class="d-flex align-items-start">
                                    <div>
                                        <h6>{{ finding.title }}</h6>
                                        <p class="mb-1">{{ finding.description }}</p>
                                        <div>
                                            <span class="badge bg-light text-dark me-1">
                                                <i class="fas fa-tag me-1"></i>{{ finding.category }}
                                            </span>
                                            <span class="badge bg-light text-dark">
                                                <i class="fas fa-weight me-1"></i>权重: {{ finding.weight }}
                                            </span>
                                        </div>
                                    </div>
                                </div>
                            </div>
                            {% endfor %}
                        </div>
                        
                        <h5 class="card-title mb-3">诊断推理过程</h5>
                        <div class="timeline">
                            {% for step in case.reasoning_steps %}
                            <div class="timeline-item">
                                <h6>{{ step.title }}</h6>
                                <p>{{ step.description }}</p>
                                {% if step.evidence %}
                                <div class="bg-light p-2 rounded small">
                                    <i class="fas fa-quote-left text-muted me-1"></i>
                                    {{ step.evidence }}
                                </div>
                                {% endif %}
                            </div>
                            {% endfor %}
                        </div>
                    </div>
                    
                    <!-- 医学影像 -->
                    <div class="tab-pane fade" id="images" role="tabpanel">
                        <div class="row">
                            {% for image in case.medical_images %}
                            <div class="col-md-6 mb-4">
                                <div class="card">
                                    <div class="card-header py-2">
                                        <div class="d-flex justify-content-between align-items-center">
                                            <h6 class="mb-0">{{ image.type }} - {{ image.body_part }}</h6>
                                            <small class="text-muted">{{ image.date }}</small>
                                        </div>
                                    </div>
                                    <div class="card-body text-center">
                                        <img src="{{ image.url }}" class="medical-image mb-3" alt="{{ image.type }}">
                                        <div class="text-start">
                                            <h6>影像所见:</h6>
                                            <p>{{ image.findings }}</p>
                                        </div>
                                    </div>
                                </div>
                            </div>
                            {% endfor %}
                        </div>
                    </div>
                    
                    <!-- 实验室结果 -->
                    <div class="tab-pane fade" id="lab" role="tabpanel">
                        <div class="table-responsive">
                            <table class="table table-bordered table-hover">
                                <thead class="table-light">
                                    <tr>
                                        <th>检测项目</th>
                                        <th>结果</th>
                                        <th>参考范围</th>
                                        <th>状态</th>
                                        <th>日期</th>
                                    </tr>
                                </thead>
                                <tbody>
                                    {% for result in case.lab_results %}
                                    <tr>
                                        <td>{{ result.name }}</td>
                                        <td>{{ result.value }} {{ result.unit }}</td>
                                        <td>{{ result.reference_range }}</td>
                                        <td>
                                            <span class="badge 
                                                {% if result.status == '正常' %}
                                                    bg-success
                                                {% elif result.status == '偏高' %}
                                                    bg-danger
                                                {% elif result.status == '偏低' %}
                                                    bg-warning
                                                {% else %}
                                                    bg-secondary
                                                {% endif %}">
                                                {{ result.status }}
                                            </span>
                                        </td>
                                        <td>{{ result.date }}</td>
                                    </tr>
                                    {% endfor %}
                                </tbody>
                            </table>
                        </div>
                    </div>
                    
                    <!-- 临床记录 -->
                    <div class="tab-pane fade" id="notes" role="tabpanel">
                        <div class="mb-4">
                            <h5 class="card-title">主诉</h5>
                            <p>{{ case.chief_complaint }}</p>
                        </div>
                        
                        <div class="mb-4">
                            <h5 class="card-title">现病史</h5>
                            <p>{{ case.present_illness }}</p>
                        </div>
                        
                        <div class="mb-4">
                            <h5 class="card-title">既往史</h5>
                            <ul>
                                {% for history in case.medical_history %}
                                <li>{{ history }}</li>
                                {% endfor %}
                            </ul>
                        </div>
                        
                        <div class="mb-4">
                            <h5 class="card-title">体格检查</h5>
                            <div class="row">
                                {% for exam in case.physical_exams %}
                                <div class="col-md-6 mb-3">
                                    <div class="card">
                                        <div class="card-header py-2">
                                            <h6 class="mb-0">{{ exam.category }}</h6>
                                        </div>
                                        <div class="card-body">
                                            <p class="mb-0">{{ exam.findings }}</p>
                                        </div>
                                    </div>
                                </div>
                                {% endfor %}
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    
    <!-- 右侧:患者信息和医生笔记 -->
    <div class="col-lg-4">
        <!-- 患者信息卡片 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">患者信息</h6>
            </div>
            <div class="card-body">
                <div class="text-center mb-3">
                    <img src="{{ url_for('static', filename='img/patient_avatar.png') }}" class="rounded-circle img-thumbnail" style="width: 100px; height: 100px;" alt="患者头像">
                    <h5 class="mt-2 mb-0">{{ case.patient.name }}</h5>
                    <p class="text-muted">{{ case.patient.age }}岁 | {{ case.patient.gender }}</p>
                </div>
                
                <hr>
                
                <div class="row mb-2">
                    <div class="col-5 text-muted">身份证号:</div>
                    <div class="col-7">{{ case.patient.id_number }}</div>
                </div>
                <div class="row mb-2">
                    <div class="col-5 text-muted">联系电话:</div>
                    <div class="col-7">{{ case.patient.phone }}</div>
                </div>
                <div class="row mb-2">
                    <div class="col-5 text-muted">血型:</div>
                    <div class="col-7">{{ case.patient.blood_type }}</div>
                </div>
                <div class="row mb-2">
                    <div class="col-5 text-muted">过敏史:</div>
                    <div class="col-7">{{ case.patient.allergies or '无' }}</div>
                </div>
                <div class="row mb-2">
                    <div class="col-5 text-muted">身高/体重:</div>
                    <div class="col-7">{{ case.patient.height }}cm / {{ case.patient.weight }}kg</div>
                </div>
                <div class="row mb-2">
                    <div class="col-5 text-muted">BMI:</div>
                    <div class="col-7">{{ case.patient.bmi }}</div>
                </div>
                
                <hr>
                
                <div class="d-grid gap-2">
                    <a href="{{ url_for('patient_history', patient_id=case.patient.id) }}" class="btn btn-outline-primary btn-sm">
                        <i class="fas fa-history me-1"></i>查看患者病史
                    </a>
                </div>
            </div>
        </div>
        
        <!-- 医生笔记卡片 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
                <h6 class="m-0 font-weight-bold text-primary">医生笔记</h6>
                <button class="btn btn-sm btn-primary" data-bs-toggle="modal" data-bs-target="#addNoteModal">
                    <i class="fas fa-plus"></i>
                </button>
            </div>
            <div class="card-body">
                {% if case.doctor_notes %}
                    {% for note in case.doctor_notes %}
                    <div class="border-bottom pb-3 mb-3">
                        <div class="d-flex justify-content-between align-items-center mb-2">
                            <h6 class="mb-0">{{ note.title }}</h6>
                            <small class="text-muted">{{ note.date }}</small>
                        </div>
                        <p class="mb-1">{{ note.content }}</p>
                        <small class="text-muted">
                            <i class="fas fa-user-md me-1"></i>{{ note.doctor_name }}
                        </small>
                    </div>
                    {% endfor %}
                {% else %}
                    <div class="text-center py-4">
                        <i class="fas fa-clipboard fa-3x text-muted mb-3"></i>
                        <p class="mb-0">暂无医生笔记</p>
                        <button class="btn btn-sm btn-primary mt-3" data-bs-toggle="modal" data-bs-target="#addNoteModal">
                            添加笔记
                        </button>
                    </div>
                {% endif %}
            </div>
        </div>
        
        <!-- 相关病例卡片 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">相关病例</h6>
            </div>
            <div class="card-body">
                {% if case.related_cases %}
                    <div class="list-group">
                        {% for related in case.related_cases %}
                        <a href="{{ url_for('view_case', case_id=related.id) }}" class="list-group-item list-group-item-action">
                            <div class="d-flex w-100 justify-content-between">
                                <h6 class="mb-1">病例 #{{ related.id }}</h6>
                                <small>{{ related.similarity }}% 相似</small>
                            </div>
                            <p class="mb-1">{{ related.diagnosis }}</p>
                            <small class="text-muted">{{ related.date }}</small>
                        </a>
                        {% endfor %}
                    </div>
                {% else %}
                    <div class="text-center py-4">
                        <i class="fas fa-search fa-3x text-muted mb-3"></i>
                        <p class="mb-0">未找到相关病例</p>
                    </div>
                {% endif %}
            </div>
        </div>
    </div>
</div>

<!-- 添加笔记模态框 -->
<div class="modal fade" id="addNoteModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">添加医生笔记</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <form id="noteForm">
                    <div class="mb-3">
                        <label for="noteTitle" class="form-label">标题</label>
                        <input type="text" class="form-control" id="noteTitle" required>
                    </div>
                    <div class="mb-3">
                        <label for="noteContent" class="form-label">内容</label>
                        <textarea class="form-control" id="noteContent" rows="5" required></textarea>
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                <button type="button" class="btn btn-primary" onclick="saveNote()">保存</button>
            </div>
        </div>
    </div>
</div>

<!-- 分享模态框 -->
<div class="modal fade" id="shareModal" tabindex="-1" aria-hidden="true">
    <div class="modal-dialog">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title">分享病例</h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <div class="mb-3">
                    <label for="shareEmail" class="form-label">收件人邮箱</label>
                    <input type="email" class="form-control" id="shareEmail" placeholder="输入收件人邮箱">
                </div>
                <div class="mb-3">
                    <label for="shareMessage" class="form-label">附加信息</label>
                    <textarea class="form-control" id="shareMessage" rows="3" placeholder="输入附加信息(可选)"></textarea>
                </div>
                <div class="mb-3">
                    <label class="form-label">分享内容</label>
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" value="" id="shareDiagnosis" checked>
                        <label class="form-check-label" for="shareDiagnosis">
                            诊断结果
                        </label>
                    </div>
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" value="" id="shareImages" checked>
                        <label class="form-check-label" for="shareImages">
                            医学影像
                        </label>
                    </div>
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" value="" id="shareLab" checked>
                        <label class="form-check-label" for="shareLab">
                            实验室结果
                        </label>
                    </div>
                    <div class="form-check">
                        <input class="form-check-input" type="checkbox" value="" id="shareNotes">
                        <label class="form-check-label" for="shareNotes">
                            医生笔记
                        </label>
                    </div>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                <button type="button" class="btn btn-primary">分享</button>
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script>
    function saveNote() {
        const title = document.getElementById('noteTitle').value;
        const content = document.getElementById('noteContent').value;
        
        if (!title || !content) {
            alert('请填写标题和内容');
            return;
        }
        
        // 在实际应用中,这里会发送AJAX请求保存笔记
        fetch('{{ url_for("add_note", case_id=case.id) }}', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                title: title,
                content: content
            }),
        })
        .then(response => response.json())
        .then(data => {
            if (data.success) {
                // 关闭模态框并刷新页面
                $('#addNoteModal').modal('hide');
                location.reload();
            } else {
                alert('保存失败: ' + data.message);
            }
        })
        .catch(error => {
            console.error('Error:', error);
            alert('发生错误,请重试');
        });
    }
</script>
{% endblock %}

modules\user_interface\templates\patient\appointments.html

{% extends "layout.html" %}

{% block title %}预约管理 - 智能医疗辅助诊断系统{% endblock %}

{% block styles %}
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css">
<style>
    .appointment-card {
        transition: transform 0.2s;
        border-left: 4px solid #4e73df;
    }
    .appointment-card:hover {
        transform: translateY(-3px);
    }
    .fc-event {
        cursor: pointer;
    }
    .status-badge {
        position: absolute;
        top: 10px;
        right: 10px;
    }
    .filter-btn.active {
        background-color: #4e73df;
        color: white;
    }
    .doctor-avatar {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        object-fit: cover;
    }
</style>
{% endblock %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-6">
        <h1 class="h2 mb-0"><i class="fas fa-calendar-alt me-2"></i>预约管理</h1>
        <p class="text-muted">管理您的医疗预约,查看历史记录并安排新的预约。</p>
    </div>
    <div class="col-md-6 text-md-end">
        <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newAppointmentModal">
            <i class="fas fa-plus-circle me-1"></i>新建预约
        </button>
    </div>
</div>

<!-- 过滤器 -->
<div class="card shadow mb-4">
    <div class="card-body">
        <div class="row align-items-center">
            <div class="col-md-2">
                <label class="mb-0 fw-bold">过滤条件:</label>
            </div>
            <div class="col-md-10">
                <div class="d-flex flex-wrap gap-2">
                    <button class="btn btn-sm filter-btn active" data-filter="all">全部</button>
                    <button class="btn btn-sm filter-btn" data-filter="pending">待确认</button>
                    <button class="btn btn-sm filter-btn" data-filter="confirmed">已确认</button>
                    <button class="btn btn-sm filter-btn" data-filter="completed">已完成</button>
                    <button class="btn btn-sm filter-btn" data-filter="canceled">已取消</button>
                    
                    <div class="ms-auto">
                        <select class="form-select form-select-sm" id="sortAppointments">
                            <option value="date-asc">日期(最近优先)</option>
                            <option value="date-desc">日期(最远优先)</option>
                            <option value="department">按科室</option>
                            <option value="doctor">按医生</option>
                        </select>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 主要内容区 -->
<div class="row">
    <!-- 左侧:预约日历 -->
    <div class="col-lg-8">
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">预约日历</h6>
            </div>
            <div class="card-body">
                <div id="appointmentCalendar"></div>
            </div>
        </div>
        
        <!-- 即将到来的预约 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
                <h6 class="m-0 font-weight-bold text-primary">即将到来的预约</h6>
                <div class="dropdown no-arrow">
                    <a class="dropdown-toggle" href="#" role="button" id="upcomingDropdown" data-bs-toggle="dropdown" aria-expanded="false">
                        <i class="fas fa-ellipsis-v fa-sm fa-fw text-gray-400"></i>
                    </a>
                    <div class="dropdown-menu dropdown-menu-end shadow" aria-labelledby="upcomingDropdown">
                        <a class="dropdown-item" href="#" id="refreshUpcoming">
                            <i class="fas fa-sync fa-sm fa-fw me-2 text-gray-400"></i>
                            刷新
                        </a>
                        <a class="dropdown-item" href="#" id="exportUpcoming">
                            <i class="fas fa-download fa-sm fa-fw me-2 text-gray-400"></i>
                            导出日历
                        </a>
                    </div>
                </div>
            </div>
            <div class="card-body">
                <div id="upcomingAppointments">
                    {% if upcoming_appointments %}
                        {% for appointment in upcoming_appointments %}
                        <div class="card mb-3 appointment-card" data-status="{{ appointment.status }}" data-date="{{ appointment.date }}">
                            <div class="card-body">
                                <div class="row">
                                    <div class="col-md-2 text-center">
                                        <div class="bg-light rounded p-3 mb-2">
                                            <h5 class="mb-0">{{ appointment.day }}</h5>
                                            <small class="text-muted">{{ appointment.month }}</small>
                                        </div>
                                        <span class="badge 
                                            {% if appointment.status == '待确认' %}
                                                bg-warning
                                            {% elif appointment.status == '已确认' %}
                                                bg-success
                                            {% elif appointment.status == '已完成' %}
                                                bg-info
                                            {% elif appointment.status == '已取消' %}
                                                bg-secondary
                                            {% endif %}">
                                            {{ appointment.status }}
                                        </span>
                                    </div>
                                    <div class="col-md-7">
                                        <div class="d-flex align-items-center mb-2">
                                            <img src="{{ appointment.doctor_avatar }}" class="doctor-avatar me-2" alt="{{ appointment.doctor_name }}">
                                            <div>
                                                <h6 class="mb-0">{{ appointment.doctor_name }}</h6>
                                                <small class="text-muted">{{ appointment.department }}</small>
                                            </div>
                                        </div>
                                        <p class="mb-1">
                                            <i class="fas fa-clock text-muted me-1"></i>{{ appointment.time }}
                                        </p>
                                        <p class="mb-1">
                                            <i class="fas fa-map-marker-alt text-muted me-1"></i>{{ appointment.location }}
                                        </p>
                                        <p class="mb-0 text-muted small">
                                            <i class="fas fa-comment-medical me-1"></i>{{ appointment.purpose }}
                                        </p>
                                    </div>
                                    <div class="col-md-3 text-end">
                                        <div class="btn-group-vertical w-100">
                                            <a href="{{ url_for('view_appointment', appointment_id=appointment.id) }}" class="btn btn-sm btn-outline-primary mb-2">
                                                <i class="fas fa-eye me-1"></i>详情
                                            </a>
                                            {% if appointment.status == '待确认' or appointment.status == '已确认' %}
                                            <button class="btn btn-sm btn-outline-danger" onclick="cancelAppointment({{ appointment.id }})">
                                                <i class="fas fa-times me-1"></i>取消
                                            </button>
                                            {% endif %}
                                        </div>
                                    </div>
                                </div>
                            </div>
                        </div>
                        {% endfor %}
                    {% else %}
                        <div class="text-center py-5">
                            <i class="fas fa-calendar-times fa-4x text-muted mb-3"></i>
                            <h5>暂无即将到来的预约</h5>
                            <p class="text-muted mb-3">您可以点击"新建预约"按钮安排新的预约</p>
                            <button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#newAppointmentModal">
                                <i class="fas fa-plus-circle me-1"></i>新建预约
                            </button>
                        </div>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
    
    <!-- 右侧:医生列表和历史预约 -->
    <div class="col-lg-4">
        <!-- 热门医生 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">热门医生</h6>
            </div>
            <div class="card-body">
                <div class="popular-doctors">
                    {% if popular_doctors %}
                        {% for doctor in popular_doctors %}
                        <div class="d-flex align-items-center mb-3 pb-3 border-bottom">
                            <img src="{{ doctor.avatar }}" class="doctor-avatar me-3" alt="{{ doctor.name }}">
                            <div class="flex-grow-1">
                                <h6 class="mb-0">{{ doctor.name }}</h6>
                                <small class="text-muted">{{ doctor.department }} | {{ doctor.title }}</small>
                                <div class="text-warning small mt-1">
                                    {% for i in range(doctor.rating|int) %}
                                    <i class="fas fa-star"></i>
                                    {% endfor %}
                                    {% if doctor.rating % 1 != 0 %}
                                    <i class="fas fa-star-half-alt"></i>
                                    {% endif %}
                                    <span class="text-muted ms-1">({{ doctor.reviews }})</span>
                                </div>
                            </div>
                            <button class="btn btn-sm btn-outline-primary" 
                                    data-bs-toggle="modal" 
                                    data-bs-target="#newAppointmentModal" 
                                    data-doctor-id="{{ doctor.id }}" 
                                    data-doctor-name="{{ doctor.name }}">
                                预约
                            </button>
                        </div>
                        {% endfor %}
                    {% else %}
                        <div class="text-center py-4">
                            <p class="text-muted mb-0">暂无热门医生信息</p>
                        </div>
                    {% endif %}
                </div>
            </div>
        </div>
        
        <!-- 历史预约 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
                <h6 class="m-0 font-weight-bold text-primary">历史预约</h6>
                <a href="{{ url_for('appointment_history') }}" class="btn btn-sm btn-link">
                    查看全部
                </a>
            </div>
            <div class="card-body">
                <div id="historyAppointments">
                    {% if history_appointments %}
                        {% for appointment in history_appointments %}
                        <div class="d-flex align-items-center mb-3 pb-3 border-bottom">
                            <div class="bg-light rounded p-2 me-3 text-center" style="min-width: 50px;">
                                <small class="d-block text-muted">{{ appointment.month }}</small>
                                <span class="fw-bold">{{ appointment.day }}</span>
                            </div>
                            <div class="flex-grow-1">
                                <h6 class="mb-0">{{ appointment.doctor_name }}</h6>
                                <small class="text-muted">{{ appointment.department }}</small>
                                <div class="mt-1">
                                    <span class="badge 
                                        {% if appointment.status == '已完成' %}
                                            bg-success
                                        {% elif appointment.status == '已取消' %}
                                            bg-secondary
                                        {% endif %}">
                                        {{ appointment.status }}
                                    </span>
                                </div>
                            </div>
                            <a href="{{ url_for('view_appointment', appointment_id=appointment.id) }}" class="btn btn-sm btn-link">
                                <i class="fas fa-eye"></i>
                            </a>
                        </div>
                        {% endfor %}
                    {% else %}
                        <div class="text-center py-4">
                            <p class="text-muted mb-0">暂无历史预约记录</p>
                        </div>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 新建预约模态框 -->
<div class="modal fade" id="newAppointmentModal" tabindex="-1" aria-labelledby="newAppointmentModalLabel" aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title" id="newAppointmentModalLabel">
                    <i class="fas fa-calendar-plus me-2"></i>新建预约
                </h5>
                <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
            </div>
            <div class="modal-body">
                <form id="appointmentForm" action="{{ url_for('create_appointment') }}" method="post">
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label for="department" class="form-label">科室</label>
                            <select class="form-select" id="department" name="department" required>
                                <option value="" selected disabled>请选择科室</option>
                                <option value="内科">内科</option>
                                <option value="外科">外科</option>
                                <option value="妇产科">妇产科</option>
                                <option value="儿科">儿科</option>
                                <option value="眼科">眼科</option>
                                <option value="耳鼻喉科">耳鼻喉科</option>
                                <option value="口腔科">口腔科</option>
                                <option value="皮肤科">皮肤科</option>
                                <option value="神经科">神经科</option>
                                <option value="心理科">心理科</option>
                                <option value="中医科">中医科</option>
                            </select>
                        </div>
                        <div class="col-md-6">
                            <label for="doctor" class="form-label">医生</label>
                            <select class="form-select" id="doctor" name="doctor_id" required disabled>
                                <option value="" selected disabled>请先选择科室</option>
                            </select>
                        </div>
                    </div>
                    
                    <div class="row mb-3">
                        <div class="col-md-6">
                            <label for="appointmentDate" class="form-label">预约日期</label>
                            <input type="date" class="form-control" id="appointmentDate" name="appointment_date" required>
                        </div>
                        <div class="col-md-6">
                            <label for="appointmentTime" class="form-label">预约时间</label>
                            <select class="form-select" id="appointmentTime" name="appointment_time" required disabled>
                                <option value="" selected disabled>请先选择日期</option>
                            </select>
                        </div>
                    </div>
                    
                    <div class="mb-3">
                        <label for="purpose" class="form-label">预约原因</label>
                        <textarea class="form-control" id="purpose" name="purpose" rows="3" placeholder="请简要描述您的症状或就诊原因" required></textarea>
                    </div>
                    
                    <div class="mb-3">
                        <div class="form-check">
                            <input class="form-check-input" type="checkbox" id="urgentCheck" name="is_urgent">
                            <label class="form-check-label" for="urgentCheck">
                                紧急情况(如需紧急就诊,我们将优先安排)
                            </label>
                        </div>
                    </div>
                    
                    <div class="alert alert-info" role="alert">
                        <i class="fas fa-info-circle me-2"></i>预约成功后,您将收到短信和邮件通知。如需取消预约,请提前24小时操作。
                    </div>
                </form>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
                <button type="submit" form="appointmentForm" class="btn btn-primary">
                    <i class="fas fa-calendar-check me-1"></i>确认预约
                </button>
            </div>
        </div>
    </div>
</div>

{% endblock %}

{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js"></script>
<script>
    document.addEventListener('DOMContentLoaded', function() {
        // 初始化日历
        const calendarEl = document.getElementById('appointmentCalendar');
        const calendar = new FullCalendar.Calendar(calendarEl, {
            initialView: 'dayGridMonth',
            headerToolbar: {
                left: 'prev,next today',
                center: 'title',
                right: 'dayGridMonth,timeGridWeek'
            },
            locale: 'zh-cn',
            buttonText: {
                today: '今天',
                month: '月',
                week: '周'
            },
            events: [
                // 这里应该从后端获取预约数据
                {% if all_appointments %}
                    {% for appointment in all_appointments %}
                    {
                        title: '{{ appointment.purpose }}',
                        start: '{{ appointment.date }}T{{ appointment.time }}',
                        allDay: false,
                        backgroundColor: 
                            {% if appointment.status == '待确认' %}
                                '#f6c23e'
                            {% elif appointment.status == '已确认' %}
                                '#4e73df'
                            {% elif appointment.status == '已完成' %}
                                '#1cc88a'
                            {% elif appointment.status == '已取消' %}
                                '#858796'
                            {% endif %},
                        borderColor: 
                            {% if appointment.status == '待确认' %}
                                '#f6c23e'
                            {% elif appointment.status == '已确认' %}
                                '#4e73df'
                            {% elif appointment.status == '已完成' %}
                                '#1cc88a'
                            {% elif appointment.status == '已取消' %}
                                '#858796'
                            {% endif %},
                        textColor: '#fff',
                        extendedProps: {
                            appointmentId: '{{ appointment.id }}',
                            doctor: '{{ appointment.doctor_name }}',
                            department: '{{ appointment.department }}',
                            status: '{{ appointment.status }}'
                        }
                    }{% if not loop.last %},{% endif %}
                    {% endfor %}
                {% endif %}
            ],
            eventClick: function(info) {
                // 点击事件时显示预约详情
                const appointmentId = info.event.extendedProps.appointmentId;
                window.location.href = "{{ url_for('view_appointment', appointment_id='') }}" + appointmentId;
            },
            dateClick: function(info) {
                // 点击日期时打开新建预约模态框,并预设日期
                const modal = new bootstrap.Modal(document.getElementById('newAppointmentModal'));
                document.getElementById('appointmentDate').value = info.dateStr;
                
                // 触发日期变更事件,加载可用时间
                const event = new Event('change');
                document.getElementById('appointmentDate').dispatchEvent(event);
                
                modal.show();
            }
        });
        calendar.render();
        
        // 过滤按钮点击事件
        const filterButtons = document.querySelectorAll('.filter-btn');
        filterButtons.forEach(button => {
            button.addEventListener('click', function() {
                // 移除所有按钮的active类
                filterButtons.forEach(btn => btn.classList.remove('active'));
                // 为当前按钮添加active类
                this.classList.add('active');
                
                // 获取过滤条件
                const filter = this.getAttribute('data-filter');
                filterAppointments(filter);
            });
        });
        
        // 排序下拉框变化事件
        document.getElementById('sortAppointments').addEventListener('change', function() {
            sortAppointments(this.value);
        });
        
        // 科室选择变化事件
        document.getElementById('department').addEventListener('change', function() {
            const department = this.value;
            const doctorSelect = document.getElementById('doctor');
            
            // 清空并禁用医生选择
            doctorSelect.innerHTML = '<option value="" selected disabled>加载中...</option>';
            
            // 这里应该从后端获取对应科室的医生数据
            // 模拟异步加载
            setTimeout(() => {
                doctorSelect.innerHTML = '';
                doctorSelect.appendChild(new Option('请选择医生', '', true, true));
                
                // 模拟数据,实际应从后端获取
                const doctors = {
                    '内科': [
                        {id: 1, name: '张医生'},
                        {id: 2, name: '李医生'}
                    ],
                    '外科': [
                        {id: 3, name: '王医生'},
                        {id: 4, name: '赵医生'}
                    ],
                    '妇产科': [
                        {id: 5, name: '刘医生'},
                        {id: 6, name: '陈医生'}
                    ]
                };
                
                if (doctors[department]) {
                    doctors[department].forEach(doctor => {
                        doctorSelect.appendChild(new Option(doctor.name, doctor.id));
                    });
                    doctorSelect.disabled = false;
                } else {
                    doctorSelect.appendChild(new Option('暂无可用医生', '', true, true));
                    doctorSelect.disabled = true;
                }
            }, 500);
        });
        
        // 日期选择变化事件
        document.getElementById('appointmentDate').addEventListener('change', function() {
            const date = this.value;
            const timeSelect = document.getElementById('appointmentTime');
            const doctorSelect = document.getElementById('doctor');
            const doctorId = doctorSelect.value;
            
            // 如果没有选择医生,提示先选择医生
            if (!doctorId) {
                alert('请先选择科室和医生');
                return;
            }
            
            // 清空并禁用时间选择
            timeSelect.innerHTML = '<option value="" selected disabled>加载中...</option>';
            
            // 这里应该从后端获取对应日期和医生的可用时间
            // 模拟异步加载
            setTimeout(() => {
                timeSelect.innerHTML = '';
                timeSelect.appendChild(new Option('请选择时间', '', true, true));
                
                // 模拟数据,实际应从后端获取
                const availableTimes = [
                    '08:00',
                    '08:30',
                    '09:00',
                    '09:30',
                    '10:00',
                    '10:30',
                    '14:00',
                    '14:30',
                    '15:00',
                    '15:30',
                    '16:00'
                ];
                
                availableTimes.forEach(time => {
                    timeSelect.appendChild(new Option(time, time));
                });
                timeSelect.disabled = false;
            }, 500);
        });
        
        // 过滤预约函数
        function filterAppointments(filter) {
            const appointments = document.querySelectorAll('.appointment-card');
            let visibleCount = 0;
            
            appointments.forEach(appointment => {
                let show = false;
                const status = appointment.getAttribute('data-status');
                
                if (filter === 'all') {
                    show = true;
                } else if (filter === 'pending' && status === '待确认') {
                    show = true;
                } else if (filter === 'confirmed' && status === '已确认') {
                    show = true;
                } else if (filter === 'completed' && status === '已完成') {
                    show = true;
                } else if (filter === 'canceled' && status === '已取消') {
                    show = true;
                }
                
                if (show) {
                    appointment.style.display = '';
                    visibleCount++;
                } else {
                    appointment.style.display = 'none';
                }
            });
            
            // 显示或隐藏"无预约"提示
            const noAppointmentsMessage = document.querySelector('#upcomingAppointments .text-center');
            if (noAppointmentsMessage) {
                if (visibleCount === 0) {
                    noAppointmentsMessage.style.display = '';
                } else {
                    noAppointmentsMessage.style.display = 'none';
                }
            }
        }
        
        // 排序预约函数
        function sortAppointments(sortType) {
            const container = document.getElementById('upcomingAppointments');
            const appointments = Array.from(container.querySelectorAll('.appointment-card'));
            
            if (appointments.length <= 1) return;
            
            appointments.sort((a, b) => {
                const dateA = new Date(a.getAttribute('data-date'));
                const dateB = new Date(b.getAttribute('data-date'));
                
                if (sortType === 'date-asc') {
                    return dateA - dateB;
                } else if (sortType === 'date-desc') {
                    return dateB - dateA;
                } else if (sortType === 'department') {
                    const deptA = a.querySelector('small.text-muted').textContent;
                    const deptB = b.querySelector('small.text-muted').textContent;
                    return deptA.localeCompare(deptB);
                } else if (sortType === 'doctor') {
                    const docA = a.querySelector('h6.mb-0').textContent;
                    const docB = b.querySelector('h6.mb-0').textContent;
                    return docA.localeCompare(docB);
                }
            });
            
            // 重新添加排序后的预约
            appointments.forEach(appointment => {
                container.appendChild(appointment);
            });
        }
        
        // 取消预约函数
        window.cancelAppointment = function(appointmentId) {
            if (confirm('确定要取消此预约吗?')) {
                // 这里应该发送请求到后端取消预约
                // 模拟异步请求
                alert('预约已取消');
                // 实际应该刷新页面或更新UI
                location.reload();
            }
        };
        
        // 预约按钮点击事件(从热门医生列表)
        const doctorButtons = document.querySelectorAll('[data-doctor-id]');
        doctorButtons.forEach(button => {
            button.addEventListener('click', function() {
                const doctorId = this.getAttribute('data-doctor-id');
                const doctorName = this.getAttribute('data-doctor-name');
                
                // 获取医生所在科室
                const department = this.closest('.d-flex').querySelector('small.text-muted').textContent.split('|')[0].trim();
                
                // 设置科室选择
                const departmentSelect = document.getElementById('department');
                for (let i = 0; i < departmentSelect.options.length; i++) {
                    if (departmentSelect.options[i].text === department) {
                        departmentSelect.selectedIndex = i;
                        break;
                    }
                }
                
                // 触发科室变更事件
                const event = new Event('change');
                departmentSelect.dispatchEvent(event);
                
                // 延迟设置医生选择(等待科室变更事件完成)
                setTimeout(() => {
                    const doctorSelect = document.getElementById('doctor');
                    for (let i = 0; i < doctorSelect.options.length; i++) {
                        if (doctorSelect.options[i].value === doctorId) {
                            doctorSelect.selectedIndex = i;
                            break;
                        }
                    }
                }, 600);
            });
        });
    });
</script>
{% endblock %}

modules\user_interface\templates\patient\dashboard.html

{% extends "layout.html" %}

{% block title %}患者主页 - 智能医疗辅助诊断系统{% endblock %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-6">
        <h1 class="h2 mb-0"><i class="fas fa-user-circle me-2"></i>患者主页</h1>
        <p class="text-muted">欢迎回来,{{ g.user.name }}。</p>
    </div>
    <div class="col-md-6 text-md-end">
        <a href="{{ url_for('appointments') }}" class="btn btn-primary">
            <i class="fas fa-calendar-plus me-1"></i>预约就诊
        </a>
    </div>
</div>

<!-- 健康状态卡片 -->
<div class="row mb-4">
    <div class="col-xl-3 col-md-6 mb-4">
        <div class="card border-left-primary shadow h-100 py-2">
            <div class="card-body">
                <div class="row no-gutters align-items-center">
                    <div class="col mr-2">
                        <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">总诊断记录</div>
                        <div class="h5 mb-0 font-weight-bold text-gray-800">{{ statistics.total_records }}</div>
                    </div>
                    <div class="col-auto">
                        <i class="fas fa-clipboard-list fa-2x text-gray-300"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="col-xl-3 col-md-6 mb-4">
        <div class="card border-left-success shadow h-100 py-2">
            <div class="card-body">
                <div class="row no-gutters align-items-center">
                    <div class="col mr-2">
                        <div class="text-xs font-weight-bold text-success text-uppercase mb-1">健康指数</div>
                        <div class="h5 mb-0 font-weight-bold text-gray-800">{{ statistics.health_index }}%</div>
                    </div>
                    <div class="col-auto">
                        <i class="fas fa-heartbeat fa-2x text-gray-300"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="col-xl-3 col-md-6 mb-4">
        <div class="card border-left-info shadow h-100 py-2">
            <div class="card-body">
                <div class="row no-gutters align-items-center">
                    <div class="col mr-2">
                        <div class="text-xs font-weight-bold text-info text-uppercase mb-1">下次复诊</div>
                        <div class="h5 mb-0 font-weight-bold text-gray-800">{{ statistics.next_appointment }}</div>
                    </div>
                    <div class="col-auto">
                        <i class="fas fa-calendar fa-2x text-gray-300"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <div class="col-xl-3 col-md-6 mb-4">
        <div class="card border-left-warning shadow h-100 py-2">
            <div class="card-body">
                <div class="row no-gutters align-items-center">
                    <div class="col mr-2">
                        <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">待处理提醒</div>
                        <div class="h5 mb-0 font-weight-bold text-gray-800">{{ statistics.pending_alerts }}</div>
                    </div>
                    <div class="col-auto">
                        <i class="fas fa-bell fa-2x text-gray-300"></i>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 最近诊断记录 -->
<div class="card shadow mb-4">
    <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
        <h6 class="m-0 font-weight-bold text-primary">最近诊断记录</h6>
        <a href="{{ url_for('medical_records') }}" class="btn btn-sm btn-primary">
            查看全部
        </a>
    </div>
    <div class="card-body">
        <div class="table-responsive">
            <table class="table table-bordered table-hover" width="100%" cellspacing="0">
                <thead>
                    <tr>
                        <th>日期</th>
                        <th>主要诊断</th>
                        <th>主治医生</th>
                        <th>严重程度</th>
                        <th>状态</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    {% for record in recent_records %}
                    <tr>
                        <td>{{ record.date }}</td>
                        <td>{{ record.primary_diagnosis }}</td>
                        <td>{{ record.doctor_name }}</td>
                        <td>
                            <div class="d-flex align-items-center">
                                <div class="progress flex-grow-1" style="height: 10px;">
                                    <div class="progress-bar 
                                        {% if record.severity == '轻度' %}
                                            bg-success
                                        {% elif record.severity == '中度' %}
                                            bg-warning
                                        {% elif record.severity == '重度' %}
                                            bg-danger
                                        {% else %}
                                            bg-info
                                        {% endif %}"
                                        role="progressbar" 
                                        style="width: 
                                        {% if record.severity == '轻度' %}
                                            30%
                                        {% elif record.severity == '中度' %}
                                            60%
                                        {% elif record.severity == '重度' %}
                                            90%
                                        {% else %}
                                            45%
                                        {% endif %}">
                                    </div>
                                </div>
                                <span class="ms-2">{{ record.severity }}</span>
                            </div>
                        </td>
                        <td>
                            <span class="badge 
                                {% if record.status == '已完成' %}
                                    bg-success
                                {% elif record.status == '治疗中' %}
                                    bg-primary
                                {% elif record.status == '待复诊' %}
                                    bg-warning
                                {% else %}
                                    bg-secondary
                                {% endif %}">
                                {{ record.status }}
                            </span>
                        </td>
                        <td>
                            <a href="{{ url_for('view_record', record_id=record.id) }}" class="btn btn-sm btn-primary">
                                <i class="fas fa-eye"></i> 查看
                            </a>
                        </td>
                    </tr>
                    {% endfor %}
                </tbody>
            </table>
        </div>
    </div>
</div>

<!-- 健康趋势和提醒 -->
<div class="row">
    <!-- 健康趋势图 -->
    <div class="col-xl-8 col-lg-7">
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">健康趋势</h6>
            </div>
            <div class="card-body">
                <div class="chart-area">
                    <canvas id="healthTrendChart"></canvas>
                </div>
                <div class="mt-4 text-center small">
                    <span class="me-2">
                        <i class="fas fa-circle text-primary"></i> 血压
                    </span>
                    <span class="me-2">
                        <i class="fas fa-circle text-success"></i> 血糖
                    </span>
                    <span class="me-2">
                        <i class="fas fa-circle text-info"></i> 体重
                    </span>
                </div>
            </div>
        </div>
    </div>

    <!-- 健康提醒 -->
    <div class="col-xl-4 col-lg-5">
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">健康提醒</h6>
            </div>
            <div class="card-body">
                <div class="list-group">
                    {% for alert in health_alerts %}
                    <div class="list-group-item list-group-item-action">
                        <div class="d-flex w-100 justify-content-between">
                            <h6 class="mb-1">{{ alert.title }}</h6>
                            <small class="text-muted">{{ alert.date }}</small>
                        </div>
                        <p class="mb-1">{{ alert.content }}</p>
                        <small class="text-muted">
                            <i class="fas 
                                {% if alert.priority == '高' %}
                                    fa-exclamation-circle text-danger
                                {% elif alert.priority == '中' %}
                                    fa-exclamation-triangle text-warning
                                {% else %}
                                    fa-info-circle text-info
                                {% endif %}
                            "></i>
                            优先级: {{ alert.priority }}
                        </small>
                    </div>
                    {% endfor %}
                </div>
            </div>
        </div>
        
        <!-- 即将到来的预约 -->
        <div class="card shadow mb-4">
            <div class="card-header py-3">
                <h6 class="m-0 font-weight-bold text-primary">即将到来的预约</h6>
            </div>
            <div class="card-body">
                {% if upcoming_appointments %}
                <div class="list-group">
                    {% for appointment in upcoming_appointments %}
                    <div class="list-group-item list-group-item-action">
                        <div class="d-flex w-100 justify-content-between">
                            <h6 class="mb-1">{{ appointment.doctor_name }}</h6>
                            <small class="text-primary">{{ appointment.date }}</small>
                        </div>
                        <p class="mb-1">{{ appointment.department }} - {{ appointment.purpose }}</p>
                        <small class="text-muted">
                            <i class="fas fa-clock"></i> {{ appointment.time }}
                            <span class="ms-2"><i class="fas fa-map-marker-alt"></i> {{ appointment.location }}</span>
                        </small>
                    </div>
                    {% endfor %}
                </div>
                {% else %}
                <div class="text-center py-4">
                    <i class="fas fa-calendar-times fa-3x text-muted mb-3"></i>
                    <p class="mb-0">暂无即将到来的预约</p>
                    <a href="{{ url_for('appointments') }}" class="btn btn-sm btn-primary mt-3">
                        预约就诊
                    </a>
                </div>
                {% endif %}
            </div>
        </div>
    </div>
</div>
{% endblock %}

{% block scripts %}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
    // 健康趋势图
    var ctx = document.getElementById("healthTrendChart");
    var myLineChart = new Chart(ctx, {
        type: 'line',
        data: {
            labels: ["1月", "2月", "3月", "4月", "5月", "6月"],
            datasets: [
                {
                    label: "血压",
                    lineTension: 0.3,
                    backgroundColor: "rgba(78, 115, 223, 0.05)",
                    borderColor: "rgba(78, 115, 223, 1)",
                    pointRadius: 3,
                    pointBackgroundColor: "rgba(78, 115, 223, 1)",
                    pointBorderColor: "rgba(78, 115, 223, 1)",
                    pointHoverRadius: 3,
                    pointHoverBackgroundColor: "rgba(78, 115, 223, 1)",
                    pointHoverBorderColor: "rgba(78, 115, 223, 1)",
                    pointHitRadius: 10,
                    pointBorderWidth: 2,
                    data: [120, 125, 123, 118, 115, 117],
                },
                {
                    label: "血糖",
                    lineTension: 0.3,
                    backgroundColor: "rgba(40, 167, 69, 0.05)",
                    borderColor: "rgba(40, 167, 69, 1)",
                    pointRadius: 3,
                    pointBackgroundColor: "rgba(40, 167, 69, 1)",
                    pointBorderColor: "rgba(40, 167, 69, 1)",
                    pointHoverRadius: 3,
                    pointHoverBackgroundColor: "rgba(40, 167, 69, 1)",
                    pointHoverBorderColor: "rgba(40, 167, 69, 1)",
                    pointHitRadius: 10,
                    pointBorderWidth: 2,
                    data: [5.8, 6.2, 6.0, 5.9, 5.7, 5.6],
                },
                {
                    label: "体重",
                    lineTension: 0.3,
                    backgroundColor: "rgba(23, 162, 184, 0.05)",
                    borderColor: "rgba(23, 162, 184, 1)",
                    pointRadius: 3,
                    pointBackgroundColor: "rgba(23, 162, 184, 1)",
                    pointBorderColor: "rgba(23, 162, 184, 1)",
                    pointHoverRadius: 3,
                    pointHoverBackgroundColor: "rgba(23, 162, 184, 1)",
                    pointHoverBorderColor: "rgba(23, 162, 184, 1)",
                    pointHitRadius: 10,
                    pointBorderWidth: 2,
                    data: [75, 74, 73.5, 72, 71.5, 71],
                }
            ],
        },
        options: {
            maintainAspectRatio: false,
            scales: {
                y: {
                    beginAtZero: false
                }
            }
        }
    });
</script>
{% endblock %}

modules\user_interface\templates\patient\medical_records.html

{% extends "layout.html" %}

{% block title %}病历记录 - 智能医疗辅助诊断系统{% endblock %}

{% block styles %}
<style>
    .record-card {
        transition: transform 0.2s;
    }
    .record-card:hover {
        transform: translateY(-5px);
    }
    .record-badge {
        position: absolute;
        top: 10px;
        right: 10px;
    }
    .filter-btn.active {
        background-color: #4e73df;
        color: white;
    }
</style>
{% endblock %}

{% block content %}
<div class="row mb-4">
    <div class="col-md-6">
        <h1 class="h2 mb-0"><i class="fas fa-file-medical-alt me-2"></i>我的病历记录</h1>
        <p class="text-muted">查看您的所有诊断记录和治疗历史。</p>
    </div>
    <div class="col-md-6 text-md-end">
        <div class="input-group">
            <input type="text" class="form-control" placeholder="搜索病历..." id="recordSearch">
            <button class="btn btn-primary" type="button">
                <i class="fas fa-search"></i>
            </button>
        </div>
    </div>
</div>

<!-- 过滤器 -->
<div class="card shadow mb-4">
    <div class="card-body">
        <div class="row align-items-center">
            <div class="col-md-2">
                <label class="mb-0 fw-bold">过滤条件:</label>
            </div>
            <div class="col-md-10">
                <div class="d-flex flex-wrap gap-2">
                    <button class="btn btn-sm filter-btn active" data-filter="all">全部</button>
                    <button class="btn btn-sm filter-btn" data-filter="recent">最近3个月</button>
                    <button class="btn btn-sm filter-btn" data-filter="completed">已完成</button>
                    <button class="btn btn-sm filter-btn" data-filter="ongoing">治疗中</button>
                    <button class="btn btn-sm filter-btn" data-filter="followup">待复诊</button>
                    
                    <div class="ms-auto">
                        <select class="form-select form-select-sm" id="sortRecords">
                            <option value="date-desc">日期(最新优先)</option>
                            <option value="date-asc">日期(最早优先)</option>
                            <option value="severity-desc">严重程度(高到低)</option>
                            <option value="severity-asc">严重程度(低到高)</option>
                        </select>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

<!-- 病历记录 -->
<div class="row" id="recordsContainer">
    {% for record in medical_records %}
    <div class="col-md-6 col-xl-4 mb-4 record-item" 
         data-date="{{ record.date }}" 
         data-status="{{ record.status }}" 
         data-severity="{{ record.severity_level }}">
        <div class="card shadow record-card h-100">
            <div class="card-header py-3 d-flex flex-row align-items-center justify-content-between">
                <h6 class="m-0 font-weight-bold text-primary">{{ record.date }}</h6>
                <span class="badge 
                    {% if record.status == '已完成' %}
                        bg-success
                    {% elif record.status == '治疗中' %}
                        bg-primary
                    {% elif record.status == '待复诊' %}
                        bg-warning
                    {% else %}
                        bg-secondary
                    {% endif %} record-badge">
                    {{ record.status }}
                </span>
            </div>
            <div class="card-body">
                <h5 class="card-title">{{ record.primary_diagnosis }}</h5>
                <p class="card-text text-muted mb-3">{{ record.description }}</p>
                
                <div class="mb-3">
                    <div class="d-flex justify-content-between align-items-center mb-1">
                        <span>严重程度</span>
                        <span class="
                            {% if record.severity == '轻度' %}
                                text-success
                            {% elif record.severity == '中度' %}
                                text-warning
                            {% elif record.severity == '重度' %}
                                text-danger
                            {% else %}
                                text-info
                            {% endif %}">
                            {{ record.severity }}
                        </span>
                    </div>
                    <div class="progress" style="height: 8px;">
                        <div class="progress-bar 
                            {% if record.severity == '轻度' %}
                                bg-success
                            {% elif record.severity == '中度' %}
                                bg-warning
                            {% elif record.severity == '重度' %}
                                bg-danger
                            {% else %}
                                bg-info
                            {% endif %}"
                            role="progressbar" 
                            style="width: 
                            {% if record.severity == '轻度' %}
                                30%
                            {% elif record.severity == '中度' %}
                                60%
                            {% elif record.severity == '重度' %}
                                90%
                            {% else %}
                                45%
                            {% endif %}">
                        </div>
                    </div>
                </div>
                
                <div class="d-flex justify-content-between align-items-center">
                    <div>
                        <small class="text-muted">
                            <i class="fas fa-user-md me-1"></i>{{ record.doctor_name }}
                        </small>
                    </div>
                    <a href="{{ url_for('view_record', record_id=record.id) }}" class="btn btn-sm btn-primary">
                        <i class="fas fa-eye me-1"></i>查看详情
                    </a>
                </div>
            </div>
            <div class="card-footer bg-light">
                <div class="d-flex justify-content-between align-items-center">
                    <small class="text-muted">
                        <i class="fas fa-hospital me-1"></i>{{ record.department }}
                    </small>
                    {% if record.has_followup %}
                    <small class="text-primary">
                        <i class="fas fa-calendar-check me-1"></i>下次复诊: {{ record.followup_date }}
                    </small>
                    {% endif %}
                </div>
            </div>
        </div>
    </div>
    {% endfor %}
</div>

<!-- 无记录提示 -->
<div id="noRecordsMessage" class="text-center py-5 d-none">
    <i class="fas fa-folder-open fa-4x text-muted mb-3"></i>
    <h5>暂无符合条件的病历记录</h5>
    <p class="text-muted">请尝试调整搜索条件或过滤器</p>
</div>

<!-- 分页 -->
<nav aria-label="病历分页" class="mt-4">
    <ul class="pagination justify-content-center">
        <li class="page-item disabled">
            <a class="page-link" href="#" tabindex="-1" aria-disabled="true">上一页</a>
        </li>
        <li class="page-item active"><a class="page-link" href="#">1</a></li>
        <li class="page-item"><a class="page-link" href="#">2</a></li>
        <li class="page-item"><a class="page-link" href="#">3</a></li>
        <li class="page-item">
            <a class="page-link" href="#">下一页</a>
        </li>
    </ul>
</nav>
{% endblock %}

{% block scripts %}
<script>
    document.addEventListener('DOMContentLoaded', function() {
        // 过滤按钮点击事件
        const filterButtons = document.querySelectorAll('.filter-btn');
        filterButtons.forEach(button => {
            button.addEventListener('click', function() {
                // 移除所有按钮的active类
                filterButtons.forEach(btn => btn.classList.remove('active'));
                // 为当前按钮添加active类
                this.classList.add('active');
                
                // 获取过滤条件
                const filter = this.getAttribute('data-filter');
                filterRecords(filter);
            });
        });
        
        // 排序下拉框变化事件
        document.getElementById('sortRecords').addEventListener('change', function() {
            sortRecords(this.value);
        });
        
        // 搜索框输入事件
        document.getElementById('recordSearch').addEventListener('input', function() {
            searchRecords(this.value);
        });
        
        // 过滤记录函数
        function filterRecords(filter) {
            const records = document.querySelectorAll('.record-item');
            let visibleCount = 0;
            
            records.forEach(record => {
                let show = false;
                const status = record.getAttribute('data-status');
                const date = new Date(record.getAttribute('data-date'));
                const threeMonthsAgo = new Date();
                threeMonthsAgo.setMonth(threeMonthsAgo.getMonth() - 3);
                
                if (filter === 'all') {
                    show = true;
                } else if (filter === 'recent' && date >= threeMonthsAgo) {
                    show = true;
                } else if (filter === 'completed' && status === '已完成') {
                    show = true;
                } else if (filter === 'ongoing' && status === '治疗中') {
                    show = true;
                } else if (filter === 'followup' && status === '待复诊') {
                    show = true;
                }
                
                if (show) {
                    record.style.display = '';
                    visibleCount++;
                } else {
                    record.style.display = 'none';
                }
            });
            
            // 显示或隐藏"无记录"提示
            const noRecordsMessage = document.getElementById('noRecordsMessage');
            if (visibleCount === 0) {
                noRecordsMessage.classList.remove('d-none');
            } else {
                noRecordsMessage.classList.add('d-none');
            }
        }
        
        // 排序记录函数
        function sortRecords(sortType) {
            const recordsContainer = document.getElementById('recordsContainer');
            const records = Array.from(document.querySelectorAll('.record-item'));
            
            records.sort((a, b) => {
                if (sortType === 'date-desc') {
                    return new Date(b.getAttribute('data-date')) - new Date(a.getAttribute('data-date'));
                } else if (sortType === 'date-asc') {
                    return new Date(a.getAttribute('data-date')) - new Date(b.getAttribute('data-date'));
                } else if (sortType === 'severity-desc') {
                    return parseInt(b.getAttribute('data-severity')) - parseInt(a.getAttribute('data-severity'));
                } else if (sortType === 'severity-asc') {
                    return parseInt(a.getAttribute('data-severity')) - parseInt(b.getAttribute('data-severity'));
                }
            });
            
            // 重新添加排序后的记录
            records.forEach(record => {
                recordsContainer.appendChild(record);
            });
        }
        
        // 搜索记录函数
        function searchRecords(query) {
            query = query.toLowerCase();
            const records = document.querySelectorAll('.record-item');
            let visibleCount = 0;
            
            records.forEach(record => {
                const title = record.querySelector('.card-title').textContent.toLowerCase();
                const description = record.querySelector('.card-text').textContent.toLowerCase();
                const doctor = record.querySelector('.text-muted i.fa-user-md').parentNode.textContent.toLowerCase();
                
                if (title.includes(query) || description.includes(query) || doctor.includes(query)) {
                    record.style.display = '';
                    visibleCount++;
                } else {
                    record.style.display = 'none';
                }
            });
            
            // 显示或隐藏"无记录"提示
            const noRecordsMessage = document.getElementById('noRecordsMessage');
            if (visibleCount === 0) {
                noRecordsMessage.classList.remove('d-none');
            } else {
                noRecordsMessage.classList.add('d-none');
            }
        }
    });
</script>
{% endblock %}

modules\utils\file_utils.py

"""
File Utility Functions for Medical Diagnosis System

This module provides utility functions for file operations, including:
- File validation
- Directory operations
- File conversion
- Path handling
"""

import os
import shutil
import json
import logging
import hashlib
from pathlib import Path
from typing import List, Dict, Any, Optional, Union, Tuple

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def ensure_directory(directory_path: str) -> bool:
    """
    Ensure that a directory exists, create it if it doesn't
    
    Args:
        directory_path: Path to the directory
        
    Returns:
        bool: True if successful, False otherwise
    """
    try:
        os.makedirs(directory_path, exist_ok=True)
        return True
    except Exception as e:
        logger.error(f"Error creating directory {directory_path}: {str(e)}")
        return False

def list_files(directory_path: str, extensions: Optional[List[str]] = None, recursive: bool = False) -> List[str]:
    """
    List all files in a directory with optional extension filtering
    
    Args:
        directory_path: Path to the directory
        extensions: List of file extensions to include (e.g., ['.jpg', '.png'])
        recursive: Whether to search recursively in subdirectories
        
    Returns:
        List[str]: List of file paths
    """
    if not os.path.isdir(directory_path):
        logger.error(f"Directory not found: {directory_path}")
        return []
    
    file_list = []
    
    if recursive:
        for root, _, files in os.walk(directory_path):
            for file in files:
                file_path = os.path.join(root, file)
                if extensions is None or any(file.lower().endswith(ext.lower()) for ext in extensions):
                    file_list.append(file_path)
    else:
        for file in os.listdir(directory_path):
            file_path = os.path.join(directory_path, file)
            if os.path.isfile(file_path) and (extensions is None or any(file.lower().endswith(ext.lower()) for ext in extensions)):
                file_list.append(file_path)
    
    return file_list

def get_file_hash(file_path: str, algorithm: str = 'md5') -> str:
    """
    Calculate the hash of a file
    
    Args:
        file_path: Path to the file
        algorithm: Hash algorithm to use ('md5', 'sha1', 'sha256')
        
    Returns:
        str: Hash of the file
    """
    if not os.path.isfile(file_path):
        logger.error(f"File not found: {file_path}")
        return ""
    
    hash_algorithms = {
        'md5': hashlib.md5,
        'sha1': hashlib.sha1,
        'sha256': hashlib.sha256
    }
    
    if algorithm not in hash_algorithms:
        logger.error(f"Unsupported hash algorithm: {algorithm}")
        return ""
    
    hash_func = hash_algorithms[algorithm]()
    
    try:
        with open(file_path, 'rb') as f:
            # Read in chunks to handle large files
            for chunk in iter(lambda: f.read(4096), b''):
                hash_func.update(chunk)
        
        return hash_func.hexdigest()
    except Exception as e:
        logger.error(f"Error calculating hash for {file_path}: {str(e)}")
        return ""

def copy_file_with_metadata(src_path: str, dest_path: str) -> bool:
    """
    Copy a file while preserving metadata
    
    Args:
        src_path: Source file path
        dest_path: Destination file path
        
    Returns:
        bool: True if successful, False otherwise
    """
    if not os.path.isfile(src_path):
        logger.error(f"Source file not found: {src_path}")
        return False
    
    try:
        # Create destination directory if it doesn't exist
        dest_dir = os.path.dirname(dest_path)
        os.makedirs(dest_dir, exist_ok=True)
        
        # Copy the file with metadata
        shutil.copy2(src_path, dest_path)
        
        logger.info(f"Successfully copied {src_path} to {dest_path}")
        return True
    except Exception as e:
        logger.error(f"Error copying {src_path} to {dest_path}: {str(e)}")
        return False

def save_json(data: Any, file_path: str, indent: int = 2) -> bool:
    """
    Save data to a JSON file
    
    Args:
        data: Data to save
        file_path: Path to the file
        indent: JSON indentation level
        
    Returns:
        bool: True if successful, False otherwise
    """
    try:
        # Create directory if it doesn't exist
        os.makedirs(os.path.dirname(file_path), exist_ok=True)
        
        with open(file_path, 'w', encoding='utf-8') as f:
            json.dump(data, f, indent=indent)
        
        logger.info(f"Successfully saved JSON data to {file_path}")
        return True
    except Exception as e:
        logger.error(f"Error saving JSON data to {file_path}: {str(e)}")
        return False

def load_json(file_path: str) -> Dict[str, Any]:
    """
    Load data from a JSON file
    
    Args:
        file_path: Path to the file
        
    Returns:
        Dict: Loaded JSON data or empty dict if error
    """
    if not os.path.isfile(file_path):
        logger.error(f"JSON file not found: {file_path}")
        return {}
    
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        logger.info(f"Successfully loaded JSON data from {file_path}")
        return data
    except Exception as e:
        logger.error(f"Error loading JSON data from {file_path}: {str(e)}")
        return {}

def get_file_size(file_path: str, format: str = 'bytes') -> Union[int, float, str]:
    """
    Get the size of a file in various formats
    
    Args:
        file_path: Path to the file
        format: Format to return ('bytes', 'kb', 'mb', 'gb', 'auto')
        
    Returns:
        Union[int, float, str]: File size in the specified format
    """
    if not os.path.isfile(file_path):
        logger.error(f"File not found: {file_path}")
        return 0
    
    size_bytes = os.path.getsize(file_path)
    
    if format == 'bytes':
        return size_bytes
    elif format == 'kb':
        return size_bytes / 1024
    elif format == 'mb':
        return size_bytes / (1024 * 1024)
    elif format == 'gb':
        return size_bytes / (1024 * 1024 * 1024)
    elif format == 'auto':
        # Automatically choose the best unit
        if size_bytes < 1024:
            return f"{size_bytes} bytes"
        elif size_bytes < 1024 * 1024:
            return f"{size_bytes / 1024:.2f} KB"
        elif size_bytes < 1024 * 1024 * 1024:
            return f"{size_bytes / (1024 * 1024):.2f} MB"
        else:
            return f"{size_bytes / (1024 * 1024 * 1024):.2f} GB"
    else:
        logger.error(f"Unsupported format: {format}")
        return size_bytes

def get_relative_path(file_path: str, base_path: str) -> str:
    """
    Get the relative path from base_path to file_path
    
    Args:
        file_path: Absolute path to the file
        base_path: Base directory path
        
    Returns:
        str: Relative path
    """
    try:
        return os.path.relpath(file_path, base_path)
    except Exception as e:
        logger.error(f"Error getting relative path: {str(e)}")
        return file_path

def is_valid_file_type(file_path: str, allowed_extensions: List[str]) -> bool:
    """
    Check if a file has an allowed extension
    
    Args:
        file_path: Path to the file
        allowed_extensions: List of allowed extensions (e.g., ['.jpg', '.png'])
        
    Returns:
        bool: True if file has an allowed extension, False otherwise
    """
    if not os.path.isfile(file_path):
        return False
    
    ext = os.path.splitext(file_path)[1].lower()
    return ext in [ext.lower() for ext in allowed_extensions]

def create_unique_filename(directory: str, base_name: str, extension: str) -> str:
    """
    Create a unique filename in the specified directory
    
    Args:
        directory: Directory path
        base_name: Base name for the file
        extension: File extension (with or without dot)
        
    Returns:
        str: Unique filename
    """
    # Ensure extension starts with a dot
    if not extension.startswith('.'):
        extension = '.' + extension
    
    # Clean base_name to ensure it's a valid filename
    base_name = ''.join(c for c in base_name if c.isalnum() or c in ' _-')
    base_name = base_name.strip().replace(' ', '_')
    
    # Create initial filename
    filename = f"{base_name}{extension}"
    full_path = os.path.join(directory, filename)
    
    # If the file doesn't exist, return the initial filename
    if not os.path.exists(full_path):
        return filename
    
    # Otherwise, append a number to make it unique
    counter = 1
    while True:
        filename = f"{base_name}_{counter}{extension}"
        full_path = os.path.join(directory, filename)
        
        if not os.path.exists(full_path):
            return filename
        
        counter += 1

def get_directory_size(directory_path: str, format: str = 'bytes') -> Union[int, float, str]:
    """
    Calculate the total size of all files in a directory
    
    Args:
        directory_path: Path to the directory
        format: Format to return ('bytes', 'kb', 'mb', 'gb', 'auto')
        
    Returns:
        Union[int, float, str]: Directory size in the specified format
    """
    if not os.path.isdir(directory_path):
        logger.error(f"Directory not found: {directory_path}")
        return 0
    
    total_size = 0
    
    for dirpath, _, filenames in os.walk(directory_path):
        for filename in filenames:
            file_path = os.path.join(dirpath, filename)
            if os.path.isfile(file_path):
                total_size += os.path.getsize(file_path)
    
    if format == 'bytes':
        return total_size
    elif format == 'kb':
        return total_size / 1024
    elif format == 'mb':
        return total_size / (1024 * 1024)
    elif format == 'gb':
        return total_size / (1024 * 1024 * 1024)
    elif format == 'auto':
        # Automatically choose the best unit
        if total_size < 1024:
            return f"{total_size} bytes"
        elif total_size < 1024 * 1024:
            return f"{total_size / 1024:.2f} KB"
        elif total_size < 1024 * 1024 * 1024:
            return f"{total_size / (1024 * 1024):.2f} MB"
        else:
            return f"{total_size / (1024 * 1024 * 1024):.2f} GB"
    else:
        logger.error(f"Unsupported format: {format}")
        return total_size


if __name__ == "__main__":
    # Example usage
    print("File Utilities Module")
    
    # List all Python files in the current directory
    python_files = list_files(".", extensions=[".py"], recursive=False)
    print(f"Python files: {python_files}")
    
    # Get size of this file
    this_file = __file__
    size = get_file_size(this_file, format='auto')
    print(f"Size of {this_file}: {size}")

modules\utils_init_.py


tests\test_text_analysis.py

"""
Unit Tests for Clinical Text Analysis Module

This module contains unit tests for the clinical text analysis components,
including text preprocessing, named entity recognition, classification,
summarization, and relation extraction.
"""

import os
import sys
import unittest
from pathlib import Path
import json

# Add parent directory to path to import modules
sys.path.append(str(Path(__file__).parent.parent))

from modules.text_analysis.text_preprocessor import ClinicalTextPreprocessor
from modules.text_analysis.medical_ner import MedicalNamedEntityRecognizer
from modules.text_analysis.text_classifier import ClinicalTextClassifier
from modules.text_analysis.text_summarizer import ClinicalTextSummarizer
from modules.text_analysis.relation_extractor import MedicalRelationExtractor
from modules.text_analysis.clinical_text_analyzer import ClinicalTextAnalyzer

# Sample clinical text for testing
SAMPLE_CLINICAL_TEXT = """
Patient: Jane Doe
DOB: 05/12/1975
Date: 03/15/2023

Chief Complaint: Headache and dizziness for 3 days.

History of Present Illness:
The patient is a 48-year-old female with a history of migraines who presents with a severe headache and dizziness for the past 3 days. The headache is described as throbbing, located in the right temporal region, and rated 8/10 in intensity. The patient reports associated photophobia, phonophobia, and nausea. She has taken ibuprofen 600mg with minimal relief. She denies fever, neck stiffness, or visual changes.

Past Medical History:
1. Migraine with aura - diagnosed 15 years ago
2. Hypertension - diagnosed 5 years ago
3. Hypothyroidism

Medications:
1. Sumatriptan 50mg PRN for migraines
2. Lisinopril 10mg daily
3. Levothyroxine 75mcg daily

Allergies: Penicillin (hives)

Assessment:
1. Migraine headache, likely exacerbation of chronic condition
2. Hypertension, well-controlled
3. Hypothyroidism, stable

Plan:
1. Administer sumatriptan 50mg now
2. Start prochlorperazine 10mg for nausea
3. Continue current medications
4. Follow up in 2 weeks
5. If symptoms worsen, return to ED
"""

class TestClinicalTextPreprocessor(unittest.TestCase):
    """
    Test cases for ClinicalTextPreprocessor
    """
    
    def setUp(self):
        """Set up test fixtures"""
        self.preprocessor = ClinicalTextPreprocessor()
        self.text = SAMPLE_CLINICAL_TEXT
    
    def test_preprocess_text(self):
        """Test text preprocessing"""
        preprocessed = self.preprocessor.preprocess_text(self.text)
        self.assertIsInstance(preprocessed, str)
        self.assertTrue(len(preprocessed) > 0)
        self.assertLess(len(preprocessed), len(self.text))  # Should be shorter after preprocessing
    
    def test_extract_sentences(self):
        """Test sentence extraction"""
        sentences = self.preprocessor.extract_sentences(self.text)
        self.assertIsInstance(sentences, list)
        self.assertTrue(len(sentences) > 0)
        self.assertIsInstance(sentences[0], str)
    
    def test_extract_medical_terms(self):
        """Test medical term extraction"""
        terms = self.preprocessor.extract_medical_terms(self.text)
        self.assertIsInstance(terms, list)
        self.assertTrue(len(terms) > 0)
        
        # Check if common medical terms are extracted
        common_terms = ["headache", "migraine", "hypertension", "hypothyroidism"]
        for term in common_terms:
            self.assertTrue(any(term.lower() in t.lower() for t in terms), f"Term '{term}' not found in extracted terms")
    
    def test_extract_demographics(self):
        """Test demographics extraction"""
        demographics = self.preprocessor.extract_demographics(self.text)
        self.assertIsInstance(demographics, dict)
        
        # Check if demographics are correctly extracted
        self.assertIn("patient_name", demographics)
        self.assertEqual(demographics["patient_name"], "Jane Doe")
        self.assertIn("patient_age", demographics)
        self.assertEqual(demographics["patient_age"], "48")
        self.assertIn("patient_gender", demographics)
        self.assertEqual(demographics["patient_gender"], "female")
    
    def test_extract_features(self):
        """Test feature extraction"""
        features = self.preprocessor.extract_features(self.text)
        self.assertIsInstance(features, dict)
        self.assertIn("feature_names", features)
        self.assertIn("feature_vector", features)
        self.assertEqual(len(features["feature_names"]), len(features["feature_vector"]))

class TestMedicalNamedEntityRecognizer(unittest.TestCase):
    """
    Test cases for MedicalNamedEntityRecognizer
    """
    
    def setUp(self):
        """Set up test fixtures"""
        self.ner = MedicalNamedEntityRecognizer()
        self.text = SAMPLE_CLINICAL_TEXT
    
    def test_extract_entities_spacy(self):
        """Test entity extraction using spaCy"""
        entities = self.ner.extract_entities(self.text, method="spacy")
        self.assertIsInstance(entities, list)
        self.assertTrue(len(entities) > 0)
        
        # Check entity structure
        for entity in entities:
            self.assertIn("text", entity)
            self.assertIn("label", entity)
            self.assertIn("start", entity)
            self.assertIn("end", entity)
    
    def test_extract_diseases(self):
        """Test disease extraction"""
        diseases = self.ner.extract_diseases(self.text)
        self.assertIsInstance(diseases, list)
        
        # Check if common diseases are extracted
        common_diseases = ["migraine", "hypertension", "hypothyroidism"]
        for disease in common_diseases:
            self.assertTrue(any(disease.lower() in d.lower() for d in diseases), f"Disease '{disease}' not found")
    
    def test_extract_medications(self):
        """Test medication extraction"""
        medications = self.ner.extract_medications(self.text)
        self.assertIsInstance(medications, list)
        
        # Check if common medications are extracted
        common_meds = ["sumatriptan", "lisinopril", "levothyroxine", "ibuprofen"]
        for med in common_meds:
            self.assertTrue(any(med.lower() in m.lower() for m in medications), f"Medication '{med}' not found")
    
    def test_extract_symptoms(self):
        """Test symptom extraction"""
        symptoms = self.ner.extract_symptoms(self.text)
        self.assertIsInstance(symptoms, list)
        
        # Check if common symptoms are extracted
        common_symptoms = ["headache", "dizziness", "nausea"]
        for symptom in common_symptoms:
            self.assertTrue(any(symptom.lower() in s.lower() for s in symptoms), f"Symptom '{symptom}' not found")

class TestClinicalTextSummarizer(unittest.TestCase):
    """
    Test cases for ClinicalTextSummarizer
    """
    
    def setUp(self):
        """Set up test fixtures"""
        self.summarizer = ClinicalTextSummarizer()
        self.text = SAMPLE_CLINICAL_TEXT
    
    def test_extractive_summarize(self):
        """Test extractive summarization"""
        summary = self.summarizer.extractive_summarize(self.text)
        self.assertIsInstance(summary, str)
        self.assertTrue(len(summary) > 0)
        self.assertLess(len(summary), len(self.text))  # Summary should be shorter than original
    
    def test_summarize(self):
        """Test summarization with different methods"""
        result = self.summarizer.summarize(self.text, method="extractive")
        self.assertIsInstance(result, dict)
        self.assertTrue(result["success"])
        self.assertIn("summary", result)
        self.assertIn("method", result)
        self.assertIn("compression_ratio", result)
    
    def test_key_findings_extract(self):
        """Test key findings extraction"""
        result = self.summarizer.key_findings_extract(self.text)
        self.assertIsInstance(result, dict)
        self.assertTrue(result["success"])
        self.assertIn("findings", result)
        
        findings = result["findings"]
        self.assertIn("diagnosis", findings)
        self.assertIn("medications", findings)
        self.assertIn("allergies", findings)
    
    def test_generate_discharge_summary(self):
        """Test discharge summary generation"""
        result = self.summarizer.generate_discharge_summary(self.text)
        self.assertIsInstance(result, dict)
        self.assertTrue(result["success"])
        self.assertIn("discharge_summary", result)
        self.assertIn("findings", result)
        self.assertIn("clinical_summary", result)

class TestMedicalRelationExtractor(unittest.TestCase):
    """
    Test cases for MedicalRelationExtractor
    """
    
    def setUp(self):
        """Set up test fixtures"""
        self.relation_extractor = MedicalRelationExtractor()
        self.ner = MedicalNamedEntityRecognizer()
        self.text = SAMPLE_CLINICAL_TEXT
        self.entities = self.ner.extract_entities(self.text, method="spacy")
    
    def test_extract_relations_rule_based(self):
        """Test rule-based relation extraction"""
        relations = self.relation_extractor.extract_relations_rule_based(self.text, self.entities)
        self.assertIsInstance(relations, list)
        
        # Check relation structure if any relations are found
        if relations:
            relation = relations[0]
            self.assertIn("relation_type", relation)
            self.assertIn("entity1", relation)
            self.assertIn("entity2", relation)
            self.assertIn("confidence", relation)
    
    def test_extract_relations(self):
        """Test relation extraction with different methods"""
        relations = self.relation_extractor.extract_relations(self.text, self.entities, method="rule-based")
        self.assertIsInstance(relations, list)
        
        # Try hybrid method as well
        hybrid_relations = self.relation_extractor.extract_relations(self.text, self.entities, method="hybrid")
        self.assertIsInstance(hybrid_relations, list)
    
    def test_build_knowledge_graph(self):
        """Test knowledge graph building"""
        relations = self.relation_extractor.extract_relations(self.text, self.entities, method="rule-based")
        graph = self.relation_extractor.build_knowledge_graph(relations)
        
        self.assertIsInstance(graph, dict)
        self.assertIn("nodes", graph)
        self.assertIn("edges", graph)
        self.assertIsInstance(graph["nodes"], list)
        self.assertIsInstance(graph["edges"], list)

class TestClinicalTextAnalyzer(unittest.TestCase):
    """
    Test cases for ClinicalTextAnalyzer
    """
    
    def setUp(self):
        """Set up test fixtures"""
        self.analyzer = ClinicalTextAnalyzer()
        self.text = SAMPLE_CLINICAL_TEXT
    
    def test_analyze_text(self):
        """Test text analysis with default pipeline"""
        results = self.analyzer.analyze_text(self.text)
        self.assertIsInstance(results, dict)
        self.assertTrue(results["success"])
        
        # Check if all pipeline components are present
        self.assertIn("preprocessing", results)
        self.assertIn("entities", results)
        self.assertIn("summary", results)
        self.assertIn("relations", results)
    
    def test_analyze_text_custom_pipeline(self):
        """Test text analysis with custom pipeline"""
        custom_pipeline = ["preprocess", "ner"]
        results = self.analyzer.analyze_text(self.text, pipeline=custom_pipeline)
        
        self.assertIsInstance(results, dict)
        self.assertTrue(results["success"])
        self.assertIn("preprocessing", results)
        self.assertIn("entities", results)
        self.assertNotIn("summary", results)  # Should not be present
        self.assertNotIn("relations", results)  # Should not be present
    
    def test_generate_report(self):
        """Test report generation"""
        results = self.analyzer.analyze_text(self.text)
        
        # Test HTML report
        html_report = self.analyzer.generate_report(results, format="html")
        self.assertIsInstance(html_report, str)
        self.assertTrue(html_report.startswith("<!DOCTYPE html>"))
        
        # Test text report
        text_report = self.analyzer.generate_report(results, format="text")
        self.assertIsInstance(text_report, str)
        self.assertTrue(text_report.startswith("CLINICAL TEXT ANALYSIS REPORT"))
        
        # Test JSON report
        json_report = self.analyzer.generate_report(results, format="json")
        self.assertIsInstance(json_report, str)
        # Verify it's valid JSON
        json_data = json.loads(json_report)
        self.assertIsInstance(json_data, dict)

if __name__ == '__main__':
    unittest.main()

您可能感兴趣的与本文相关的镜像

Python3.11

Python3.11

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

天天进步2015

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值