Sfoglia il codice sorgente

feat: 补充消息发送状态反馈

samien 2 mesi fa
parent
commit
5649d5ead8

+ 2 - 2
src/client/components/composer/ComposerShell.vue

@@ -498,7 +498,7 @@ defineExpose({
           <button v-if="mode === 'chat' && streaming && canSubmit" class="queue-toggle" :class="{ active: queueMode }" type="button" :title="queueMode ? '消息将排队发送' : '消息将 Steer 当前会话'" @click="emit('queueToggle')">
             {{ queueMode ? '排队' : 'Steer' }}
           </button>
-          <span v-if="sending" class="composer-sending" aria-label="正在发送" />
+          <span v-if="sending" class="composer-sending" role="status" aria-live="polite"><i />正在发送</span>
           <span class="composer-send-hitbox">
             <button
               class="composer-send"
@@ -598,7 +598,7 @@ defineExpose({
 .composer-send:not(:disabled):hover { transform: translateY(-1px); box-shadow: 0 7px 16px rgba(41,36,39,.22); }
 .composer-send:disabled { cursor: not-allowed; background: #9f9f9f; color: #fff; }
 .composer-send--stop { background: var(--accent); }
-.composer-sending { width: 5px; height: 5px; border-radius: 50%; background: var(--warning); animation: composer-pulse 1s ease-in-out infinite; }
+.composer-sending { display: inline-flex; align-items: center; gap: 5px; color: var(--text-muted); font-size: 9px; white-space: nowrap; }.composer-sending i { width: 5px; height: 5px; border-radius: 50%; background: var(--warning); animation: composer-pulse 1s ease-in-out infinite; }
 @keyframes composer-pulse { 50% { opacity: .25; transform: scale(.7); } }
 .composer-attachments { display: flex; width: 100%; max-width: 760px; margin: 0 auto 9px; flex-wrap: wrap; gap: 7px; }
 .composer-attachment { position: relative; display: flex; max-width: 210px; min-height: 48px; align-items: center; gap: 8px; padding: 6px 28px 6px 9px; overflow: hidden; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-raised); color: var(--text-secondary); }

+ 23 - 0
src/client/components/messages/MessageTimeline.vue

@@ -158,6 +158,17 @@ function hasConversationActions(message: UiMessage): boolean {
   return message.role === 'user' || (message.role === 'assistant' && Boolean(message.content.trim()))
 }
 
+function deliveryLabel(status?: UiMessage['status']): string {
+  return ({
+    preparing: '正在准备附件',
+    attached: '附件已就绪',
+    pending: '正在发送',
+    accepted: '已发送',
+    failed: '发送失败',
+    'unknown-receipt': '回执未知,请检查后重试',
+  } as Partial<Record<NonNullable<UiMessage['status']>, string>>)[status || 'settled'] || ''
+}
+
 watch(() => props.messages.length, async () => {
   if (!pinnedToBottom.value) return
   await nextTick()
@@ -271,6 +282,17 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
               />
             </div>
 
+            <div
+              v-if="message.role === 'user' && deliveryLabel(message.status)"
+              class="message__delivery"
+              :class="`message__delivery--${message.status}`"
+              role="status"
+              aria-live="polite"
+            >
+              <AppIcon :name="message.status === 'accepted' ? 'check' : message.status === 'failed' || message.status === 'unknown-receipt' ? 'alert' : 'dots'" :size="11" />
+              {{ deliveryLabel(message.status) }}
+            </div>
+
             <div v-if="message.role !== 'user' && message.attachments?.length" class="message__attachments">
               <button v-for="attachment in message.attachments" :key="attachment.id" type="button" @click="emit('preview', attachment)">
                 <img v-if="attachment.kind === 'image' && attachment.url" :src="attachment.url" :alt="attachment.name" />
@@ -340,6 +362,7 @@ defineExpose({ scrollToMessage, scrollToAnchor, scrollToBottom })
 .message--user .message__body { max-width: min(610px, 76%); padding: 11px 15px; border-radius: 20px 20px 6px 20px; background: #eceff3; }
 .dark .message--user .message__body { background: #2d323a; }
 .message--user .message__meta { display: none; }
+.message__delivery { display: flex; min-height: 18px; align-items: center; justify-content: flex-end; gap: 4px; margin: 5px 1px -4px; color: var(--text-muted); font-size: 9px; }.message__delivery--accepted { color: var(--success); }.message__delivery--failed, .message__delivery--unknown-receipt { color: var(--danger); }
 .message--system { justify-content: center; }.message--system .message__avatar, .message--system .message__meta { display: none; }.message--system .message__body { max-width: 82%; padding: 6px 10px; border: 1px solid var(--line); border-radius: 9px; color: var(--text-muted); text-align: center; }
 .delegation-event { min-width: 230px; max-width: 460px; text-align: left; }.delegation-event summary { display: flex; align-items: center; gap: 8px; cursor: pointer; list-style: none; }.delegation-event summary::-webkit-details-marker { display: none; }.delegation-event summary > span { display: flex; min-width: 0; flex-direction: column; gap: 2px; }.delegation-event strong { color: var(--text-secondary); font-size: 11px; font-weight: 650; }.delegation-event small { color: var(--text-muted); font-size: 9px; }.delegation-event :deep(.markdown) { max-height: 260px; margin-top: 9px; overflow: auto; color: var(--text-secondary); font-size: 10px; }
 .system-event { max-width: 460px; text-align: left; }.system-event summary { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; list-style: none; color: var(--text-muted); font-size: 10px; }.system-event summary::-webkit-details-marker { display: none; }.system-event :deep(.markdown) { max-height: 220px; margin-top: 8px; overflow: auto; color: var(--text-secondary); font-size: 10px; }