diff --git a/yunkes/src/views/ChatTimeline.vue b/yunkes/src/views/ChatTimeline.vue index 3e160ee..94b1706 100644 --- a/yunkes/src/views/ChatTimeline.vue +++ b/yunkes/src/views/ChatTimeline.vue @@ -57,6 +57,16 @@ const hasActive = computed(() => !!activeId.value) const hasMoreOlder = computed(() => currentPage.value > 1) +/** + * 关键词过滤:仅作用于"当前已加载"的消息(/api/messages 无 q 参数,非全量历史)。 + * keyword 为空 → 直接返回 messages.value。 + */ +const visibleMessages = computed(() => { + const k = keyword.value.trim() + if (!k) return messages.value + return messages.value.filter((m) => (m.text ?? '').includes(k)) +}) + const conversationTitle = computed(() => { if (isGroup.value) { return `群聊记录 · ${props.roomId}` @@ -68,23 +78,41 @@ const conversationTitle = computed(() => { return '聊天记录' }) -/** 从路由 query 读日期窗(从 StaffList stat modal 带窗跳来时有值);缺失=全量(向后兼容)。 */ -const windowStart = computed(() => { - const s = Array.isArray(route.query.start) ? route.query.start[0] : route.query.start - return s != null && /^\d+$/.test(String(s)) ? Number(s) : undefined -}) -const windowEnd = computed(() => { - const e = Array.isArray(route.query.end) ? route.query.end[0] : route.query.end - return e != null && /^\d+$/.test(String(e)) ? Number(e) : undefined -}) +const TWO_WEEKS_MS = 14 * 86_400_000 + +function defaultRange(): [Date, Date] { + const end = new Date() + end.setHours(23, 59, 59, 999) + const start = new Date(end.getTime() - TWO_WEEKS_MS) + start.setHours(0, 0, 0, 0) + return [start, end] +} +function rangeFromQuery(): [Date, Date] | null { + const s = route.query.start, + e = route.query.end + const sv = Array.isArray(s) ? s[0] : s, + ev = Array.isArray(e) ? e[0] : e + if (sv && ev && /^\d+$/.test(String(sv)) && /^\d+$/.test(String(ev))) { + return [new Date(Number(sv)), new Date(Number(ev))] + } + return null +} +// 本地日期窗 ref:mount 时取 route.query(stat modal 带窗入口)否则默认近 2 周; +// 跨会话切换持久(不随路由 param 变化重置),与云客真机一致。 +const dateRange = ref<[Date, Date]>(rangeFromQuery() ?? defaultRange()) +const winStart = computed(() => dateRange.value[0].getTime()) +const winEnd = computed(() => dateRange.value[1].getTime()) +const keyword = ref('') + +function onDateChange(): void { + loadInitial() +} const subtitle = computed(() => { let s = `渠道:${channelLabel(channelTyped.value)} · 归属账号:${props.ownerId}` + (total.value > 0 ? ` · 共 ${total.value} 条` : '') - if (windowStart.value && windowEnd.value) { - s += ` · 时段:${formatTime(windowStart.value)} ~ ${formatTime(windowEnd.value)}` - } + s += ` · 时段:${formatTime(winStart.value)} ~ ${formatTime(winEnd.value)}` return s }) @@ -99,15 +127,13 @@ function scrollToBottom(): void { /** Shared query params for both DM and group timelines. */ function queryFor(pageNum: number) { - // 日期窗为 both-or-neither:仅当 start/end 同时存在才透传,避免单边过滤 - // (getMessages→clean() 会逐键剔除 undefined,单边 start 会变成下界过滤)。 - const hasWindow = windowStart.value != null && windowEnd.value != null return { channel: channelTyped.value, owner_account_id: props.ownerId, talker_id: isGroup.value ? undefined : props.talkerId, room_id: isGroup.value ? props.roomId : undefined, - ...(hasWindow ? { start: windowStart.value, end: windowEnd.value } : {}), + start: winStart.value, + end: winEnd.value, page: pageNum, page_size: PAGE_SIZE, } @@ -223,9 +249,9 @@ const drawerTitle = computed(() => { }) // Reload when the route params change (navigating between conversations). +// dateRange 变化走 onDateChange,不在此 watcher 内。 watch( - () => - [props.channel, props.ownerId, props.talkerId, props.roomId, windowStart.value, windowEnd.value] as const, + () => [props.channel, props.ownerId, props.talkerId, props.roomId] as const, () => { loadInitial() }, @@ -256,7 +282,22 @@ onMounted(loadInitial)