ContextPanel.vue 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. <template>
  2. <aside class="panel-right" aria-label="上下文面板">
  3. <!-- Idle: doctor image -->
  4. <DoctorAssistantFigure v-if="store.contextPanelMode === 'idle'" />
  5. <!-- Loading: skeleton timeline -->
  6. <template v-else-if="store.contextPanelMode === 'loading'">
  7. <div class="pr-header">
  8. <div class="pr-label">分析中</div>
  9. </div>
  10. <div class="timeline-skeleton">
  11. <div class="sk-item" v-for="i in 6" :key="i">
  12. <div class="sk-dot" />
  13. <div class="sk-line" />
  14. </div>
  15. </div>
  16. </template>
  17. <!-- Task / Completed: timeline + card -->
  18. <template v-else-if="store.contextPanelMode === 'task' || store.contextPanelMode === 'completed'">
  19. <div class="pr-header">
  20. <div class="pr-label">
  21. {{ store.displayedCard?.cardKey === 'appointment-success' ? '挂号结果' : '挂号进度' }}
  22. </div>
  23. </div>
  24. <RegistrationProgress />
  25. <div class="panel-card-container">
  26. <CardRenderer
  27. v-if="store.displayedCard"
  28. :card="store.displayedCard"
  29. @action="handleAction"
  30. @finish="store.resetToIdle"
  31. />
  32. </div>
  33. </template>
  34. <!-- Error -->
  35. <template v-else>
  36. <div class="pr-header">
  37. <div class="pr-label">异常</div>
  38. </div>
  39. <ErrorCard
  40. :message="store.errorMessage"
  41. :trace-id="store.currentTraceId"
  42. @restart="store.resetToIdle"
  43. />
  44. </template>
  45. </aside>
  46. </template>
  47. <script setup lang="ts">
  48. import CardRenderer from '../cards/CardRenderer.vue';
  49. import ErrorCard from '../cards/ErrorCard.vue';
  50. import { useTerminalStore } from '../state/terminalStore';
  51. import DoctorAssistantFigure from './DoctorAssistantFigure.vue';
  52. import RegistrationProgress from './RegistrationProgress.vue';
  53. const store = useTerminalStore();
  54. async function handleAction(data: { actionName: string; payload: Record<string, unknown> }) {
  55. store.saveSelection(data.payload);
  56. try {
  57. await store.submitCard(data.payload);
  58. } catch {
  59. // error already set in store
  60. }
  61. }
  62. </script>
  63. <style scoped>
  64. .panel-right {
  65. background: var(--surface);
  66. border-left: 1px solid var(--border);
  67. padding: 24px 20px;
  68. overflow-y: auto;
  69. display: flex;
  70. flex-direction: column;
  71. }
  72. .pr-header {
  73. margin-bottom: 20px;
  74. }
  75. .pr-label {
  76. font-size: var(--fs-xs);
  77. color: var(--muted);
  78. text-transform: uppercase;
  79. letter-spacing: 0.06em;
  80. font-weight: 500;
  81. }
  82. .panel-card-container {
  83. flex: 1;
  84. overflow-y: auto;
  85. }
  86. /* ─── Loading skeleton timeline ─── */
  87. .timeline-skeleton {
  88. position: relative;
  89. padding-left: 28px;
  90. }
  91. .timeline-skeleton::before {
  92. content: '';
  93. position: absolute;
  94. left: 10px;
  95. top: 8px;
  96. bottom: 8px;
  97. width: 0;
  98. border-left: 2px dotted var(--border);
  99. }
  100. .sk-item {
  101. position: relative;
  102. padding: 8px 0 8px 14px;
  103. display: flex;
  104. align-items: center;
  105. gap: 8px;
  106. }
  107. .sk-dot {
  108. position: absolute;
  109. left: -22px;
  110. top: 12px;
  111. width: 10px;
  112. height: 10px;
  113. border-radius: 50%;
  114. background: var(--border);
  115. border: 2px solid var(--surface);
  116. z-index: 1;
  117. }
  118. .sk-line {
  119. height: 12px;
  120. border-radius: 4px;
  121. background: linear-gradient(90deg, var(--border) 25%, var(--bg) 50%, var(--border) 75%);
  122. background-size: 200% 100%;
  123. animation: shimmer 1.5s ease-in-out infinite;
  124. width: 100%;
  125. }
  126. @keyframes shimmer {
  127. 0% { background-position: 200% 0; }
  128. 100% { background-position: -200% 0; }
  129. }
  130. </style>