Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 

759 wiersze
38 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. .sign-img-wrap .el-image__inner { object-position: center bottom !important; }
  15. .timeline-list { padding: 0; list-style: none; }
  16. .timeline-list li { position: relative; padding: 0 0 20px 24px; border-left: 2px solid #EBEEF5; }
  17. .timeline-list li:last-child { border-left-color: transparent; padding-bottom: 0; }
  18. .timeline-list li::before { content: ''; position: absolute; left: -6px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: var(--el-color-primary); }
  19. .timeline-list li .time { font-size: 12px; color: #909399; margin-bottom: 4px; }
  20. .timeline-list li .desc { font-size: 14px; color: #303133; }
  21. .timeline-list li .subdesc { font-size: 13px; color: #606266; margin-top: 4px; }
  22. .timeline-list li .reason { font-size: 13px; color: #F56C6C; margin-top: 4px; }
  23. .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; }
  24. .sample-photo-edit { position: relative; width: 80px; height: 80px; }
  25. .sample-photo-edit .el-image { width: 80px; height: 80px; border-radius: 6px; border: 1px solid #eee; }
  26. .sample-photo-edit .del { position: absolute; top: -6px; right: -6px; cursor: pointer; background: rgba(0,0,0,0.6); color: #fff; border-radius: 50%; width: 20px; height: 20px; display:flex; align-items:center; justify-content:center; font-size:12px; z-index:10; line-height:1; }
  27. </style>
  28. {% endblock %}
  29. {% block content %}
  30. <div id="detailApp" v-cloak>
  31. <div v-loading="loading" style="padding-bottom:80px;">
  32. <!-- 页头 -->
  33. <el-page-header @back="goBack" style="margin-bottom:16px;">
  34. <template #content>
  35. <span style="font-size:16px;font-weight:600;">患者详情</span>
  36. <el-tag v-if="patient.status === -1" type="info" size="small" style="margin-left:12px;">待提交</el-tag>
  37. <el-tag v-else-if="patient.status === 0" type="warning" size="small" style="margin-left:12px;">待审核</el-tag>
  38. <el-tag v-else-if="patient.status === 1" type="success" size="small" style="margin-left:12px;">审核通过</el-tag>
  39. <el-tag v-else-if="patient.status === 2" type="danger" size="small" style="margin-left:12px;">已驳回</el-tag>
  40. </template>
  41. </el-page-header>
  42. <!-- 基本信息 -->
  43. <div class="detail-panel">
  44. <h3>基本信息</h3>
  45. <div class="info-grid">
  46. <div class="info-item"><span class="label">患者编号:</span><span class="value">${ patient.patient_no }</span></div>
  47. <div class="info-item"><span class="label">姓名:</span><span class="value">${ patient.name }</span></div>
  48. <div class="info-item"><span class="label">性别:</span><span class="value">${ patient.gender }</span></div>
  49. <div class="info-item"><span class="label">手机号:</span><span class="value">${ patient.phone }</span></div>
  50. <div class="info-item"><span class="label">身份证号:</span><span class="value">${ patient.id_card }</span></div>
  51. <div class="info-item"><span class="label">出生日期:</span><span class="value">${ patient.birth_date }</span></div>
  52. <div class="info-item">
  53. <span class="label">所在地区:</span>
  54. <span class="value">${ patient.province_name } ${ patient.city_name } ${ patient.district_name }</span>
  55. </div>
  56. <div class="info-item"><span class="label">详细地址:</span><span class="value">${ patient.address }</span></div>
  57. <div class="info-item">
  58. <span class="label">瘤种:</span>
  59. <span class="value">
  60. <el-tag v-if="patient.tag" type="danger" size="small">${ patient.tag }</el-tag>
  61. <span v-else style="color:#999;">无</span>
  62. </span>
  63. </div>
  64. <div class="info-item"><span class="label">提交时间:</span><span class="value">${ patient.create_time }</span></div>
  65. <div class="info-item"><span class="label">医院名称:</span><span class="value">${ patient.hospital || '—' }</span></div>
  66. <div class="info-item">
  67. <span class="label">医院所在地:</span>
  68. <span class="value">${ hospitalRegionText || '—' }</span>
  69. </div>
  70. <div class="info-item"><span class="label">紧急联系人:</span><span class="value">${ patient.emergency_contact || '—' }</span></div>
  71. <div class="info-item"><span class="label">紧急联系电话:</span><span class="value">${ patient.emergency_phone || '—' }</span></div>
  72. <div class="info-item" v-if="patient.income_amount"><span class="label">年可支配收入:</span><span class="value">${ patient.income_amount } 元</span></div>
  73. <div class="info-item" v-if="patient.guardian_name"><span class="label">监护人姓名:</span><span class="value">${ patient.guardian_name }</span></div>
  74. <div class="info-item" v-if="patient.guardian_id_card"><span class="label">监护人身份证:</span><span class="value">${ patient.guardian_id_card }</span></div>
  75. <div class="info-item" v-if="patient.guardian_relation"><span class="label">与患者关系:</span><span class="value">${ patient.guardian_relation }</span></div>
  76. </div>
  77. </div>
  78. <!-- 实名认证照片 -->
  79. <div class="detail-panel" v-if="patient.id_card_front || patient.id_card_back || patient.photo">
  80. <h3>实名认证照片</h3>
  81. <p style="font-size:13px;color:#909399;margin-bottom:12px;">
  82. <template v-if="patient.id_card_type === 2">无证件儿童免冠照片</template>
  83. <template v-else>身份证人像面 / 国徽面照片</template>
  84. </p>
  85. <div class="doc-images">
  86. <div v-if="patient.id_card_front">
  87. <el-image :src="patient.id_card_front" fit="cover" :preview-src-list="authImageList"
  88. :initial-index="0" style="width:260px;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 v-if="patient.id_card_back">
  92. <el-image :src="patient.id_card_back" fit="cover" :preview-src-list="authImageList"
  93. :initial-index="patient.id_card_front ? 1 : 0" style="width:260px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
  94. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">国徽面</div>
  95. </div>
  96. <div v-if="patient.photo">
  97. <el-image :src="patient.photo" fit="cover" :preview-src-list="[patient.photo]"
  98. :initial-index="0" style="width:170px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
  99. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">免冠照片</div>
  100. </div>
  101. </div>
  102. </div>
  103. <!-- 上传资料 -->
  104. <div class="detail-panel">
  105. <h3>上传资料</h3>
  106. <p style="font-size:13px;color:#909399;margin-bottom:12px;">患者上传的检查报告单或出院诊断证明书</p>
  107. <div class="doc-images" v-if="patient.documents && patient.documents.length">
  108. <div v-for="(doc, idx) in patient.documents" :key="idx">
  109. <el-image :src="doc" fit="cover" :preview-src-list="patient.documents" :initial-index="idx"
  110. style="width:200px;height:140px;border-radius:8px;border:1px solid #EBEEF5;" />
  111. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">资料 ${ idx + 1 }</div>
  112. </div>
  113. </div>
  114. <el-empty v-else description="暂无上传资料" :image-size="60" />
  115. </div>
  116. <!-- 签字材料 -->
  117. <div class="detail-panel">
  118. <h3>签字材料</h3>
  119. <p style="font-size:13px;color:#909399;margin-bottom:16px;">患者签署的授权材料</p>
  120. <div style="display:flex;gap:20px;flex-wrap:wrap;">
  121. <div v-for="item in signDocs" :key="item.key" style="width:220px;">
  122. <div style="font-size:13px;color:#303133;font-weight:500;margin-bottom:8px;">${ item.label }</div>
  123. <template v-if="item.url && isImageUrl(item.url)">
  124. <el-image :src="item.url" fit="cover" :preview-src-list="signImageList" :initial-index="signImageList.indexOf(item.url)"
  125. class="sign-img-wrap" style="width:220px;height:160px;border-radius:8px;border:1px solid #EBEEF5;cursor:pointer;" />
  126. </template>
  127. <template v-else-if="item.url">
  128. <div class="sign-doc-card" @click="downloadSign(item.url, item.label)">
  129. <div style="font-size:36px;margin-bottom:8px;">📝</div>
  130. <div style="font-size:12px;color:#67C23A;margin-bottom:8px;">已签署</div>
  131. <div style="font-size:13px;color:var(--el-color-primary);font-weight:500;">⬇ 下载</div>
  132. </div>
  133. </template>
  134. <template v-else>
  135. <div style="width:220px;height:160px;border:1px dashed #DCDFE6;border-radius:8px;display:flex;align-items:center;justify-content:center;">
  136. <span style="font-size:13px;color:#909399;">未上传</span>
  137. </div>
  138. </template>
  139. </div>
  140. </div>
  141. </div>
  142. <!-- 送检信息 -->
  143. <div class="detail-panel">
  144. <h3>送检信息</h3>
  145. <div class="info-grid">
  146. <div class="info-item"><span class="label">状态:</span>
  147. <span class="value">
  148. <el-tag :type="sampleInfoStatusType(patient.sample_info_status)" size="small">
  149. ${ sampleInfoStatusText(patient.sample_info_status) }
  150. </el-tag>
  151. </span>
  152. </div>
  153. <div class="info-item"><span class="label">送检样本:</span>
  154. <span class="value">
  155. <template v-if="patient.sample_types && patient.sample_types.length">
  156. <el-tag v-for="st in patient.sample_types" :key="st" size="small" style="margin-right:4px;">${ st }</el-tag>
  157. </template>
  158. <span v-else style="color:#999;">未选择</span>
  159. </span>
  160. </div>
  161. <div class="info-item" v-if="patient.wax_return"><span class="label">样本需寄回:</span><span class="value">是</span></div>
  162. <div class="info-item" v-if="patient.return_name"><span class="label">收件人:</span><span class="value">${ patient.return_name }</span></div>
  163. <div class="info-item" v-if="patient.return_phone"><span class="label">收件电话:</span><span class="value">${ patient.return_phone }</span></div>
  164. <div class="info-item" v-if="patient.return_address"><span class="label">收件地址:</span>
  165. <span class="value">${ patient.return_province_name || '' } ${ patient.return_city_name || '' } ${ patient.return_district_name || '' } ${ patient.return_address }</span>
  166. </div>
  167. <div class="info-item" v-if="patient.report_email"><span class="label">报告邮箱:</span><span class="value">${ patient.report_email }</span></div>
  168. <div class="info-item" v-if="patient.sample_tracking_no"><span class="label">物流单号:</span><span class="value">${ patient.sample_tracking_no }</span></div>
  169. <div class="info-item" v-if="patient.sample_info_status == 2 && patient.return_tracking_no"><span class="label">回寄单号:</span><span class="value">${ patient.return_tracking_no }</span></div>
  170. <div class="info-item" v-if="patient.sample_info_status == 2 && patient.return_time"><span class="label">回寄时间:</span><span class="value">${ patient.return_time }</span></div>
  171. <div class="info-item" v-if="patient.sample_edit_reason"><span class="label">申请原因:</span><span class="value">${ patient.sample_edit_reason }</span></div>
  172. <div class="info-item" v-if="patient.sample_edit_apply_time"><span class="label">申请时间:</span><span class="value">${ patient.sample_edit_apply_time }</span></div>
  173. <div class="info-item" v-if="patient.sample_edit_reject_reason"><span class="label">驳回原因:</span><span class="value">${ patient.sample_edit_reject_reason }</span></div>
  174. <div class="info-item" v-if="patient.sample_edit_audit_time"><span class="label">处理时间:</span><span class="value">${ patient.sample_edit_audit_time }</span></div>
  175. </div>
  176. <div v-if="patient.sample_photos && patient.sample_photos.length" style="margin-top:16px;">
  177. <div style="font-size:13px;color:#909399;margin-bottom:8px;">送检单照片</div>
  178. <div class="doc-images">
  179. <div v-for="(photo, idx) in patient.sample_photos" :key="idx">
  180. <el-image :src="photo" fit="cover" :preview-src-list="patient.sample_photos" :initial-index="idx"
  181. style="width:200px;height:140px;border-radius:8px;border:1px solid #EBEEF5;" />
  182. </div>
  183. </div>
  184. </div>
  185. </div>
  186. <!-- 流转日志 -->
  187. <div class="detail-panel">
  188. <h3>流转日志</h3>
  189. <ul class="timeline-list" v-if="audits.length">
  190. <li v-for="(item, idx) in audits" :key="idx">
  191. <div class="time">${ item.create_time }</div>
  192. <div class="desc">${ item.title || item.content || '流转记录' }</div>
  193. <div class="subdesc" v-if="item.content && item.title">${ item.content }</div>
  194. <div class="reason" v-if="item.reason">${ item.action === 'reject' || item.action === 'sample_reject_edit' ? '驳回原因' : '原因' }:${ item.reason }</div>
  195. </li>
  196. </ul>
  197. <el-empty v-else description="暂无流转日志" :image-size="60" />
  198. </div>
  199. </div>
  200. <!-- 固定底部操作栏 -->
  201. <div class="fixed-bottom-bar">
  202. <el-button @click="goBack">返 回</el-button>
  203. <el-button v-if="(patient.status === 0 || patient.status === 2) && canAudit" type="success" @click="handleApprove">审核通过</el-button>
  204. <el-button v-if="(patient.status === 0 || patient.status === 1) && canAudit" type="danger" @click="showRejectDialog">驳 回</el-button>
  205. <el-button v-if="canEdit" type="primary" @click="showSampleEditDialog">编辑送检信息</el-button>
  206. <el-button v-if="patient.sample_info_status == 3 && canEdit" type="success" @click="approveSampleEdit">通过送检修改申请</el-button>
  207. <el-button v-if="patient.sample_info_status == 3 && canEdit" type="danger" @click="showRejectSampleEditDialog">驳回送检修改申请</el-button>
  208. <el-button v-if="patient.sample_info_status == 1 && canEdit" type="warning" @click="resetSampleInfo">重置送检信息编辑</el-button>
  209. <el-button v-if="patient.wax_return && (patient.sample_info_status == 1 || patient.sample_info_status == 2) && canEdit" type="primary" @click="showReturnTrackingDialog">
  210. ${ patient.sample_info_status == 2 ? '修改回寄物流单号' : '填写回寄物流单号' }
  211. </el-button>
  212. </div>
  213. <!-- 驳回弹窗 -->
  214. <el-dialog v-model="rejectVisible" title="驳回审核" width="540px" destroy-on-close :close-on-click-modal="false" :z-index="2000">
  215. <p style="font-weight:500;color:#303133;margin-bottom:12px;">请选择或填写驳回原因(必填):</p>
  216. <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;">
  217. <el-check-tag v-for="(r, i) in commonReasons" :key="i" :checked="selectedReasons.includes(i)"
  218. @change="toggleReason(i)">${ r }</el-check-tag>
  219. </div>
  220. <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因,或点击上方常见原因快速选择"></el-input>
  221. <div style="text-align:right;margin-top:20px;">
  222. <el-button @click="rejectVisible = false">取消</el-button>
  223. <el-button type="danger" @click="doReject" :loading="rejectSaving">确认驳回</el-button>
  224. </div>
  225. </el-dialog>
  226. <!-- 回寄物流单号弹窗 -->
  227. <el-dialog v-model="returnTrackingVisible" :title="patient.sample_info_status == 2 ? '修改回寄物流单号' : '填写回寄物流单号'" width="460px" destroy-on-close :close-on-click-modal="false" :z-index="2000">
  228. <el-form label-width="110px">
  229. <el-form-item label="回寄物流单号" required>
  230. <el-input v-model="returnTrackingNo" placeholder="请输入回寄物流单号" maxlength="100" clearable></el-input>
  231. </el-form-item>
  232. </el-form>
  233. <div style="text-align:right;margin-top:20px;">
  234. <el-button @click="returnTrackingVisible = false">取消</el-button>
  235. <el-button type="primary" @click="saveReturnTrackingNo" :loading="returnTrackingSaving">保存</el-button>
  236. </div>
  237. </el-dialog>
  238. <!-- 驳回送检修改申请弹窗 -->
  239. <el-dialog v-model="sampleRejectVisible" title="驳回送检修改申请" width="520px" destroy-on-close :close-on-click-modal="false" :z-index="2000">
  240. <el-form label-width="90px">
  241. <el-form-item label="驳回原因" required>
  242. <el-input v-model="sampleRejectReason" type="textarea" :rows="4" placeholder="请输入驳回原因" maxlength="500" show-word-limit></el-input>
  243. </el-form-item>
  244. </el-form>
  245. <div style="text-align:right;margin-top:20px;">
  246. <el-button @click="sampleRejectVisible = false">取消</el-button>
  247. <el-button type="danger" @click="rejectSampleEdit" :loading="sampleRejectSaving">确认驳回</el-button>
  248. </div>
  249. </el-dialog>
  250. <!-- 编辑送检信息弹窗 -->
  251. <el-dialog v-model="sampleEditVisible" title="编辑送检信息" width="760px" destroy-on-close draggable :close-on-click-modal="false" :z-index="2000">
  252. <el-form :model="sampleEditForm" label-width="120px">
  253. <el-form-item label="送检样本类型">
  254. <el-checkbox-group v-model="sampleEditForm.sample_types" @change="onSampleEditTypesChange">
  255. <el-checkbox v-for="st in sampleTypeList" :key="st.id" :label="st.name">${ st.name }</el-checkbox>
  256. </el-checkbox-group>
  257. </el-form-item>
  258. <el-form-item v-if="sampleEditShowWaxReturn" :label="sampleEditNeedReturnNames + '是否需寄回'">
  259. <el-radio-group v-model="sampleEditForm.wax_return">
  260. <el-radio :label="1">是</el-radio>
  261. <el-radio :label="0">否</el-radio>
  262. </el-radio-group>
  263. </el-form-item>
  264. <template v-if="sampleEditShowWaxReturn && sampleEditForm.wax_return === 1">
  265. <el-form-item label="回寄收件人" required>
  266. <el-input v-model="sampleEditForm.return_name" placeholder="请输入回寄收件人"></el-input>
  267. </el-form-item>
  268. <el-form-item label="回寄电话" required>
  269. <el-input v-model="sampleEditForm.return_phone" placeholder="请输入回寄电话" maxlength="20"></el-input>
  270. </el-form-item>
  271. <el-form-item label="回寄地区" required>
  272. <el-cascader v-model="sampleEditForm.returnRegionCodes" :options="regionTree"
  273. :props="{ value: 'code', label: 'name', children: 'children' }"
  274. placeholder="请选择省/市/区" clearable style="width:100%;" />
  275. </el-form-item>
  276. <el-form-item label="详细地址" required>
  277. <el-input v-model="sampleEditForm.return_address" placeholder="请输入详细地址"></el-input>
  278. </el-form-item>
  279. </template>
  280. <template v-if="sampleEditForm.sample_types && sampleEditForm.sample_types.length">
  281. <el-form-item label="报告接收邮箱">
  282. <el-input v-model="sampleEditForm.report_email" placeholder="请输入邮箱地址"></el-input>
  283. </el-form-item>
  284. <el-form-item label="送检物流单号">
  285. <el-input v-model="sampleEditForm.sample_tracking_no" placeholder="请输入送检物流单号"></el-input>
  286. </el-form-item>
  287. <el-form-item label="送检单照片">
  288. <div style="display:flex;flex-wrap:wrap;gap:10px;">
  289. <div class="sample-photo-edit" v-for="(photo, idx) in sampleEditForm.sample_photos" :key="idx">
  290. <el-image :src="photo" fit="cover" :preview-src-list="sampleEditForm.sample_photos" :initial-index="idx"></el-image>
  291. <div class="del" @click="sampleEditForm.sample_photos.splice(idx, 1)">×</div>
  292. </div>
  293. <el-upload action="/admin/upload" :show-file-list="false" accept="image/*" :headers="uploadHeaders" :on-success="onSampleEditPhotoUpload">
  294. <div style="width:80px;height:80px;border:1px dashed #DCDFE6;border-radius:6px;display:flex;align-items:center;justify-content:center;color:#909399;font-size:24px;cursor:pointer;">+</div>
  295. </el-upload>
  296. </div>
  297. </el-form-item>
  298. </template>
  299. </el-form>
  300. <div style="text-align:right;margin-top:20px;">
  301. <el-button @click="sampleEditVisible = false">取消</el-button>
  302. <el-button type="primary" @click="saveSampleEdit" :loading="sampleEditSaving">保存并生效</el-button>
  303. </div>
  304. </el-dialog>
  305. </div>{% endblock %}
  306. {% block js %}
  307. <script>
  308. var patientId = '{{ patientId }}';
  309. var canAuditVal = {{ canAudit | dump | safe }};
  310. var canEditVal = {{ canEdit | dump | safe }};
  311. var { createApp, ref, reactive, onMounted } = Vue;
  312. var app = createApp({
  313. delimiters: ['${', '}'],
  314. setup() {
  315. var loading = ref(true);
  316. var canAudit = ref(canAuditVal);
  317. var canEdit = ref(canEditVal);
  318. var patient = reactive({
  319. id: '', patient_no: '', name: '', phone: '', id_card: '', gender: '', birth_date: '',
  320. province_code: '', city_code: '', district_code: '',
  321. province_name: '', city_name: '', district_name: '',
  322. address: '', hospital: '',
  323. hospital_province_code: '', hospital_city_code: '', hospital_district_code: '',
  324. hospital_province_name: '', hospital_city_name: '', hospital_district_name: '',
  325. tag: '', documents: [],
  326. sample_types: [], wax_return: 0,
  327. return_name: '', return_phone: '',
  328. return_province_code: '', return_city_code: '', return_district_code: '',
  329. return_province_name: '', return_city_name: '', return_district_name: '',
  330. return_address: '',
  331. report_email: '', sample_tracking_no: '', sample_photos: [],
  332. sample_info_status: 0, return_tracking_no: '', return_time: '',
  333. sample_edit_reason: '', sample_edit_reject_reason: '',
  334. sample_edit_apply_time: '', sample_edit_audit_time: '',
  335. sign_income: '', sign_privacy: '', sign_promise: '',
  336. emergency_contact: '', emergency_phone: '',
  337. status: -1, create_time: ''
  338. });
  339. var audits = ref([]);
  340. var rejectVisible = ref(false);
  341. var rejectSaving = ref(false);
  342. var rejectReason = ref('');
  343. var returnTrackingVisible = ref(false);
  344. var returnTrackingSaving = ref(false);
  345. var returnTrackingNo = ref('');
  346. var uploadHeaders = {};
  347. var sampleRejectVisible = ref(false);
  348. var sampleRejectSaving = ref(false);
  349. var sampleRejectReason = ref('');
  350. var sampleEditVisible = ref(false);
  351. var sampleEditSaving = ref(false);
  352. var sampleTypeList = ref([]);
  353. var regionTree = ref([]);
  354. var sampleEditForm = reactive({
  355. sample_types: [],
  356. wax_return: 0,
  357. return_name: '',
  358. return_phone: '',
  359. returnRegionCodes: [],
  360. return_address: '',
  361. report_email: '',
  362. sample_tracking_no: '',
  363. sample_photos: []
  364. });
  365. var selectedReasons = ref([]);
  366. var commonReasons = [
  367. '身份证照片模糊,请重新上传',
  368. '病历资料不完整,请补充',
  369. '检查报告缺失,请上传',
  370. '姓名与身份证信息不一致',
  371. '手机号无法联系,请核实',
  372. '提交信息存在明显错误',
  373. '资料过期,请提供最新版本'
  374. ];
  375. async function loadDetail() {
  376. loading.value = true;
  377. try {
  378. var res = await fetch('/admin/patient/info?id=' + patientId).then(function(r) { return r.json(); });
  379. if (res.code === 0) {
  380. Object.assign(patient, res.data.patient);
  381. audits.value = res.data.audits || [];
  382. } else {
  383. ElementPlus.ElMessage.error(res.msg || '加载失败');
  384. }
  385. } finally {
  386. loading.value = false;
  387. }
  388. }
  389. async function loadSampleTypes() {
  390. try {
  391. var res = await fetch('/common/sampleTypes').then(function(r) { return r.json(); });
  392. if (res.code === 0) sampleTypeList.value = (res.data && res.data.list) || [];
  393. } catch(e) {}
  394. }
  395. async function loadRegions() {
  396. try {
  397. var res = await fetch('/common/regions').then(function(r) { return r.json(); });
  398. if (res.code === 0) regionTree.value = res.data || [];
  399. } catch(e) {}
  400. }
  401. function goBack() {
  402. window.location.href = '/admin/patient.html';
  403. }
  404. function downloadSign(url, name) {
  405. if (!url) { ElementPlus.ElMessage.warning('该材料未上传'); return; }
  406. var a = document.createElement('a');
  407. a.href = url; a.target = '_blank'; a.download = name; a.click();
  408. }
  409. function isImageUrl(url) {
  410. if (!url) return false;
  411. var lower = url.split('?')[0].toLowerCase();
  412. return /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/.test(lower);
  413. }
  414. var hospitalRegionText = Vue.computed(function() {
  415. return [
  416. patient.hospital_province_name,
  417. patient.hospital_city_name,
  418. patient.hospital_district_name
  419. ].filter(Boolean).join(' ');
  420. });
  421. var signDocs = Vue.computed(function() {
  422. var docs = [
  423. { key: 'income', label: '个人可支配收入声明', url: patient.sign_income },
  424. { key: 'privacy', label: '个人信息处理同意书', url: patient.sign_privacy },
  425. { key: 'promise', label: '声明与承诺', url: patient.sign_promise }
  426. ];
  427. if (patient.sign_privacy_jhr) {
  428. docs.push({ key: 'privacy_jhr', label: '监护人个人信息处理同意书', url: patient.sign_privacy_jhr });
  429. }
  430. return docs;
  431. });
  432. var signImageList = Vue.computed(function() {
  433. return [patient.sign_income, patient.sign_privacy, patient.sign_promise, patient.sign_privacy_jhr].filter(function(u) { return u && isImageUrl(u); });
  434. });
  435. var authImageList = Vue.computed(function() {
  436. return [patient.id_card_front, patient.id_card_back, patient.photo].filter(Boolean);
  437. });
  438. var sampleEditShowWaxReturn = Vue.computed(function() {
  439. if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) return false;
  440. return sampleEditForm.sample_types.some(function(name) {
  441. var st = sampleTypeList.value.find(function(item) { return item.name === name; });
  442. return st && st.need_return;
  443. });
  444. });
  445. var sampleEditNeedReturnNames = Vue.computed(function() {
  446. if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) return '';
  447. return sampleEditForm.sample_types.filter(function(name) {
  448. var st = sampleTypeList.value.find(function(item) { return item.name === name; });
  449. return st && st.need_return;
  450. }).join('、');
  451. });
  452. function sampleInfoStatusText(status) {
  453. if (Number(status) === 3) return '修改申请待审核';
  454. if (Number(status) === 2) return '已寄回';
  455. if (Number(status) === 1) return '已生效';
  456. return '可修改';
  457. }
  458. function sampleInfoStatusType(status) {
  459. if (Number(status) === 3) return 'warning';
  460. if (Number(status) === 2) return 'primary';
  461. if (Number(status) === 1) return 'success';
  462. return 'warning';
  463. }
  464. async function handleApprove() {
  465. try {
  466. await ElementPlus.ElMessageBox.confirm(
  467. '确定要通过该患者的审核吗?通过后患者将收到审核通过通知。',
  468. '确认审核通过',
  469. { confirmButtonText: '确认通过', cancelButtonText: '取消', type: 'success' }
  470. );
  471. var res = await fetch('/admin/patient/approve', {
  472. method: 'POST',
  473. headers: { 'Content-Type': 'application/json' },
  474. body: JSON.stringify({ id: patient.id })
  475. }).then(function(r) { return r.json(); });
  476. if (res.code === 0) {
  477. ElementPlus.ElMessage.success('审核已通过');
  478. loadDetail();
  479. } else {
  480. ElementPlus.ElMessage.error(res.msg || '操作失败');
  481. }
  482. } catch(e) {}
  483. }
  484. function showRejectDialog() {
  485. rejectReason.value = '';
  486. selectedReasons.value = [];
  487. rejectVisible.value = true;
  488. }
  489. function toggleReason(index) {
  490. var pos = selectedReasons.value.indexOf(index);
  491. if (pos > -1) { selectedReasons.value.splice(pos, 1); }
  492. else { selectedReasons.value.push(index); }
  493. rejectReason.value = selectedReasons.value.map(function(i) { return commonReasons[i]; }).join(';');
  494. }
  495. async function doReject() {
  496. if (!rejectReason.value.trim()) { ElementPlus.ElMessage.warning('请填写驳回原因'); return; }
  497. rejectSaving.value = true;
  498. try {
  499. var res = await fetch('/admin/patient/reject', {
  500. method: 'POST',
  501. headers: { 'Content-Type': 'application/json' },
  502. body: JSON.stringify({ id: patient.id, reason: rejectReason.value.trim() })
  503. }).then(function(r) { return r.json(); });
  504. if (res.code === 0) {
  505. ElementPlus.ElMessage.success('已驳回');
  506. rejectVisible.value = false;
  507. loadDetail();
  508. } else {
  509. ElementPlus.ElMessage.error(res.msg || '操作失败');
  510. }
  511. } finally {
  512. rejectSaving.value = false;
  513. }
  514. }
  515. async function resetSampleInfo() {
  516. try {
  517. await ElementPlus.ElMessageBox.confirm(
  518. '确认将该患者送检信息状态重置为可修改吗?重置后患者可在小程序重新编辑并提交送检信息。',
  519. '重置送检信息编辑',
  520. { confirmButtonText: '确认重置', cancelButtonText: '取消', type: 'warning' }
  521. );
  522. var res = await fetch('/admin/patient/resetSampleInfo', {
  523. method: 'POST',
  524. headers: { 'Content-Type': 'application/json' },
  525. body: JSON.stringify({ id: patient.id })
  526. }).then(function(r) { return r.json(); });
  527. if (res.code === 0) {
  528. ElementPlus.ElMessage.success('已重置为可修改');
  529. loadDetail();
  530. } else {
  531. ElementPlus.ElMessage.error(res.msg || '重置失败');
  532. }
  533. } catch(e) {}
  534. }
  535. function showReturnTrackingDialog() {
  536. returnTrackingNo.value = patient.return_tracking_no || '';
  537. returnTrackingVisible.value = true;
  538. }
  539. async function saveReturnTrackingNo() {
  540. if (!returnTrackingNo.value.trim()) {
  541. ElementPlus.ElMessage.warning('请填写回寄物流单号');
  542. return;
  543. }
  544. returnTrackingSaving.value = true;
  545. try {
  546. var res = await fetch('/admin/patient/saveReturnTrackingNo', {
  547. method: 'POST',
  548. headers: { 'Content-Type': 'application/json' },
  549. body: JSON.stringify({ id: patient.id, return_tracking_no: returnTrackingNo.value.trim() })
  550. }).then(function(r) { return r.json(); });
  551. if (res.code === 0) {
  552. ElementPlus.ElMessage.success('保存成功');
  553. returnTrackingVisible.value = false;
  554. loadDetail();
  555. } else {
  556. ElementPlus.ElMessage.error(res.msg || '保存失败');
  557. }
  558. } finally {
  559. returnTrackingSaving.value = false;
  560. }
  561. }
  562. async function approveSampleEdit() {
  563. try {
  564. await ElementPlus.ElMessageBox.confirm(
  565. '确认通过该患者的送检信息修改申请吗?通过后患者可在小程序重新编辑并提交送检信息。',
  566. '通过送检修改申请',
  567. { confirmButtonText: '确认通过', cancelButtonText: '取消', type: 'success' }
  568. );
  569. var res = await fetch('/admin/patient/approveSampleEdit', {
  570. method: 'POST',
  571. headers: { 'Content-Type': 'application/json' },
  572. body: JSON.stringify({ id: patient.id })
  573. }).then(function(r) { return r.json(); });
  574. if (res.code === 0) {
  575. ElementPlus.ElMessage.success('已通过申请');
  576. loadDetail();
  577. } else {
  578. ElementPlus.ElMessage.error(res.msg || '操作失败');
  579. }
  580. } catch(e) {}
  581. }
  582. function showRejectSampleEditDialog() {
  583. sampleRejectReason.value = '';
  584. sampleRejectVisible.value = true;
  585. }
  586. async function rejectSampleEdit() {
  587. if (!sampleRejectReason.value.trim()) {
  588. ElementPlus.ElMessage.warning('请填写驳回原因');
  589. return;
  590. }
  591. sampleRejectSaving.value = true;
  592. try {
  593. var res = await fetch('/admin/patient/rejectSampleEdit', {
  594. method: 'POST',
  595. headers: { 'Content-Type': 'application/json' },
  596. body: JSON.stringify({ id: patient.id, reason: sampleRejectReason.value.trim() })
  597. }).then(function(r) { return r.json(); });
  598. if (res.code === 0) {
  599. ElementPlus.ElMessage.success('已驳回申请');
  600. sampleRejectVisible.value = false;
  601. loadDetail();
  602. } else {
  603. ElementPlus.ElMessage.error(res.msg || '操作失败');
  604. }
  605. } finally {
  606. sampleRejectSaving.value = false;
  607. }
  608. }
  609. function onSampleEditTypesChange() {
  610. if (!sampleEditShowWaxReturn.value) {
  611. sampleEditForm.wax_return = 0;
  612. sampleEditForm.return_name = '';
  613. sampleEditForm.return_phone = '';
  614. sampleEditForm.returnRegionCodes = [];
  615. sampleEditForm.return_address = '';
  616. }
  617. if (!sampleEditForm.sample_types || !sampleEditForm.sample_types.length) {
  618. sampleEditForm.report_email = '';
  619. sampleEditForm.sample_tracking_no = '';
  620. sampleEditForm.sample_photos = [];
  621. }
  622. }
  623. function showSampleEditDialog() {
  624. Object.assign(sampleEditForm, {
  625. sample_types: (patient.sample_types || []).slice(),
  626. wax_return: patient.wax_return ? 1 : 0,
  627. return_name: patient.return_name || '',
  628. return_phone: patient.return_phone || '',
  629. returnRegionCodes: [patient.return_province_code, patient.return_city_code, patient.return_district_code].filter(Boolean),
  630. return_address: patient.return_address || '',
  631. report_email: patient.report_email || '',
  632. sample_tracking_no: patient.sample_tracking_no || '',
  633. sample_photos: (patient.sample_photos || []).slice()
  634. });
  635. sampleEditVisible.value = true;
  636. }
  637. function onSampleEditPhotoUpload(res) {
  638. if (res.code === 0 && res.data && res.data.url) {
  639. sampleEditForm.sample_photos.push(res.data.url);
  640. } else {
  641. ElementPlus.ElMessage.error(res.msg || '上传失败');
  642. }
  643. }
  644. async function saveSampleEdit() {
  645. var regionCodes = sampleEditForm.returnRegionCodes || [];
  646. if (sampleEditShowWaxReturn.value && sampleEditForm.wax_return === 1) {
  647. if (!sampleEditForm.return_name.trim()) return ElementPlus.ElMessage.warning('请填写回寄收件人');
  648. if (!sampleEditForm.return_phone.trim()) return ElementPlus.ElMessage.warning('请填写回寄电话');
  649. if (regionCodes.length !== 3) return ElementPlus.ElMessage.warning('请选择回寄地区');
  650. if (!sampleEditForm.return_address.trim()) return ElementPlus.ElMessage.warning('请填写回寄详细地址');
  651. }
  652. if (sampleEditForm.report_email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(sampleEditForm.report_email)) {
  653. return ElementPlus.ElMessage.warning('邮箱格式不正确');
  654. }
  655. sampleEditSaving.value = true;
  656. try {
  657. var res = await fetch('/admin/patient/editSampleInfo', {
  658. method: 'POST',
  659. headers: { 'Content-Type': 'application/json' },
  660. body: JSON.stringify({
  661. id: patient.id,
  662. sample_types: sampleEditForm.sample_types || [],
  663. wax_return: sampleEditShowWaxReturn.value ? sampleEditForm.wax_return : 0,
  664. return_name: sampleEditForm.return_name.trim(),
  665. return_phone: sampleEditForm.return_phone.trim(),
  666. return_province_code: regionCodes[0] || '',
  667. return_city_code: regionCodes[1] || '',
  668. return_district_code: regionCodes[2] || '',
  669. return_address: sampleEditForm.return_address.trim(),
  670. report_email: sampleEditForm.report_email.trim(),
  671. sample_tracking_no: sampleEditForm.sample_tracking_no.trim(),
  672. sample_photos: sampleEditForm.sample_photos || []
  673. })
  674. }).then(function(r) { return r.json(); });
  675. if (res.code === 0) {
  676. ElementPlus.ElMessage.success('送检信息已保存并生效');
  677. sampleEditVisible.value = false;
  678. loadDetail();
  679. } else {
  680. ElementPlus.ElMessage.error(res.msg || '保存失败');
  681. }
  682. } finally {
  683. sampleEditSaving.value = false;
  684. }
  685. }
  686. onMounted(function() {
  687. loadDetail();
  688. loadSampleTypes();
  689. loadRegions();
  690. });
  691. return {
  692. loading, patient, audits, canAudit, canEdit,
  693. returnTrackingVisible, returnTrackingSaving, returnTrackingNo,
  694. sampleRejectVisible, sampleRejectSaving, sampleRejectReason,
  695. sampleEditVisible, sampleEditSaving, sampleEditForm, sampleTypeList, regionTree, uploadHeaders,
  696. rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons,
  697. hospitalRegionText, goBack, downloadSign, isImageUrl, signDocs, signImageList,
  698. authImageList, sampleInfoStatusText, sampleInfoStatusType, sampleEditShowWaxReturn, sampleEditNeedReturnNames,
  699. handleApprove, showRejectDialog, toggleReason, doReject, resetSampleInfo,
  700. showReturnTrackingDialog, saveReturnTrackingNo,
  701. approveSampleEdit, showRejectSampleEditDialog, rejectSampleEdit,
  702. showSampleEditDialog, onSampleEditTypesChange, onSampleEditPhotoUpload, saveSampleEdit
  703. };
  704. }
  705. });
  706. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  707. app.mount('#detailApp');
  708. </script>
  709. {% endblock %}