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)
|
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(() => {
|
const conversationTitle = computed(() => {
|
||||||
if (isGroup.value) {
|
if (isGroup.value) {
|
||||||
return `群聊记录 · ${props.roomId}`
|
return `群聊记录 · ${props.roomId}`
|
||||||
@@ -68,23 +78,41 @@ const conversationTitle = computed(() => {
|
|||||||
return '聊天记录'
|
return '聊天记录'
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 从路由 query 读日期窗(从 StaffList stat modal 带窗跳来时有值);缺失=全量(向后兼容)。 */
|
const TWO_WEEKS_MS = 14 * 86_400_000
|
||||||
const windowStart = computed(() => {
|
|
||||||
const s = Array.isArray(route.query.start) ? route.query.start[0] : route.query.start
|
function defaultRange(): [Date, Date] {
|
||||||
return s != null && /^\d+$/.test(String(s)) ? Number(s) : undefined
|
const end = new Date()
|
||||||
})
|
end.setHours(23, 59, 59, 999)
|
||||||
const windowEnd = computed(() => {
|
const start = new Date(end.getTime() - TWO_WEEKS_MS)
|
||||||
const e = Array.isArray(route.query.end) ? route.query.end[0] : route.query.end
|
start.setHours(0, 0, 0, 0)
|
||||||
return e != null && /^\d+$/.test(String(e)) ? Number(e) : undefined
|
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(() => {
|
const subtitle = computed(() => {
|
||||||
let s =
|
let s =
|
||||||
`渠道:${channelLabel(channelTyped.value)} · 归属账号:${props.ownerId}` +
|
`渠道:${channelLabel(channelTyped.value)} · 归属账号:${props.ownerId}` +
|
||||||
(total.value > 0 ? ` · 共 ${total.value} 条` : '')
|
(total.value > 0 ? ` · 共 ${total.value} 条` : '')
|
||||||
if (windowStart.value && windowEnd.value) {
|
s += ` · 时段:${formatTime(winStart.value)} ~ ${formatTime(winEnd.value)}`
|
||||||
s += ` · 时段:${formatTime(windowStart.value)} ~ ${formatTime(windowEnd.value)}`
|
|
||||||
}
|
|
||||||
return s
|
return s
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -99,15 +127,13 @@ function scrollToBottom(): void {
|
|||||||
|
|
||||||
/** Shared query params for both DM and group timelines. */
|
/** Shared query params for both DM and group timelines. */
|
||||||
function queryFor(pageNum: number) {
|
function queryFor(pageNum: number) {
|
||||||
// 日期窗为 both-or-neither:仅当 start/end 同时存在才透传,避免单边过滤
|
|
||||||
// (getMessages→clean() 会逐键剔除 undefined,单边 start 会变成下界过滤)。
|
|
||||||
const hasWindow = windowStart.value != null && windowEnd.value != null
|
|
||||||
return {
|
return {
|
||||||
channel: channelTyped.value,
|
channel: channelTyped.value,
|
||||||
owner_account_id: props.ownerId,
|
owner_account_id: props.ownerId,
|
||||||
talker_id: isGroup.value ? undefined : props.talkerId,
|
talker_id: isGroup.value ? undefined : props.talkerId,
|
||||||
room_id: isGroup.value ? props.roomId : undefined,
|
room_id: isGroup.value ? props.roomId : undefined,
|
||||||
...(hasWindow ? { start: windowStart.value, end: windowEnd.value } : {}),
|
start: winStart.value,
|
||||||
|
end: winEnd.value,
|
||||||
page: pageNum,
|
page: pageNum,
|
||||||
page_size: PAGE_SIZE,
|
page_size: PAGE_SIZE,
|
||||||
}
|
}
|
||||||
@@ -223,9 +249,9 @@ const drawerTitle = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Reload when the route params change (navigating between conversations).
|
// Reload when the route params change (navigating between conversations).
|
||||||
|
// dateRange 变化走 onDateChange,不在此 watcher 内。
|
||||||
watch(
|
watch(
|
||||||
() =>
|
() => [props.channel, props.ownerId, props.talkerId, props.roomId] as const,
|
||||||
[props.channel, props.ownerId, props.talkerId, props.roomId, windowStart.value, windowEnd.value] as const,
|
|
||||||
() => {
|
() => {
|
||||||
loadInitial()
|
loadInitial()
|
||||||
},
|
},
|
||||||
@@ -256,7 +282,22 @@ onMounted(loadInitial)
|
|||||||
</div>
|
</div>
|
||||||
<div class="yk-chat__sub">{{ subtitle }}</div>
|
<div class="yk-chat__sub">{{ subtitle }}</div>
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<el-alert
|
<el-alert
|
||||||
@@ -288,9 +329,12 @@ onMounted(loadInitial)
|
|||||||
<template v-if="messages.length === 0 && !loading">
|
<template v-if="messages.length === 0 && !loading">
|
||||||
<div class="yk-empty">暂无聊天记录</div>
|
<div class="yk-empty">暂无聊天记录</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else-if="visibleMessages.length === 0 && keyword && !loading">
|
||||||
|
<div class="yk-empty">当前已加载消息中无匹配</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<MessageBubble
|
<MessageBubble
|
||||||
v-for="m in messages"
|
v-for="m in visibleMessages"
|
||||||
:key="m.msg_id"
|
:key="m.msg_id"
|
||||||
:message="m"
|
:message="m"
|
||||||
@open-detail="openDetail"
|
@open-detail="openDetail"
|
||||||
@@ -386,6 +430,12 @@ onMounted(loadInitial)
|
|||||||
justify-content: center;
|
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
|
/* Make the stream its own scroll region so scrollToBottom + load-older scroll
|
||||||
* anchoring target the right element. Height reserves space for the app
|
* anchoring target the right element. Height reserves space for the app
|
||||||
* header + chat header + main padding. */
|
* header + chat header + main padding. */
|
||||||
|
|||||||
Reference in New Issue
Block a user