Răsfoiți Sursa

fix: 兼容消息复制与相对日期时间

samien 2 luni în urmă
părinte
comite
0cf6275756

+ 18 - 7
src/client/components/messages/MessageTimeline.vue

@@ -9,6 +9,7 @@ import TurnTrace from './TurnTrace.vue'
 import type { UiInteraction, UiLocalFileLink, UiMessage } from './types'
 import { buildMessageTimelineRows } from '@/utils/turnTrace'
 import { displayContentForMessage } from '@/utils/messageDisplay'
+import { formatMessageTime } from '@/utils/messageTime'
 
 const props = withDefaults(defineProps<{
   messages: UiMessage[]
@@ -62,12 +63,7 @@ const pinnedToBottom = ref(true)
 const showJump = ref(false)
 const timelineRows = computed(() => buildMessageTimelineRows(props.messages))
 
-function formatTime(value?: string | number | Date) {
-  if (!value) return ''
-  const date = value instanceof Date ? value : new Date(value)
-  if (Number.isNaN(date.getTime())) return ''
-  return new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit' }).format(date)
-}
+const formatTime = formatMessageTime
 
 function delegationSummary(metadata?: Record<string, unknown>): string {
   const total = Number(metadata?.task_count ?? 0)
@@ -129,7 +125,22 @@ function scrollToAnchor(messageId: string, anchorId: string): boolean {
 }
 
 async function copyMessage(message: UiMessage) {
-  await navigator.clipboard.writeText(displayContentForMessage(message.role, message.content))
+  const text = displayContentForMessage(message.role, message.content)
+  try {
+    if (navigator.clipboard?.writeText && window.isSecureContext) {
+      await navigator.clipboard.writeText(text)
+      return
+    }
+  } catch { /* Fall through to the HTTP-compatible browser fallback. */ }
+  const textarea = document.createElement('textarea')
+  textarea.value = text
+  textarea.setAttribute('readonly', '')
+  textarea.style.cssText = 'position:fixed;left:-9999px;top:0;opacity:0'
+  document.body.appendChild(textarea)
+  textarea.select()
+  const copied = document.execCommand('copy')
+  textarea.remove()
+  if (!copied) throw new Error('浏览器拒绝访问剪贴板')
 }
 
 function hasConversationActions(message: UiMessage): boolean {

+ 18 - 0
src/client/utils/messageTime.ts

@@ -0,0 +1,18 @@
+function calendarStart(value: Date): number {
+  return new Date(value.getFullYear(), value.getMonth(), value.getDate()).getTime()
+}
+
+export function formatMessageTime(value: string | number | Date | undefined, now = new Date()): string {
+  if (!value) return ''
+  const date = value instanceof Date ? value : new Date(value)
+  if (Number.isNaN(date.getTime())) return ''
+  const time = new Intl.DateTimeFormat('zh-CN', { hour: '2-digit', minute: '2-digit', hour12: false }).format(date)
+  const days = Math.round((calendarStart(now) - calendarStart(date)) / 86_400_000)
+  if (days === 0) return time
+  if (days === 1) return `昨天 ${time}`
+  if (days >= 2 && days < 7) {
+    return `周${'日一二三四五六'[date.getDay()]} ${time}`
+  }
+  const dateLabel = `${date.getMonth() + 1}月${date.getDate()}日`
+  return date.getFullYear() === now.getFullYear() ? `${dateLabel} ${time}` : `${date.getFullYear()}年${dateLabel} ${time}`
+}

+ 14 - 0
tests/client/messageTime.test.ts

@@ -0,0 +1,14 @@
+import { describe, expect, it } from 'vitest'
+import { formatMessageTime } from '@/utils/messageTime'
+
+describe('message timestamps', () => {
+  const now = new Date('2026-08-18T12:00:00+08:00')
+
+  it('uses time, yesterday, weekday, and a concrete date by recency', () => {
+    expect(formatMessageTime('2026-08-18T09:05:00+08:00', now)).toBe('09:05')
+    expect(formatMessageTime('2026-08-17T09:05:00+08:00', now)).toBe('昨天 09:05')
+    expect(formatMessageTime('2026-08-15T09:05:00+08:00', now)).toBe('周六 09:05')
+    expect(formatMessageTime('2026-08-09T09:05:00+08:00', now)).toBe('8月9日 09:05')
+    expect(formatMessageTime('2025-08-09T09:05:00+08:00', now)).toBe('2025年8月9日 09:05')
+  })
+})