feat(chat): getSessions + ChatSidebar 会话列表组件
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ import type {
|
|||||||
Message,
|
Message,
|
||||||
Page,
|
Page,
|
||||||
PhoneCall,
|
PhoneCall,
|
||||||
|
SessionList,
|
||||||
Sms,
|
Sms,
|
||||||
Staff,
|
Staff,
|
||||||
StaffChatStats,
|
StaffChatStats,
|
||||||
@@ -238,6 +239,22 @@ export function getStaffChatStats(query: StaffChatStatsQuery): Promise<StaffChat
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Chat sessions (账号会话列表:好友+群,今日消息数+最近联系时间)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export interface SessionQuery {
|
||||||
|
owner_account_id: string
|
||||||
|
channel?: Channel
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSessions(query: SessionQuery): Promise<SessionList> {
|
||||||
|
return getJson<SessionList>(
|
||||||
|
'/sessions',
|
||||||
|
clean({ owner_account_id: query.owner_account_id, channel: query.channel }),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Sync monitor
|
// Sync monitor
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -300,6 +317,7 @@ export type {
|
|||||||
Message,
|
Message,
|
||||||
Page,
|
Page,
|
||||||
PhoneCall,
|
PhoneCall,
|
||||||
|
SessionList,
|
||||||
Sms,
|
Sms,
|
||||||
Staff,
|
Staff,
|
||||||
StaffChatStats,
|
StaffChatStats,
|
||||||
|
|||||||
@@ -277,3 +277,21 @@ export interface StaffChatStats {
|
|||||||
summary: StaffChatStatsSummary
|
summary: StaffChatStatsSummary
|
||||||
customers: StaffChatCustomer[]
|
customers: StaffChatCustomer[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 账号会话列表一行(好友或群),镜像 backend SessionOut。 */
|
||||||
|
export interface ChatSession {
|
||||||
|
type: 'friend' | 'group'
|
||||||
|
talker_id?: string | null
|
||||||
|
room_id?: string | null
|
||||||
|
name?: string | null
|
||||||
|
avatar?: string | null
|
||||||
|
today_count: number
|
||||||
|
last_chat_time?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SessionList {
|
||||||
|
owner_account_id: string
|
||||||
|
channel: string
|
||||||
|
today_start: number
|
||||||
|
sessions: ChatSession[]
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { getSessions } from '@/api'
|
||||||
|
import type { ChatSession, Channel } from '@/api/types'
|
||||||
|
import { formatTime } from '@/utils/format'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
channel: Channel
|
||||||
|
ownerId: string
|
||||||
|
activeType: 'friend' | 'group' | null
|
||||||
|
activeId: string | null
|
||||||
|
}>()
|
||||||
|
const emit = defineEmits<{ select: [ChatSession] }>()
|
||||||
|
|
||||||
|
const sessions = ref<ChatSession[]>([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const errorMsg = ref<string | null>(null)
|
||||||
|
const filter = ref<'all' | 'friend' | 'group'>('all')
|
||||||
|
const keyword = ref('')
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
loading.value = true
|
||||||
|
errorMsg.value = null
|
||||||
|
try {
|
||||||
|
const res = await getSessions({ owner_account_id: props.ownerId, channel: props.channel })
|
||||||
|
sessions.value = res.sessions
|
||||||
|
} catch (e) {
|
||||||
|
errorMsg.value = (e as Error).message
|
||||||
|
sessions.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = computed(() =>
|
||||||
|
sessions.value
|
||||||
|
.filter((s) => (filter.value === 'all' ? true : s.type === filter.value))
|
||||||
|
.filter((s) => {
|
||||||
|
const k = keyword.value.trim()
|
||||||
|
if (!k) return true
|
||||||
|
return (s.name ?? '').includes(k) || (s.talker_id ?? '').includes(k) || (s.room_id ?? '').includes(k)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
function isActive(s: ChatSession): boolean {
|
||||||
|
return props.activeType === s.type && props.activeId === (s.talker_id ?? s.room_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => [props.ownerId, props.channel] as const, load, { immediate: true })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="yk-chat-side">
|
||||||
|
<div class="yk-chat-side__top">
|
||||||
|
<el-select v-model="filter" size="small" style="width: 96px">
|
||||||
|
<el-option label="全部" value="all" />
|
||||||
|
<el-option label="好友" value="friend" />
|
||||||
|
<el-option label="群聊" value="group" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="keyword" size="small" placeholder="搜索昵称/号" clearable />
|
||||||
|
</div>
|
||||||
|
<el-alert v-if="errorMsg" :title="errorMsg" type="error" :closable="false" />
|
||||||
|
<div v-loading="loading" class="yk-chat-side__list">
|
||||||
|
<div v-if="!loading && filtered.length === 0" class="yk-empty">暂无会话</div>
|
||||||
|
<div
|
||||||
|
v-for="s in filtered"
|
||||||
|
:key="(s.talker_id ?? s.room_id) + s.type"
|
||||||
|
class="yk-chat-side__item"
|
||||||
|
:class="{ 'is-active': isActive(s) }"
|
||||||
|
@click="emit('select', s)"
|
||||||
|
>
|
||||||
|
<el-avatar :size="36" :src="s.avatar ?? undefined">{{ (s.name ?? '?').slice(0, 1) }}</el-avatar>
|
||||||
|
<div class="yk-chat-side__meta">
|
||||||
|
<div class="yk-chat-side__name">
|
||||||
|
<span>{{ s.name ?? '--' }}</span>
|
||||||
|
<span class="yk-chat-side__today">今日{{ s.today_count }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="yk-chat-side__time">{{ s.last_chat_time ? formatTime(s.last_chat_time) : '--' }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.yk-chat-side { display: flex; flex-direction: column; height: 100%; width: 260px; border-right: 1px solid var(--el-border-color-light); background: #fff; }
|
||||||
|
.yk-chat-side__top { display: flex; gap: 6px; padding: 8px; border-bottom: 1px solid var(--el-border-color-lighter); }
|
||||||
|
.yk-chat-side__list { flex: 1; overflow-y: auto; }
|
||||||
|
.yk-chat-side__item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; cursor: pointer; }
|
||||||
|
.yk-chat-side__item:hover { background: #f5f7fa; }
|
||||||
|
.yk-chat-side__item.is-active { background: #ecf5ff; }
|
||||||
|
.yk-chat-side__meta { min-width: 0; flex: 1; }
|
||||||
|
.yk-chat-side__name { display: flex; justify-content: space-between; gap: 6px; font-size: 13px; color: #303133; }
|
||||||
|
.yk-chat-side__today { font-size: 11px; color: #909399; flex-shrink: 0; }
|
||||||
|
.yk-chat-side__time { font-size: 11px; color: #c0c4cc; margin-top: 2px; }
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user