You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

333 regels
16 KiB

  1. {% extends "./layout.html" %}
  2. {% block title %}患者详情{% endblock %}
  3. {% block css %}
  4. <style>
  5. .detail-panel { background: #fff; border-radius: 8px; padding: 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); margin-bottom: 16px; }
  6. .detail-panel h3 { font-size: 16px; color: #303133; margin-bottom: 20px; padding-bottom: 12px; border-bottom: 1px solid #EBEEF5; }
  7. .info-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
  8. .info-item { display: flex; }
  9. .info-item .label { width: 100px; color: #909399; font-size: 14px; flex-shrink: 0; }
  10. .info-item .value { color: #303133; font-size: 14px; }
  11. .doc-images { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 12px; }
  12. .sign-doc-card { width: 200px; border: 1px solid #EBEEF5; border-radius: 8px; padding: 16px; text-align: center; cursor: pointer; transition: all 0.2s; }
  13. .sign-doc-card:hover { border-color: var(--el-color-primary); box-shadow: 0 2px 12px rgba(255,120,0,0.15); }
  14. .timeline-list { padding: 0; list-style: none; }
  15. .timeline-list li { position: relative; padding: 0 0 20px 24px; border-left: 2px solid #EBEEF5; }
  16. .timeline-list li:last-child { border-left-color: transparent; padding-bottom: 0; }
  17. .timeline-list li::before { content: ''; position: absolute; left: -6px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: var(--el-color-primary); }
  18. .timeline-list li .time { font-size: 12px; color: #909399; margin-bottom: 4px; }
  19. .timeline-list li .desc { font-size: 14px; color: #303133; }
  20. .timeline-list li .reason { font-size: 13px; color: #F56C6C; margin-top: 4px; }
  21. .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; }
  22. </style>
  23. {% endblock %}
  24. {% block content %}
  25. <div id="detailApp" v-cloak>
  26. <div v-loading="loading" style="padding-bottom:80px;">
  27. <!-- 页头 -->
  28. <el-page-header @back="goBack" style="margin-bottom:16px;">
  29. <template #content>
  30. <span style="font-size:16px;font-weight:600;">患者详情</span>
  31. <el-tag v-if="patient.status === -1" type="info" size="small" style="margin-left:12px;">待提交</el-tag>
  32. <el-tag v-else-if="patient.status === 0" type="warning" size="small" style="margin-left:12px;">待审核</el-tag>
  33. <el-tag v-else-if="patient.status === 1" type="success" size="small" style="margin-left:12px;">审核通过</el-tag>
  34. <el-tag v-else-if="patient.status === 2" type="danger" size="small" style="margin-left:12px;">已驳回</el-tag>
  35. </template>
  36. </el-page-header>
  37. <!-- 基本信息 -->
  38. <div class="detail-panel">
  39. <h3>基本信息</h3>
  40. <div class="info-grid">
  41. <div class="info-item"><span class="label">患者编号:</span><span class="value">${ patient.patient_no }</span></div>
  42. <div class="info-item"><span class="label">姓名:</span><span class="value">${ patient.name }</span></div>
  43. <div class="info-item"><span class="label">性别:</span><span class="value">${ patient.gender }</span></div>
  44. <div class="info-item"><span class="label">手机号:</span><span class="value">${ patient.phone }</span></div>
  45. <div class="info-item"><span class="label">身份证号:</span><span class="value">${ patient.id_card }</span></div>
  46. <div class="info-item"><span class="label">出生日期:</span><span class="value">${ patient.birth_date }</span></div>
  47. <div class="info-item">
  48. <span class="label">所在地区:</span>
  49. <span class="value">${ patient.province_name } ${ patient.city_name } ${ patient.district_name }</span>
  50. </div>
  51. <div class="info-item"><span class="label">详细地址:</span><span class="value">${ patient.address }</span></div>
  52. <div class="info-item">
  53. <span class="label">瘤种:</span>
  54. <span class="value">
  55. <el-tag v-if="patient.tag" type="danger" size="small">${ patient.tag }</el-tag>
  56. <span v-else style="color:#999;">无</span>
  57. </span>
  58. </div>
  59. <div class="info-item"><span class="label">提交时间:</span><span class="value">${ patient.create_time }</span></div>
  60. <div class="info-item"><span class="label">紧急联系人:</span><span class="value">${ patient.emergency_contact || '—' }</span></div>
  61. <div class="info-item"><span class="label">紧急联系电话:</span><span class="value">${ patient.emergency_phone || '—' }</span></div>
  62. <div class="info-item" v-if="patient.income_amount"><span class="label">年可支配收入:</span><span class="value">${ patient.income_amount } 元</span></div>
  63. <div class="info-item" v-if="patient.guardian_name"><span class="label">监护人姓名:</span><span class="value">${ patient.guardian_name }</span></div>
  64. <div class="info-item" v-if="patient.guardian_id_card"><span class="label">监护人身份证:</span><span class="value">${ patient.guardian_id_card }</span></div>
  65. <div class="info-item" v-if="patient.guardian_relation"><span class="label">与患者关系:</span><span class="value">${ patient.guardian_relation }</span></div>
  66. </div>
  67. </div>
  68. <!-- 实名认证照片 -->
  69. <div class="detail-panel" v-if="patient.id_card_front || patient.id_card_back || patient.photo">
  70. <h3>实名认证照片</h3>
  71. <p style="font-size:13px;color:#909399;margin-bottom:12px;">
  72. <template v-if="patient.id_card_type === 2">无证件儿童免冠照片</template>
  73. <template v-else>身份证人像面 / 国徽面照片</template>
  74. </p>
  75. <div class="doc-images">
  76. <div v-if="patient.id_card_front">
  77. <el-image :src="patient.id_card_front" fit="cover" :preview-src-list="authImageList"
  78. :initial-index="0" style="width:260px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
  79. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">人像面</div>
  80. </div>
  81. <div v-if="patient.id_card_back">
  82. <el-image :src="patient.id_card_back" fit="cover" :preview-src-list="authImageList"
  83. :initial-index="patient.id_card_front ? 1 : 0" style="width:260px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
  84. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">国徽面</div>
  85. </div>
  86. <div v-if="patient.photo">
  87. <el-image :src="patient.photo" fit="cover" :preview-src-list="[patient.photo]"
  88. :initial-index="0" style="width:170px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
  89. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">免冠照片</div>
  90. </div>
  91. </div>
  92. </div>
  93. <!-- 上传资料 -->
  94. <div class="detail-panel">
  95. <h3>上传资料</h3>
  96. <p style="font-size:13px;color:#909399;margin-bottom:12px;">患者上传的检查报告单或出院诊断证明书</p>
  97. <div class="doc-images" v-if="patient.documents && patient.documents.length">
  98. <div v-for="(doc, idx) in patient.documents" :key="idx">
  99. <el-image :src="doc" fit="cover" :preview-src-list="patient.documents" :initial-index="idx"
  100. style="width:200px;height:140px;border-radius:8px;border:1px solid #EBEEF5;" />
  101. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">资料 ${ idx + 1 }</div>
  102. </div>
  103. </div>
  104. <el-empty v-else description="暂无上传资料" :image-size="60" />
  105. </div>
  106. <!-- 签字材料 -->
  107. <div class="detail-panel">
  108. <h3>签字材料</h3>
  109. <p style="font-size:13px;color:#909399;margin-bottom:16px;">患者签署的三份授权材料</p>
  110. <div style="display:flex;gap:20px;flex-wrap:wrap;">
  111. <div v-for="item in signDocs" :key="item.key" style="width:220px;">
  112. <div style="font-size:13px;color:#303133;font-weight:500;margin-bottom:8px;">${ item.label }</div>
  113. <template v-if="item.url && isImageUrl(item.url)">
  114. <el-image :src="item.url" fit="cover" :preview-src-list="signImageList" :initial-index="signImageList.indexOf(item.url)"
  115. style="width:220px;height:160px;border-radius:8px;border:1px solid #EBEEF5;cursor:pointer;" />
  116. </template>
  117. <template v-else-if="item.url">
  118. <div class="sign-doc-card" @click="downloadSign(item.url, item.label)">
  119. <div style="font-size:36px;margin-bottom:8px;">📝</div>
  120. <div style="font-size:12px;color:#67C23A;margin-bottom:8px;">已签署</div>
  121. <div style="font-size:13px;color:var(--el-color-primary);font-weight:500;">⬇ 下载</div>
  122. </div>
  123. </template>
  124. <template v-else>
  125. <div style="width:220px;height:160px;border:1px dashed #DCDFE6;border-radius:8px;display:flex;align-items:center;justify-content:center;">
  126. <span style="font-size:13px;color:#909399;">未上传</span>
  127. </div>
  128. </template>
  129. </div>
  130. </div>
  131. </div>
  132. <!-- 审核记录 -->
  133. <div class="detail-panel">
  134. <h3>审核记录</h3>
  135. <ul class="timeline-list" v-if="audits.length">
  136. <li v-for="(item, idx) in audits" :key="idx">
  137. <div class="time">${ item.create_time }</div>
  138. <div class="desc" v-if="item.action === 'submit'">${ item.operator_name || '系统' } 提交了患者资料,等待审核</div>
  139. <div class="desc" v-else-if="item.action === 'approve'">${ item.operator_name } 审核通过</div>
  140. <div class="desc" v-else-if="item.action === 'reject'">${ item.operator_name } 驳回了资料</div>
  141. <div class="reason" v-if="item.reason">驳回原因:${ item.reason }</div>
  142. </li>
  143. </ul>
  144. <el-empty v-else description="暂无审核记录" :image-size="60" />
  145. </div>
  146. </div>
  147. <!-- 固定底部操作栏 -->
  148. <div class="fixed-bottom-bar">
  149. <el-button @click="goBack">返 回</el-button>
  150. <el-button v-if="patient.status === 0 && canAudit" type="success" @click="handleApprove">审核通过</el-button>
  151. <el-button v-if="patient.status === 0 && canAudit" type="danger" @click="showRejectDialog">驳 回</el-button>
  152. </div>
  153. <!-- 驳回弹窗 -->
  154. <el-dialog v-model="rejectVisible" title="驳回审核" width="540px" destroy-on-close :close-on-click-modal="false" :z-index="2000">
  155. <p style="font-weight:500;color:#303133;margin-bottom:12px;">请选择或填写驳回原因(必填):</p>
  156. <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;">
  157. <el-check-tag v-for="(r, i) in commonReasons" :key="i" :checked="selectedReasons.includes(i)"
  158. @change="toggleReason(i)">${ r }</el-check-tag>
  159. </div>
  160. <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因,或点击上方常见原因快速选择"></el-input>
  161. <div style="text-align:right;margin-top:20px;">
  162. <el-button @click="rejectVisible = false">取消</el-button>
  163. <el-button type="danger" @click="doReject" :loading="rejectSaving">确认驳回</el-button>
  164. </div>
  165. </el-dialog>
  166. </div>
  167. {% endblock %}
  168. {% block js %}
  169. <script>
  170. var patientId = '{{ patientId }}';
  171. var canAuditVal = {{ canAudit | dump | safe }};
  172. var { createApp, ref, reactive, onMounted } = Vue;
  173. var app = createApp({
  174. delimiters: ['${', '}'],
  175. setup() {
  176. var loading = ref(true);
  177. var canAudit = ref(canAuditVal);
  178. var patient = reactive({
  179. id: '', patient_no: '', name: '', phone: '', id_card: '', gender: '', birth_date: '',
  180. province_code: '', city_code: '', district_code: '',
  181. province_name: '', city_name: '', district_name: '',
  182. address: '', tag: '', documents: [], sign_income: '', sign_privacy: '', sign_promise: '',
  183. emergency_contact: '', emergency_phone: '',
  184. status: -1, create_time: ''
  185. });
  186. var audits = ref([]);
  187. var rejectVisible = ref(false);
  188. var rejectSaving = ref(false);
  189. var rejectReason = ref('');
  190. var selectedReasons = ref([]);
  191. var commonReasons = [
  192. '身份证照片模糊,请重新上传',
  193. '病历资料不完整,请补充',
  194. '检查报告缺失,请上传',
  195. '姓名与身份证信息不一致',
  196. '手机号无法联系,请核实',
  197. '提交信息存在明显错误',
  198. '资料过期,请提供最新版本'
  199. ];
  200. async function loadDetail() {
  201. loading.value = true;
  202. try {
  203. var res = await fetch('/admin/patient/info?id=' + patientId).then(function(r) { return r.json(); });
  204. if (res.code === 0) {
  205. Object.assign(patient, res.data.patient);
  206. audits.value = res.data.audits || [];
  207. } else {
  208. ElementPlus.ElMessage.error(res.msg || '加载失败');
  209. }
  210. } finally {
  211. loading.value = false;
  212. }
  213. }
  214. function goBack() {
  215. window.location.href = '/admin/patient.html';
  216. }
  217. function downloadSign(url, name) {
  218. if (!url) { ElementPlus.ElMessage.warning('该材料未上传'); return; }
  219. var a = document.createElement('a');
  220. a.href = url; a.target = '_blank'; a.download = name; a.click();
  221. }
  222. function isImageUrl(url) {
  223. if (!url) return false;
  224. var lower = url.split('?')[0].toLowerCase();
  225. return /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/.test(lower);
  226. }
  227. var signDocs = Vue.computed(function() {
  228. var docs = [
  229. { key: 'income', label: '个人可支配收入声明', url: patient.sign_income },
  230. { key: 'privacy', label: '个人信息处理同意书', url: patient.sign_privacy },
  231. { key: 'promise', label: '声明与承诺', url: patient.sign_promise }
  232. ];
  233. if (patient.sign_privacy_jhr) {
  234. docs.push({ key: 'privacy_jhr', label: '监护人个人信息处理同意书', url: patient.sign_privacy_jhr });
  235. }
  236. return docs;
  237. });
  238. var signImageList = Vue.computed(function() {
  239. return [patient.sign_income, patient.sign_privacy, patient.sign_promise, patient.sign_privacy_jhr].filter(function(u) { return u && isImageUrl(u); });
  240. });
  241. var authImageList = Vue.computed(function() {
  242. return [patient.id_card_front, patient.id_card_back, patient.photo].filter(Boolean);
  243. });
  244. async function handleApprove() {
  245. try {
  246. await ElementPlus.ElMessageBox.confirm(
  247. '确定要通过该患者的审核吗?通过后患者将收到审核通过通知。',
  248. '确认审核通过',
  249. { confirmButtonText: '确认通过', cancelButtonText: '取消', type: 'success' }
  250. );
  251. var res = await fetch('/admin/patient/approve', {
  252. method: 'POST',
  253. headers: { 'Content-Type': 'application/json' },
  254. body: JSON.stringify({ id: patient.id })
  255. }).then(function(r) { return r.json(); });
  256. if (res.code === 0) {
  257. ElementPlus.ElMessage.success('审核已通过');
  258. loadDetail();
  259. } else {
  260. ElementPlus.ElMessage.error(res.msg || '操作失败');
  261. }
  262. } catch(e) {}
  263. }
  264. function showRejectDialog() {
  265. rejectReason.value = '';
  266. selectedReasons.value = [];
  267. rejectVisible.value = true;
  268. }
  269. function toggleReason(index) {
  270. var pos = selectedReasons.value.indexOf(index);
  271. if (pos > -1) { selectedReasons.value.splice(pos, 1); }
  272. else { selectedReasons.value.push(index); }
  273. rejectReason.value = selectedReasons.value.map(function(i) { return commonReasons[i]; }).join(';');
  274. }
  275. async function doReject() {
  276. if (!rejectReason.value.trim()) { ElementPlus.ElMessage.warning('请填写驳回原因'); return; }
  277. rejectSaving.value = true;
  278. try {
  279. var res = await fetch('/admin/patient/reject', {
  280. method: 'POST',
  281. headers: { 'Content-Type': 'application/json' },
  282. body: JSON.stringify({ id: patient.id, reason: rejectReason.value.trim() })
  283. }).then(function(r) { return r.json(); });
  284. if (res.code === 0) {
  285. ElementPlus.ElMessage.success('已驳回');
  286. rejectVisible.value = false;
  287. loadDetail();
  288. } else {
  289. ElementPlus.ElMessage.error(res.msg || '操作失败');
  290. }
  291. } finally {
  292. rejectSaving.value = false;
  293. }
  294. }
  295. onMounted(function() { loadDetail(); });
  296. return {
  297. loading, patient, audits, canAudit,
  298. rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons,
  299. goBack, downloadSign, isImageUrl, signDocs, signImageList, authImageList, handleApprove, showRejectDialog, toggleReason, doReject
  300. };
  301. }
  302. });
  303. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  304. app.mount('#detailApp');
  305. </script>
  306. {% endblock %}