feat(chat): 页内日期选择器(跨会话持久)+ 关键词搜索(已加载页过滤)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
</div>
|
||||
<div class="yk-chat__sub">{{ subtitle }}</div>
|
||||
</div>
|
||||
<!-- Task 4 在此插日期选择器 + 关键词搜索;本 task 留空 -->
|
||||
<div class="yk-chat__filters">
|
||||
<el-date-picker
|
||||
v-model="dateRange"
|
||||
type="daterange"
|
||||
size="small"
|
||||
:clearable="false"
|
||||
@change="onDateChange"
|
||||
/>
|
||||
<el-input
|
||||
v-model="keyword"
|
||||
size="small"
|
||||
placeholder="搜索当前已加载消息"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-alert
|
||||
@@ -288,9 +329,12 @@ onMounted(loadInitial)
|
||||
<template v-if="messages.length === 0 && !loading">
|
||||
<div class="yk-empty">暂无聊天记录</div>
|
||||
</template>
|
||||
<template v-else-if="visibleMessages.length === 0 && keyword && !loading">
|
||||
<div class="yk-empty">当前已加载消息中无匹配</div>
|
||||
</template>
|
||||
|
||||
<MessageBubble
|
||||
v-for="m in messages"
|
||||
v-for="m in visibleMessages"
|
||||
:key="m.msg_id"
|
||||
:message="m"
|
||||
@open-detail="openDetail"
|
||||
@@ -386,6 +430,12 @@ onMounted(loadInitial)
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.yk-chat__filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Make the stream its own scroll region so scrollToBottom + load-older scroll
|
||||
* anchoring target the right element. Height reserves space for the app
|
||||
* header + chat header + main padding. */
|
||||
|
||||
Reference in New Issue
Block a user