mirror of
https://gitee.com/JavaLionLi/plus-ui.git
synced 2026-09-14 16:17:31 +08:00
- chat-core: 类型定义(Message/Conversation/SsePayload) + SseSubscriber 订阅共享 sseEventBus 'customerservice' event,按 msgType 分发 - chat-core utils: formatRelativeTime / extractUrlsAsLinks + 19 个 vitest 单测 - chat-ui: MessageStream/MessageBubble/MessageInput/MessageStateBadge/ DateDivider/ConversationDivider 组件 + useSseConversation/useMessageStream composables;CSS variable 暴露主题色 - api/customerservice: conversation/message/agent/mediaUser HTTP 客户端 按 spec 04-api.md endpoint 形态先行,后端 Task 5-13 完成后无需改前端 - views/customerservice/workbench: 三栏布局 + Pinia store 处理 6 种 SSE payload + 自动重连后全量对齐(spec 05-sse.md §6) - vitest.config.ts: 独立 standalone 配置,跳过 dev server 的 unocss 链
153 lines
3.5 KiB
Vue
153 lines
3.5 KiB
Vue
<template>
|
|
<div class="conv-list">
|
|
<el-tabs v-model="active" @tab-change="onTabChange">
|
|
<el-tab-pane label="我的对话" name="mine"></el-tab-pane>
|
|
<el-tab-pane label="无主队列" name="unassigned"></el-tab-pane>
|
|
</el-tabs>
|
|
|
|
<div class="conv-list-rows">
|
|
<div
|
|
v-for="row in rows"
|
|
:key="row.id"
|
|
class="conv-row"
|
|
:class="{ active: row.id === openedId, 'has-failure': row.hasSendFailure }"
|
|
@click="$emit('open', row.id)"
|
|
>
|
|
<img v-if="row.mediaUser.avatarUrl" :src="row.mediaUser.avatarUrl" class="conv-row-avatar" alt="" />
|
|
<div v-else class="conv-row-avatar conv-row-avatar-fallback">{{ initial(row.mediaUser.nickname) }}</div>
|
|
<div class="conv-row-body">
|
|
<div class="conv-row-line1">
|
|
<span class="conv-row-name">{{ row.mediaUser.nickname || row.mediaUser.externalUserIdInApp }}</span>
|
|
<span v-if="row.lastMsgAt" class="conv-row-time">{{ formatRelativeTime(row.lastMsgAt) }}</span>
|
|
</div>
|
|
<div class="conv-row-line2">
|
|
<span class="conv-row-preview">{{ row.lastMsgPreview ?? '' }}</span>
|
|
<el-badge v-if="row.unreadCount" :value="row.unreadCount" type="danger" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-if="rows.length === 0" class="conv-list-empty">暂无对话</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref } from 'vue';
|
|
import type { ConversationListItem } from '@/chat-core';
|
|
import { formatRelativeTime } from '@/chat-core';
|
|
|
|
const props = defineProps<{
|
|
mine: ConversationListItem[];
|
|
unassigned: ConversationListItem[];
|
|
openedId?: number | null;
|
|
}>();
|
|
|
|
defineEmits<{
|
|
open: [id: number];
|
|
'tab-change': [tab: 'mine' | 'unassigned'];
|
|
}>();
|
|
|
|
const active = ref<'mine' | 'unassigned'>('mine');
|
|
|
|
const rows = computed(() => (active.value === 'mine' ? props.mine : props.unassigned));
|
|
|
|
function initial(name?: string): string {
|
|
if (!name) return '?';
|
|
return name.charAt(0).toUpperCase();
|
|
}
|
|
|
|
function onTabChange(name: string | number) {
|
|
active.value = name as 'mine' | 'unassigned';
|
|
}
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.conv-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 100%;
|
|
border-right: 1px solid var(--chat-border-color);
|
|
}
|
|
|
|
.conv-list-rows {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.conv-row {
|
|
display: flex;
|
|
padding: 10px 12px;
|
|
cursor: pointer;
|
|
border-bottom: 1px solid var(--chat-border-color);
|
|
gap: 8px;
|
|
|
|
&:hover {
|
|
background: #f5f7fa;
|
|
}
|
|
|
|
&.active {
|
|
background: #ecf5ff;
|
|
}
|
|
|
|
&.has-failure {
|
|
border-left: 3px solid var(--chat-status-failed);
|
|
}
|
|
}
|
|
|
|
.conv-row-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
object-fit: cover;
|
|
background: var(--chat-border-color);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: #fff;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.conv-row-body {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.conv-row-line1,
|
|
.conv-row-line2 {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.conv-row-name {
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.conv-row-time {
|
|
font-size: 12px;
|
|
color: var(--chat-divider-color);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.conv-row-preview {
|
|
font-size: 12px;
|
|
color: var(--chat-divider-color);
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
flex: 1;
|
|
}
|
|
|
|
.conv-list-empty {
|
|
text-align: center;
|
|
color: var(--chat-divider-color);
|
|
padding: 24px;
|
|
font-size: 13px;
|
|
}
|
|
</style>
|