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.
 
 
 
 
 

487 regels
24 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 .reason { font-size: 13px; color: #F56C6C; margin-top: 4px; }
  22. .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; }
  23. </style>
  24. {% endblock %}
  25. {% block content %}
  26. <div id="detailApp" v-cloak>
  27. <div v-loading="loading" style="padding-bottom:80px;">
  28. <!-- 页头 -->
  29. <el-page-header @back="goBack" style="margin-bottom:16px;">
  30. <template #content>
  31. <span style="font-size:16px;font-weight:600;">患者详情</span>
  32. <el-tag v-if="patient.status === -1" type="info" size="small" style="margin-left:12px;">待提交</el-tag>
  33. <el-tag v-else-if="patient.status === 0" type="warning" size="small" style="margin-left:12px;">待审核</el-tag>
  34. <el-tag v-else-if="patient.status === 1" type="success" size="small" style="margin-left:12px;">审核通过</el-tag>
  35. <el-tag v-else-if="patient.status === 2" type="danger" size="small" style="margin-left:12px;">已驳回</el-tag>
  36. </template>
  37. </el-page-header>
  38. <!-- 基本信息 -->
  39. <div class="detail-panel">
  40. <h3>基本信息</h3>
  41. <div class="info-grid">
  42. <div class="info-item"><span class="label">患者编号:</span><span class="value">${ patient.patient_no }</span></div>
  43. <div class="info-item"><span class="label">姓名:</span><span class="value">${ patient.name }</span></div>
  44. <div class="info-item"><span class="label">性别:</span><span class="value">${ patient.gender }</span></div>
  45. <div class="info-item"><span class="label">手机号:</span><span class="value">${ patient.phone }</span></div>
  46. <div class="info-item"><span class="label">身份证号:</span><span class="value">${ patient.id_card }</span></div>
  47. <div class="info-item"><span class="label">出生日期:</span><span class="value">${ patient.birth_date }</span></div>
  48. <div class="info-item">
  49. <span class="label">所在地区:</span>
  50. <span class="value">${ patient.province_name } ${ patient.city_name } ${ patient.district_name }</span>
  51. </div>
  52. <div class="info-item"><span class="label">详细地址:</span><span class="value">${ patient.address }</span></div>
  53. <div class="info-item">
  54. <span class="label">瘤种:</span>
  55. <span class="value">
  56. <el-tag v-if="patient.tag" type="danger" size="small">${ patient.tag }</el-tag>
  57. <span v-else style="color:#999;">无</span>
  58. </span>
  59. </div>
  60. <div class="info-item"><span class="label">提交时间:</span><span class="value">${ patient.create_time }</span></div>
  61. <div class="info-item"><span class="label">医院名称:</span><span class="value">${ patient.hospital || '—' }</span></div>
  62. <div class="info-item">
  63. <span class="label">医院所在地:</span>
  64. <span class="value">${ hospitalRegionText || '—' }</span>
  65. </div>
  66. <div class="info-item"><span class="label">紧急联系人:</span><span class="value">${ patient.emergency_contact || '—' }</span></div>
  67. <div class="info-item"><span class="label">紧急联系电话:</span><span class="value">${ patient.emergency_phone || '—' }</span></div>
  68. <div class="info-item" v-if="patient.income_amount"><span class="label">年可支配收入:</span><span class="value">${ patient.income_amount } 元</span></div>
  69. <div class="info-item" v-if="patient.guardian_name"><span class="label">监护人姓名:</span><span class="value">${ patient.guardian_name }</span></div>
  70. <div class="info-item" v-if="patient.guardian_id_card"><span class="label">监护人身份证:</span><span class="value">${ patient.guardian_id_card }</span></div>
  71. <div class="info-item" v-if="patient.guardian_relation"><span class="label">与患者关系:</span><span class="value">${ patient.guardian_relation }</span></div>
  72. </div>
  73. </div>
  74. <!-- 实名认证照片 -->
  75. <div class="detail-panel" v-if="patient.id_card_front || patient.id_card_back || patient.photo">
  76. <h3>实名认证照片</h3>
  77. <p style="font-size:13px;color:#909399;margin-bottom:12px;">
  78. <template v-if="patient.id_card_type === 2">无证件儿童免冠照片</template>
  79. <template v-else>身份证人像面 / 国徽面照片</template>
  80. </p>
  81. <div class="doc-images">
  82. <div v-if="patient.id_card_front">
  83. <el-image :src="patient.id_card_front" fit="cover" :preview-src-list="authImageList"
  84. :initial-index="0" style="width:260px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
  85. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">人像面</div>
  86. </div>
  87. <div v-if="patient.id_card_back">
  88. <el-image :src="patient.id_card_back" fit="cover" :preview-src-list="authImageList"
  89. :initial-index="patient.id_card_front ? 1 : 0" style="width:260px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
  90. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">国徽面</div>
  91. </div>
  92. <div v-if="patient.photo">
  93. <el-image :src="patient.photo" fit="cover" :preview-src-list="[patient.photo]"
  94. :initial-index="0" style="width:170px;height:170px;border-radius:8px;border:1px solid #EBEEF5;" />
  95. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">免冠照片</div>
  96. </div>
  97. </div>
  98. </div>
  99. <!-- 上传资料 -->
  100. <div class="detail-panel">
  101. <h3>上传资料</h3>
  102. <p style="font-size:13px;color:#909399;margin-bottom:12px;">患者上传的检查报告单或出院诊断证明书</p>
  103. <div class="doc-images" v-if="patient.documents && patient.documents.length">
  104. <div v-for="(doc, idx) in patient.documents" :key="idx">
  105. <el-image :src="doc" fit="cover" :preview-src-list="patient.documents" :initial-index="idx"
  106. style="width:200px;height:140px;border-radius:8px;border:1px solid #EBEEF5;" />
  107. <div style="font-size:12px;color:#909399;text-align:center;margin-top:6px;">资料 ${ idx + 1 }</div>
  108. </div>
  109. </div>
  110. <el-empty v-else description="暂无上传资料" :image-size="60" />
  111. </div>
  112. <!-- 签字材料 -->
  113. <div class="detail-panel">
  114. <h3>签字材料</h3>
  115. <p style="font-size:13px;color:#909399;margin-bottom:16px;">患者签署的授权材料</p>
  116. <div style="display:flex;gap:20px;flex-wrap:wrap;">
  117. <div v-for="item in signDocs" :key="item.key" style="width:220px;">
  118. <div style="font-size:13px;color:#303133;font-weight:500;margin-bottom:8px;">${ item.label }</div>
  119. <template v-if="item.url && isImageUrl(item.url)">
  120. <el-image :src="item.url" fit="cover" :preview-src-list="signImageList" :initial-index="signImageList.indexOf(item.url)"
  121. class="sign-img-wrap" style="width:220px;height:160px;border-radius:8px;border:1px solid #EBEEF5;cursor:pointer;" />
  122. </template>
  123. <template v-else-if="item.url">
  124. <div class="sign-doc-card" @click="downloadSign(item.url, item.label)">
  125. <div style="font-size:36px;margin-bottom:8px;">📝</div>
  126. <div style="font-size:12px;color:#67C23A;margin-bottom:8px;">已签署</div>
  127. <div style="font-size:13px;color:var(--el-color-primary);font-weight:500;">⬇ 下载</div>
  128. </div>
  129. </template>
  130. <template v-else>
  131. <div style="width:220px;height:160px;border:1px dashed #DCDFE6;border-radius:8px;display:flex;align-items:center;justify-content:center;">
  132. <span style="font-size:13px;color:#909399;">未上传</span>
  133. </div>
  134. </template>
  135. </div>
  136. </div>
  137. </div>
  138. <!-- 送检信息 -->
  139. <div class="detail-panel">
  140. <h3>送检信息</h3>
  141. <div class="info-grid">
  142. <div class="info-item"><span class="label">状态:</span>
  143. <span class="value">
  144. <el-tag :type="sampleInfoStatusType(patient.sample_info_status)" size="small">
  145. ${ sampleInfoStatusText(patient.sample_info_status) }
  146. </el-tag>
  147. </span>
  148. </div>
  149. <div class="info-item"><span class="label">送检样本:</span>
  150. <span class="value">
  151. <template v-if="patient.sample_types && patient.sample_types.length">
  152. <el-tag v-for="st in patient.sample_types" :key="st" size="small" style="margin-right:4px;">${ st }</el-tag>
  153. </template>
  154. <span v-else style="color:#999;">未选择</span>
  155. </span>
  156. </div>
  157. <div class="info-item" v-if="patient.wax_return"><span class="label">样本需寄回:</span><span class="value">是</span></div>
  158. <div class="info-item" v-if="patient.return_name"><span class="label">收件人:</span><span class="value">${ patient.return_name }</span></div>
  159. <div class="info-item" v-if="patient.return_phone"><span class="label">收件电话:</span><span class="value">${ patient.return_phone }</span></div>
  160. <div class="info-item" v-if="patient.return_address"><span class="label">收件地址:</span>
  161. <span class="value">${ patient.return_province_name || '' } ${ patient.return_city_name || '' } ${ patient.return_district_name || '' } ${ patient.return_address }</span>
  162. </div>
  163. <div class="info-item" v-if="patient.report_email"><span class="label">报告邮箱:</span><span class="value">${ patient.report_email }</span></div>
  164. <div class="info-item" v-if="patient.sample_tracking_no"><span class="label">物流单号:</span><span class="value">${ patient.sample_tracking_no }</span></div>
  165. <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>
  166. <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>
  167. </div>
  168. <div v-if="patient.sample_photos && patient.sample_photos.length" style="margin-top:16px;">
  169. <div style="font-size:13px;color:#909399;margin-bottom:8px;">送检单照片</div>
  170. <div class="doc-images">
  171. <div v-for="(photo, idx) in patient.sample_photos" :key="idx">
  172. <el-image :src="photo" fit="cover" :preview-src-list="patient.sample_photos" :initial-index="idx"
  173. style="width:200px;height:140px;border-radius:8px;border:1px solid #EBEEF5;" />
  174. </div>
  175. </div>
  176. </div>
  177. </div>
  178. <!-- 审核记录 -->
  179. <div class="detail-panel">
  180. <h3>审核记录</h3>
  181. <ul class="timeline-list" v-if="audits.length">
  182. <li v-for="(item, idx) in audits" :key="idx">
  183. <div class="time">${ item.create_time }</div>
  184. <div class="desc" v-if="item.action === 'submit'">${ item.operator_name || '系统' } 提交了患者资料,等待审核</div>
  185. <div class="desc" v-else-if="item.action === 'approve'">${ item.operator_name } 审核通过</div>
  186. <div class="desc" v-else-if="item.action === 'reject'">${ item.operator_name } 驳回了资料</div>
  187. <div class="reason" v-if="item.reason">驳回原因:${ item.reason }</div>
  188. </li>
  189. </ul>
  190. <el-empty v-else description="暂无审核记录" :image-size="60" />
  191. </div>
  192. </div>
  193. <!-- 固定底部操作栏 -->
  194. <div class="fixed-bottom-bar">
  195. <el-button @click="goBack">返 回</el-button>
  196. <el-button v-if="(patient.status === 0 || patient.status === 2) && canAudit" type="success" @click="handleApprove">审核通过</el-button>
  197. <el-button v-if="(patient.status === 0 || patient.status === 1) && canAudit" type="danger" @click="showRejectDialog">驳 回</el-button>
  198. <el-button v-if="patient.sample_info_status == 1 && canEdit" type="warning" @click="resetSampleInfo">重置送检信息编辑</el-button>
  199. <el-button v-if="patient.wax_return && (patient.sample_info_status == 1 || patient.sample_info_status == 2) && canEdit" type="primary" @click="showReturnTrackingDialog">
  200. ${ patient.sample_info_status == 2 ? '修改回寄物流单号' : '填写回寄物流单号' }
  201. </el-button>
  202. </div>
  203. <!-- 驳回弹窗 -->
  204. <el-dialog v-model="rejectVisible" title="驳回审核" width="540px" destroy-on-close :close-on-click-modal="false" :z-index="2000">
  205. <p style="font-weight:500;color:#303133;margin-bottom:12px;">请选择或填写驳回原因(必填):</p>
  206. <div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;">
  207. <el-check-tag v-for="(r, i) in commonReasons" :key="i" :checked="selectedReasons.includes(i)"
  208. @change="toggleReason(i)">${ r }</el-check-tag>
  209. </div>
  210. <el-input v-model="rejectReason" type="textarea" :rows="3" placeholder="请输入驳回原因,或点击上方常见原因快速选择"></el-input>
  211. <div style="text-align:right;margin-top:20px;">
  212. <el-button @click="rejectVisible = false">取消</el-button>
  213. <el-button type="danger" @click="doReject" :loading="rejectSaving">确认驳回</el-button>
  214. </div>
  215. </el-dialog>
  216. <!-- 回寄物流单号弹窗 -->
  217. <el-dialog v-model="returnTrackingVisible" :title="patient.sample_info_status == 2 ? '修改回寄物流单号' : '填写回寄物流单号'" width="460px" destroy-on-close :close-on-click-modal="false" :z-index="2000">
  218. <el-form label-width="110px">
  219. <el-form-item label="回寄物流单号" required>
  220. <el-input v-model="returnTrackingNo" placeholder="请输入回寄物流单号" maxlength="100" clearable></el-input>
  221. </el-form-item>
  222. </el-form>
  223. <div style="text-align:right;margin-top:20px;">
  224. <el-button @click="returnTrackingVisible = false">取消</el-button>
  225. <el-button type="primary" @click="saveReturnTrackingNo" :loading="returnTrackingSaving">保存</el-button>
  226. </div>
  227. </el-dialog>
  228. </div>{% endblock %}
  229. {% block js %}
  230. <script>
  231. var patientId = '{{ patientId }}';
  232. var canAuditVal = {{ canAudit | dump | safe }};
  233. var canEditVal = {{ canEdit | dump | safe }};
  234. var { createApp, ref, reactive, onMounted } = Vue;
  235. var app = createApp({
  236. delimiters: ['${', '}'],
  237. setup() {
  238. var loading = ref(true);
  239. var canAudit = ref(canAuditVal);
  240. var canEdit = ref(canEditVal);
  241. var patient = reactive({
  242. id: '', patient_no: '', name: '', phone: '', id_card: '', gender: '', birth_date: '',
  243. province_code: '', city_code: '', district_code: '',
  244. province_name: '', city_name: '', district_name: '',
  245. address: '', hospital: '',
  246. hospital_province_code: '', hospital_city_code: '', hospital_district_code: '',
  247. hospital_province_name: '', hospital_city_name: '', hospital_district_name: '',
  248. tag: '', documents: [],
  249. sample_types: [], wax_return: 0,
  250. return_name: '', return_phone: '',
  251. return_province_code: '', return_city_code: '', return_district_code: '',
  252. return_province_name: '', return_city_name: '', return_district_name: '',
  253. return_address: '',
  254. report_email: '', sample_tracking_no: '', sample_photos: [],
  255. sample_info_status: 0, return_tracking_no: '', return_time: '',
  256. sign_income: '', sign_privacy: '', sign_promise: '',
  257. emergency_contact: '', emergency_phone: '',
  258. status: -1, create_time: ''
  259. });
  260. var audits = ref([]);
  261. var rejectVisible = ref(false);
  262. var rejectSaving = ref(false);
  263. var rejectReason = ref('');
  264. var returnTrackingVisible = ref(false);
  265. var returnTrackingSaving = ref(false);
  266. var returnTrackingNo = ref('');
  267. var selectedReasons = ref([]); var commonReasons = [
  268. '身份证照片模糊,请重新上传',
  269. '病历资料不完整,请补充',
  270. '检查报告缺失,请上传',
  271. '姓名与身份证信息不一致',
  272. '手机号无法联系,请核实',
  273. '提交信息存在明显错误',
  274. '资料过期,请提供最新版本'
  275. ];
  276. async function loadDetail() {
  277. loading.value = true;
  278. try {
  279. var res = await fetch('/admin/patient/info?id=' + patientId).then(function(r) { return r.json(); });
  280. if (res.code === 0) {
  281. Object.assign(patient, res.data.patient);
  282. audits.value = res.data.audits || [];
  283. } else {
  284. ElementPlus.ElMessage.error(res.msg || '加载失败');
  285. }
  286. } finally {
  287. loading.value = false;
  288. }
  289. }
  290. function goBack() {
  291. window.location.href = '/admin/patient.html';
  292. }
  293. function downloadSign(url, name) {
  294. if (!url) { ElementPlus.ElMessage.warning('该材料未上传'); return; }
  295. var a = document.createElement('a');
  296. a.href = url; a.target = '_blank'; a.download = name; a.click();
  297. }
  298. function isImageUrl(url) {
  299. if (!url) return false;
  300. var lower = url.split('?')[0].toLowerCase();
  301. return /\.(png|jpg|jpeg|gif|bmp|webp|svg)$/.test(lower);
  302. }
  303. var hospitalRegionText = Vue.computed(function() {
  304. return [
  305. patient.hospital_province_name,
  306. patient.hospital_city_name,
  307. patient.hospital_district_name
  308. ].filter(Boolean).join(' ');
  309. });
  310. var signDocs = Vue.computed(function() {
  311. var docs = [
  312. { key: 'income', label: '个人可支配收入声明', url: patient.sign_income },
  313. { key: 'privacy', label: '个人信息处理同意书', url: patient.sign_privacy },
  314. { key: 'promise', label: '声明与承诺', url: patient.sign_promise }
  315. ];
  316. if (patient.sign_privacy_jhr) {
  317. docs.push({ key: 'privacy_jhr', label: '监护人个人信息处理同意书', url: patient.sign_privacy_jhr });
  318. }
  319. return docs;
  320. });
  321. var signImageList = Vue.computed(function() {
  322. return [patient.sign_income, patient.sign_privacy, patient.sign_promise, patient.sign_privacy_jhr].filter(function(u) { return u && isImageUrl(u); });
  323. });
  324. var authImageList = Vue.computed(function() {
  325. return [patient.id_card_front, patient.id_card_back, patient.photo].filter(Boolean);
  326. });
  327. function sampleInfoStatusText(status) {
  328. if (Number(status) === 2) return '已寄回';
  329. if (Number(status) === 1) return '已生效';
  330. return '可修改';
  331. }
  332. function sampleInfoStatusType(status) {
  333. if (Number(status) === 2) return 'primary';
  334. if (Number(status) === 1) return 'success';
  335. return 'warning';
  336. }
  337. async function handleApprove() {
  338. try {
  339. await ElementPlus.ElMessageBox.confirm(
  340. '确定要通过该患者的审核吗?通过后患者将收到审核通过通知。',
  341. '确认审核通过',
  342. { confirmButtonText: '确认通过', cancelButtonText: '取消', type: 'success' }
  343. );
  344. var res = await fetch('/admin/patient/approve', {
  345. method: 'POST',
  346. headers: { 'Content-Type': 'application/json' },
  347. body: JSON.stringify({ id: patient.id })
  348. }).then(function(r) { return r.json(); });
  349. if (res.code === 0) {
  350. ElementPlus.ElMessage.success('审核已通过');
  351. loadDetail();
  352. } else {
  353. ElementPlus.ElMessage.error(res.msg || '操作失败');
  354. }
  355. } catch(e) {}
  356. }
  357. function showRejectDialog() {
  358. rejectReason.value = '';
  359. selectedReasons.value = [];
  360. rejectVisible.value = true;
  361. }
  362. function toggleReason(index) {
  363. var pos = selectedReasons.value.indexOf(index);
  364. if (pos > -1) { selectedReasons.value.splice(pos, 1); }
  365. else { selectedReasons.value.push(index); }
  366. rejectReason.value = selectedReasons.value.map(function(i) { return commonReasons[i]; }).join(';');
  367. }
  368. async function doReject() {
  369. if (!rejectReason.value.trim()) { ElementPlus.ElMessage.warning('请填写驳回原因'); return; }
  370. rejectSaving.value = true;
  371. try {
  372. var res = await fetch('/admin/patient/reject', {
  373. method: 'POST',
  374. headers: { 'Content-Type': 'application/json' },
  375. body: JSON.stringify({ id: patient.id, reason: rejectReason.value.trim() })
  376. }).then(function(r) { return r.json(); });
  377. if (res.code === 0) {
  378. ElementPlus.ElMessage.success('已驳回');
  379. rejectVisible.value = false;
  380. loadDetail();
  381. } else {
  382. ElementPlus.ElMessage.error(res.msg || '操作失败');
  383. }
  384. } finally {
  385. rejectSaving.value = false;
  386. }
  387. }
  388. async function resetSampleInfo() {
  389. try {
  390. await ElementPlus.ElMessageBox.confirm(
  391. '确认将该患者送检信息状态重置为可修改吗?重置后患者可在小程序重新编辑并提交送检信息。',
  392. '重置送检信息编辑',
  393. { confirmButtonText: '确认重置', cancelButtonText: '取消', type: 'warning' }
  394. );
  395. var res = await fetch('/admin/patient/resetSampleInfo', {
  396. method: 'POST',
  397. headers: { 'Content-Type': 'application/json' },
  398. body: JSON.stringify({ id: patient.id })
  399. }).then(function(r) { return r.json(); });
  400. if (res.code === 0) {
  401. ElementPlus.ElMessage.success('已重置为可修改');
  402. loadDetail();
  403. } else {
  404. ElementPlus.ElMessage.error(res.msg || '重置失败');
  405. }
  406. } catch(e) {}
  407. }
  408. function showReturnTrackingDialog() {
  409. returnTrackingNo.value = patient.return_tracking_no || '';
  410. returnTrackingVisible.value = true;
  411. }
  412. async function saveReturnTrackingNo() {
  413. if (!returnTrackingNo.value.trim()) {
  414. ElementPlus.ElMessage.warning('请填写回寄物流单号');
  415. return;
  416. }
  417. returnTrackingSaving.value = true;
  418. try {
  419. var res = await fetch('/admin/patient/saveReturnTrackingNo', {
  420. method: 'POST',
  421. headers: { 'Content-Type': 'application/json' },
  422. body: JSON.stringify({ id: patient.id, return_tracking_no: returnTrackingNo.value.trim() })
  423. }).then(function(r) { return r.json(); });
  424. if (res.code === 0) {
  425. ElementPlus.ElMessage.success('保存成功');
  426. returnTrackingVisible.value = false;
  427. loadDetail();
  428. } else {
  429. ElementPlus.ElMessage.error(res.msg || '保存失败');
  430. }
  431. } finally {
  432. returnTrackingSaving.value = false;
  433. }
  434. }
  435. onMounted(function() { loadDetail(); });
  436. return {
  437. loading, patient, audits, canAudit, canEdit,
  438. returnTrackingVisible, returnTrackingSaving, returnTrackingNo,
  439. rejectVisible, rejectSaving, rejectReason, selectedReasons, commonReasons,
  440. hospitalRegionText, goBack, downloadSign, isImageUrl, signDocs, signImageList,
  441. authImageList, sampleInfoStatusText, sampleInfoStatusType,
  442. handleApprove, showRejectDialog, toggleReason, doReject, resetSampleInfo,
  443. showReturnTrackingDialog, saveReturnTrackingNo
  444. };
  445. }
  446. });
  447. app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
  448. app.mount('#detailApp');
  449. </script>
  450. {% endblock %}