|
- {% extends "./layout.html" %}
-
- {% block title %}患者详情{% endblock %}
-
- {% block css %}
- <style>
- .detail-panel { background: #fff; border-radius: 8px; padding: 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 16px; }
- .detail-panel h3 { font-size: 16px; color: #303133; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid #EBEEF5; }
- .info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
- .info-item { display: flex; }
- .info-item .label { width: 100px; color: #909399; font-size: 14px; flex-shrink: 0; }
- .info-item .value { color: #303133; font-size: 14px; }
- .doc-images { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 12px; }
- .sign-doc-card { width: 200px; border: 1px solid #EBEEF5; border-radius: 8px; padding: 16px; text-align: center; cursor: pointer; transition: all 0.2s; }
- .sign-doc-card:hover { border-color: var(--el-color-primary); box-shadow: 0 2px 12px rgba(255,120,0,0.15); }
- .timeline-list { padding: 0; list-style: none; }
- .timeline-list li { position: relative; padding: 0 0 20px 24px; border-left: 2px solid #EBEEF5; }
- .timeline-list li:last-child { border-left-color: transparent; padding-bottom: 0; }
- .timeline-list li::before { content: ''; position: absolute; left: -6px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: var(--el-color-primary); }
- .timeline-list li .time { font-size: 12px; color: #909399; margin-bottom: 4px; }
- .timeline-list li .desc { font-size: 14px; color: #303133; }
- .timeline-list li .reason { font-size: 13px; color: #F56C6C; margin-top: 4px; }
- .fixed-bottom-bar { position: fixed; bottom: 0; left: 220px; right: 0; height: 64px; background: #fff; box-shadow: 0 -2px 8px rgba(0,0,0,0.08); display: flex; align-items: center; justify-content: center; gap: 16px; z-index: 10; padding: 0 24px; }
- </style>
- {% endblock %}
-
- {% block content %}
- <div id="detailApp" v-cloak>
- <div v-loading="loading" style="padding-bottom:80px;">
- <!-- 页头 -->
- <el-page-header @back="goBack" style="margin-bottom:16px;">
- <template #content>
- <span style="font-size:16px;font-weight:600;">患者详情</span>
- <el-tag v-if="patient.status === -1" type="info" size="small" style="margin-left:12px;">待提交</el-tag>
- <el-tag v-else-if="patient.status === 0" type="warning" size="small" style="margin-left:12px;">待审核</el-tag>
- <el-tag v-else-if="patient.status === 1" type="success" size="small" style="margin-left:12px;">审核通过</el-tag>
- <el-tag v-else-if="patient.status === 2" type="danger" size="small" style="margin-left:12px;">已驳回</el-tag>
- </template>
- </el-page-header>
-
- <!-- 基本信息 -->
- <div class="detail-panel">
- <h3>基本信息</h3>
- <div class="info-grid">
- <div class="info-item"><span class="label">患者编号:</span><span class="value">${ patient.patient_no }</span></div>
- <div class="info-item"><span class="label">姓名:</span><span class="value">${ patient.name }</span></div>
- <div class="info-item"><span class="label">性别:</span><span class="value">${ patient.gender }</span></div>
- <div class="info-item"><span class="label">手机号:</span><span class="value">${ patient.phone }</span></div>
- <div class="info-item"><span class="label">身份证号:</span><span class="value">${ patient.id_card }</span></div>
- <div class="info-item"><span class="label">出生日期:</span><span class="value">${ patient.birth_date }</span></div>
- <div class="info-item">
- <span class="label">所在地区:</span>
- <span class="value">${ patient.province_name } ${ patient.city_name } ${ patient.district_name }</span>
- </div>
- <div class="info-item"><span class="label">详细地址:</span><span class="value">${ patient.address }</span></div>
- <div class="info-item">
- <span class="label">瘤种:</span>
- <span class="value">
- <el-tag v-if="patient.tag" type="danger" size="small">${ patient.tag }</el-tag>
- <span v-else style="color:#999;">无</span>
- </span>
- </div>
- <div class="info-item"><span class="label">提交时间:</span><span class="value">${ patient.create_time }</span></div>
- <div class="info-item"><span class="label">紧急联系人:</span><span class="value">${ patient.emergency_contact || '—' }</span></div>
- <div class="info-item"><span class="label">紧急联系电话:</span><span class="value">${ patient.emergency_phone || '—' }</span></div>
- </div>
- </div>
-
- <!-- 上传资料 -->
- <div class="detail-panel">
- <h3>上传资料</h3>
- <p style="font-size:13px;color:#909399;margin-bottom:12px;">患者上传的检查报告单或出院诊断证明书</p>
- <div class="doc-images" v-if="patient.documents && patient.documents.length">
- <div v-for="(doc, idx) in patient.documents" :key="idx">
- <el-image :src="doc" fit="cover" :preview-src-list="patient.documents" :initial-index="idx"
- style="width:200px;height:140px;border-radius:8px;border:1px solid #EBEEF5;" />
- <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">资料 ${ idx + 1 }</div>
- </div>
- </div>
- <el-empty v-else description="暂无上传资料" :image-size="60" />
- </div>
-
- <!-- 签字材料 -->
- <div class="detail-panel">
- <h3>签字材料</h3>
- <p style="font-size:13px;color:#909399;margin-bottom:16px;">患者签署的三份授权材料</p>
- <div style="display:flex;gap:20px;flex-wrap:wrap;">
- <div v-for="item in signDocs" :key="item.key" style="width:220px;">
- <div style="font-size:13px;color:#303133;font-weight:500;margin-bottom:8px;">${ item.label }</div>
- <template v-if="item.url && isImageUrl(item.url)">
- <el-image :src="item.url" fit="cover" :preview-src-list="signImageList" :initial-index="signImageList.indexOf(item.url)"
- style="width:220px;height:160px;border-radius:8px;border:1px solid #EBEEF5;cursor:pointer;" />
- </template>
- <template v-else-if="item.url">
- <div class="sign-doc-card" @click="downloadSign(item.url, item.label)">
- <div style="font-size:36px;margin-bottom:8px;">📝</div>
- <div style="font-size:12px;color:#67C23A;margin-bottom:8px;">已签署</div>
- <div style="font-size:13px;color:var(--el-color-primary);font-weight:500;">⬇ 下载</div>
- </div>
- </template>
- <template v-else>
- <div style="width:220px;height:160px;border:1px dashed #DCDFE6;border-radius:8px;display:flex;align-items:center;justify-content:center;">
- <span style="font-size:13px;color:#909399;">未上传</span>
- </div>
- </template>
- </div>
- </div>
- </div>
-
- <!-- 审核记录 -->
- <div class="detail-panel">
- <h3>审核记录</h3>
- <ul class="timeline-list" v-if="audits.length">
- <li v-for="(item, idx) in audits" :key="idx">
- <div class="time">${ item.create_time }</div>
- <div class="desc" v-if="item.action === 'submit'">${ item.operator_name || '系统' } 提交了患者资料,等待审核</div>
- <div class="desc" v-else-if="item.action === 'approve'">${ item.operator_name } 审核通过</div>
- <div class="desc" v-else-if="item.action === 'reject'">${ item.operator_name } 驳回了资料</div>
- <div class="reason" v-if="item.reason">驳回原因:${ item.reason }</div>
- </li>
- </ul>
- <el-empty v-else description="暂无审核记录" :image-size="60" />
- </div>
- </div>
-
- <!-- 固定底部操作栏 -->
- <div class="fixed-bottom-bar">
- <el-button @click="goBack">返 回</el-button>
- <el-button v-if="patient.status === 0 && canAudit" type="success" @click="handleApprove">审核通过</el-button>
- <el-button v-if="patient.status === 0 && canAudit" type="danger" @click="showRejectDialog">驳 回</el-button>
- </div>
-
- <!-- 驳回弹窗 -->
- <el-dialog v-model="rejectVisible" title="驳回审核" width="540px" destroy-on-close :close-on-click-modal="false" :z-index="2000">
- <p style="font-weight:500;color:#303133;margin-bottom:12px;">请选择或填写驳回原因(必填):</p>
- <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;">
- <el-check-tag v-for="(r, i) in commonReasons" :key="i" :checked="selectedReasons.includes(i)"
- @change="toggleReason(i)">${ r }</el-check-tag>
- </div>
- <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因,或点击上方常见原因快速选择"></el-input>
- <div style="text-align:right;margin-top:20px;">
- <el-button @click="rejectVisible = false">取消</el-button>
- <el-button type="danger" @click="doReject" :loading="rejectSaving">确认驳回</el-button>
- </div>
- </el-dialog>
- </div>
- {% endblock %}
-
- {% block js %}
- <script>
- var patientId = '{{ patientId }}';
- var canAuditVal = {{ canAudit | dump | safe }};
- var { createApp, ref, reactive, onMounted } = Vue;
-
- var app = createApp({
- delimiters: ['${', '}'],
- setup() {
- var loading = ref(true);
- var canAudit = ref(canAuditVal);
- var patient = reactive({
- id: '', patient_no: '', name: '', phone: '', id_card: '', gender: '', birth_date: '',
- province_code: '', city_code: '', district_code: '',
- province_name: '', city_name: '', district_name: '',
- address: '', tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: '',
- emergency_contact: '', emergency_phone: '',
- status: -1, create_time: ''
- });
- var audits = ref([]);
- var rejectVisible = ref(false);
- var rejectSaving = ref(false);
- var rejectReason = ref('');
- var selectedReasons = ref([]);
- var commonReasons = [
- '身份证照片模糊,请重新上传',
- '病历资料不完整,请补充',
- '检查报告缺失,请上传',
- '姓名与身份证信息不一致',
- '手机号无法联系,请核实',
- '提交信息存在明显错误',
- '资料过期,请提供最新版本'
- ];
-
- async function loadDetail() {
- loading.value = true;
- try {
- var res = await fetch('/admin/patient/info?id=' + patientId).then(function(r) { return r.json(); });
- if (res.code === 0) {
- Object.assign(patient, res.data.patient);
- audits.value = res.data.audits || [];
- } else {
- ElementPlus.ElMessage.error(res.msg || '加载失败');
- }
- } finally {
- loading.value = false;
- }
- }
-
- function goBack() {
- window.location.href = '/admin/patient.html';
- }
-
- function downloadSign(url, name) {
- if (!url) { ElementPlus.ElMessage.warning('该材料未上传'); return; }
- var a = document.createElement('a');
- a.href = url; a.target = '_blank'; a.download = name; a.click();
- }
-
- function isImageUrl(url) {
- if (!url) return false;
- var lower = url.split('?')[0].toLowerCase();
- return /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/.test(lower);
- }
-
- var signDocs = Vue.computed(function() {
- return [
- { key: 'income', label: '个人可支配收入声明', url: patient.sign_income },
- { key: 'privacy', label: '个人信息处理同意书', url: patient.sign_privacy },
- { key: 'promise', label: '声明与承诺', url: patient.sign_promise }
- ];
- });
-
- var signImageList = Vue.computed(function() {
- return [patient.sign_income, patient.sign_privacy, patient.sign_promise].filter(function(u) { return u && isImageUrl(u); });
- });
-
- async function handleApprove() {
- try {
- await ElementPlus.ElMessageBox.confirm(
- '确定要通过该患者的审核吗?通过后患者将收到审核通过通知。',
- '确认审核通过',
- { confirmButtonText: '确认通过', cancelButtonText: '取消', type: 'success' }
- );
- var res = await fetch('/admin/patient/approve', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: patient.id })
- }).then(function(r) { return r.json(); });
- if (res.code === 0) {
- ElementPlus.ElMessage.success('审核已通过');
- loadDetail();
- } else {
- ElementPlus.ElMessage.error(res.msg || '操作失败');
- }
- } catch(e) {}
- }
-
- function showRejectDialog() {
- rejectReason.value = '';
- selectedReasons.value = [];
- rejectVisible.value = true;
- }
-
- function toggleReason(index) {
- var pos = selectedReasons.value.indexOf(index);
- if (pos > -1) { selectedReasons.value.splice(pos, 1); }
- else { selectedReasons.value.push(index); }
- rejectReason.value = selectedReasons.value.map(function(i) { return commonReasons[i]; }).join(';');
- }
-
- async function doReject() {
- if (!rejectReason.value.trim()) { ElementPlus.ElMessage.warning('请填写驳回原因'); return; }
- rejectSaving.value = true;
- try {
- var res = await fetch('/admin/patient/reject', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ id: patient.id, reason: rejectReason.value.trim() })
- }).then(function(r) { return r.json(); });
- if (res.code === 0) {
- ElementPlus.ElMessage.success('已驳回');
- rejectVisible.value = false;
- loadDetail();
- } else {
- ElementPlus.ElMessage.error(res.msg || '操作失败');
- }
- } finally {
- rejectSaving.value = false;
- }
- }
-
- onMounted(function() { loadDetail(); });
-
- return {
- loading, patient, audits, canAudit,
- rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons,
- goBack, downloadSign, isImageUrl, signDocs, signImageList, handleApprove, showRejectDialog, toggleReason, doReject
- };
- }
- });
-
- app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
- app.mount('#detailApp');
- </script>
- {% endblock %}
|