RegistrationConfirm.vue 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. <template>
  2. <el-card shadow="never" class="registration-confirm-card">
  3. <template #header>
  4. <div class="flex items-center justify-between">
  5. <span class="text-base font-semibold">挂号确认</span>
  6. <el-tag v-if="!finalized" type="danger" size="small">L2 需确认</el-tag>
  7. <el-tag v-else-if="confirmed" type="success" size="small">已完成</el-tag>
  8. <el-tag v-else type="info" size="small">{{ command.status }}</el-tag>
  9. </div>
  10. </template>
  11. <!-- 确认信息 -->
  12. <div class="mb-3">
  13. <div class="text-sm text-gray-500 mb-2">请确认以下挂号信息</div>
  14. <div class="summary-grid">
  15. <div v-for="(v, k) in summaryEntries" :key="k" class="summary-item">
  16. <span class="summary-key">{{ k }}</span>
  17. <span class="summary-val">{{ v }}</span>
  18. </div>
  19. </div>
  20. </div>
  21. <!-- 操作按钮(等待确认) -->
  22. <div v-if="command.status === 'AWAITING_CONFIRMATION' || command.status === 'PREPARED'" class="flex gap-2 items-center">
  23. <el-button type="primary" size="large" :loading="confirming" @click="handleConfirm">
  24. 确认挂号
  25. </el-button>
  26. <el-button size="large" :loading="rejecting" @click="handleReject">
  27. 取消
  28. </el-button>
  29. </div>
  30. <!-- 执行中 -->
  31. <div v-if="command.status === 'EXECUTING'" class="flex items-center gap-2 text-gray-500">
  32. <el-icon class="is-loading"><i class="el-icon-loading" /></el-icon>
  33. <span>正在执行挂号,请稍候…</span>
  34. </div>
  35. <!-- 成功 -->
  36. <el-result
  37. v-if="command.status === 'SUCCEEDED'"
  38. icon="success"
  39. title="挂号成功"
  40. :sub-title="successSubtitle"
  41. >
  42. <template #extra>
  43. <div v-if="command.result && Object.keys(command.result).length" class="mt-2">
  44. <div class="text-sm text-gray-500 mb-1">预约详情</div>
  45. <pre class="text-xs bg-gray-50 p-2 rounded">{{ JSON.stringify(command.result, null, 2) }}</pre>
  46. </div>
  47. </template>
  48. </el-result>
  49. <!-- 失败 -->
  50. <el-result
  51. v-if="command.status === 'FAILED'"
  52. icon="error"
  53. title="挂号失败"
  54. :sub-title="command.error?.message ?? '请稍后重试'"
  55. >
  56. <template #extra>
  57. <el-button type="primary" @click="$emit('retry')">重试</el-button>
  58. <el-button @click="$emit('cancel')">返回重选</el-button>
  59. </template>
  60. </el-result>
  61. <!-- 已拒绝 -->
  62. <el-result
  63. v-if="command.status === 'REJECTED'"
  64. icon="info"
  65. title="已取消挂号"
  66. sub-title="您可以重新选择号源"
  67. >
  68. <template #extra>
  69. <el-button type="primary" @click="$emit('cancel')">重新选择</el-button>
  70. </template>
  71. </el-result>
  72. <!-- 已过期 -->
  73. <el-result
  74. v-if="command.status === 'EXPIRED'"
  75. icon="warning"
  76. title="挂号已过期"
  77. sub-title="号源可能已被释放,请重新查询"
  78. >
  79. <template #extra>
  80. <el-button type="primary" @click="$emit('revaluate')">重新查询</el-button>
  81. </template>
  82. </el-result>
  83. <!-- UNKNOWN 轮询中 -->
  84. <el-alert
  85. v-if="command.status === 'UNKNOWN'"
  86. title="正在查询挂号结果…"
  87. type="warning"
  88. :closable="false"
  89. show-icon
  90. />
  91. <!-- 操作反馈 -->
  92. <div v-if="feedbackMessage" :class="['mt-2 text-sm', feedbackOk ? 'text-green-600' : 'text-red-500']">
  93. {{ feedbackMessage }}
  94. </div>
  95. </el-card>
  96. </template>
  97. <script setup lang="ts">
  98. import { computed, ref, watch, onUnmounted } from 'vue';
  99. import { confirmCommand, rejectCommand, getCommand } from '@/api/v2';
  100. import type { Command } from '@/api/v2/types';
  101. const props = defineProps<{
  102. command: Command;
  103. }>();
  104. const emit = defineEmits<{
  105. update: [command: Command];
  106. retry: [];
  107. cancel: [];
  108. revaluate: [];
  109. }>();
  110. const confirming = ref(false);
  111. const rejecting = ref(false);
  112. const feedbackMessage = ref('');
  113. const feedbackOk = ref(false);
  114. const confirmed = computed(() => props.command.status === 'SUCCEEDED');
  115. const finalized = computed(() =>
  116. ['SUCCEEDED', 'FAILED', 'REJECTED', 'EXPIRED'].includes(props.command.status));
  117. const successSubtitle = computed(() => {
  118. const r = props.command.result;
  119. if (!r) return '';
  120. const id = r['appointmentId'] ?? r['appointment_id'] ?? '';
  121. return id ? `预约编号:${id}` : '请按时就诊';
  122. });
  123. // ---- 轮询(UNKNOWN 状态) ----
  124. let pollTimer: ReturnType<typeof setInterval> | null = null;
  125. watch(() => props.command.status, (status) => {
  126. if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
  127. if (status === 'UNKNOWN' && props.command) {
  128. pollTimer = setInterval(async () => {
  129. try {
  130. const res = await getCommand(props.command!.commandId);
  131. const updated = (res as any).data?.data ?? (res as any).data ?? res;
  132. if (updated) {
  133. emit('update', updated as Command);
  134. if (updated.status !== 'UNKNOWN' && pollTimer) {
  135. clearInterval(pollTimer);
  136. pollTimer = null;
  137. }
  138. }
  139. } catch { /* silent */ }
  140. }, 3000);
  141. }
  142. });
  143. onUnmounted(() => { if (pollTimer) clearInterval(pollTimer); });
  144. // ---- 操作 ----
  145. async function handleConfirm() {
  146. confirming.value = true;
  147. feedbackMessage.value = '';
  148. try {
  149. const res = await confirmCommand(props.command.commandId, {
  150. commandVersion: props.command.commandVersion,
  151. confirmation: { channel: 'CARD', explicit: true },
  152. });
  153. const data = (res as any).data?.data ?? (res as any).data ?? res;
  154. if (data) {
  155. emit('update', data as Command);
  156. feedbackMessage.value = '挂号成功';
  157. feedbackOk.value = true;
  158. }
  159. } catch (e: any) {
  160. feedbackMessage.value = parseError(e);
  161. feedbackOk.value = false;
  162. } finally {
  163. confirming.value = false;
  164. }
  165. }
  166. async function handleReject() {
  167. rejecting.value = true;
  168. feedbackMessage.value = '';
  169. try {
  170. const res = await rejectCommand(props.command.commandId, {
  171. commandVersion: props.command.commandVersion,
  172. reason: '用户取消挂号',
  173. });
  174. const data = (res as any).data?.data ?? (res as any).data ?? res;
  175. if (data) {
  176. emit('update', data as Command);
  177. feedbackMessage.value = '已取消';
  178. feedbackOk.value = true;
  179. }
  180. } catch (e: any) {
  181. feedbackMessage.value = parseError(e);
  182. feedbackOk.value = false;
  183. } finally {
  184. rejecting.value = false;
  185. }
  186. }
  187. function parseError(e: any): string {
  188. return e?.response?.data?.message ?? e?.message ?? '操作失败';
  189. }
  190. // ---- summary ----
  191. const labelMap: Record<string, string> = {
  192. department: '科室', doctor: '医生', visitTime: '就诊时间',
  193. feeYuan: '费用', note: '备注', lockId: '锁号编号',
  194. patientName: '患者',
  195. };
  196. const summaryEntries = computed<[string, string][]>(() => {
  197. if (!props.command?.summary) return [];
  198. return Object.entries(props.command.summary)
  199. .filter(([k]) => k !== 'lockExpiresAt')
  200. .map(([k, v]) => [labelMap[k] ?? k, String(v)]);
  201. });
  202. </script>
  203. <style scoped>
  204. .registration-confirm-card {
  205. border-left: 3px solid #3b82f6;
  206. }
  207. .summary-grid {
  208. display: grid;
  209. grid-template-columns: 1fr 1fr;
  210. gap: 8px;
  211. padding: 12px;
  212. background: #f0f9ff;
  213. border-radius: 6px;
  214. }
  215. .summary-item {
  216. display: flex;
  217. flex-direction: column;
  218. gap: 2px;
  219. }
  220. .summary-key { font-size: 12px; color: #6b7280; }
  221. .summary-val { font-size: 15px; color: #1f2937; font-weight: 500; }
  222. .flex { display: flex; }
  223. .items-center { align-items: center; }
  224. .justify-between { justify-content: space-between; }
  225. .gap-2 { gap: 8px; }
  226. .mb-1 { margin-bottom: 4px; }
  227. .mb-2 { margin-bottom: 8px; }
  228. .mb-3 { margin-bottom: 12px; }
  229. .mt-2 { margin-top: 8px; }
  230. .text-xs { font-size: 12px; }
  231. .text-sm { font-size: 13px; }
  232. .text-base { font-size: 15px; }
  233. .font-semibold { font-weight: 600; }
  234. .text-gray-500 { color: #6b7280; }
  235. .text-green-600 { color: #16a34a; }
  236. .text-red-500 { color: #ef4444; }
  237. .bg-gray-50 { background: #f9fafb; }
  238. .p-2 { padding: 8px; }
  239. .rounded { border-radius: 4px; }
  240. </style>