snack-mall/admin-snack/src/views/chat/list.vue

285 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script setup lang="ts">
import { ref, onMounted, nextTick } from 'vue'
import { Search, ChatDotRound, Promotion, Picture } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { getChatSessionPageApi, getChatMessagesApi, sendChatMessageApi } from '@/api/chat'
import type { ChatSession, ChatMessage } from '@/api/chat'
import { fromNow } from '@/utils/date'
const sessionList = ref<ChatSession[]>([])
const activeSession = ref<ChatSession | null>(null)
const messages = ref<ChatMessage[]>([])
const inputText = ref('')
const messagesRef = ref<HTMLElement>()
async function fetchSessions() {
const res: any = await getChatSessionPageApi({ current: 1, size: 20 })
sessionList.value = res.records
if (!activeSession.value && sessionList.value.length > 0) {
selectSession(sessionList.value[0])
}
}
async function selectSession(s: ChatSession) {
activeSession.value = s
const res: any = await getChatMessagesApi(s.id)
messages.value = res
await nextTick()
scrollToBottom()
}
async function sendMessage() {
if (!inputText.value.trim() || !activeSession.value) return
const content = inputText.value
inputText.value = ''
// 立即本地显示
messages.value.push({
id: Date.now(),
sessionId: activeSession.value.id,
senderType: 1,
type: 'text',
content,
createTime: new Date().toLocaleString('zh-CN')
})
await nextTick()
scrollToBottom()
// 异步推送
try {
await sendChatMessageApi(activeSession.value.id, { type: 'text', content })
} catch (e) {
ElMessage.error('发送失败')
}
}
function scrollToBottom() {
if (messagesRef.value) {
messagesRef.value.scrollTop = messagesRef.value.scrollHeight
}
}
onMounted(fetchSessions)
</script>
<template>
<div class="page-container chat-page">
<div class="page-header">
<div>
<h2>客服消息</h2>
<p class="page-desc">实时处理用户咨询支持文字/图片消息</p>
</div>
</div>
<div class="chat-container">
<!-- 会话列表 -->
<div class="session-list">
<div class="session-header">
<el-input
placeholder="搜索用户"
:prefix-icon="Search"
size="default"
clearable
/>
</div>
<div class="session-scroll">
<div
v-for="s in sessionList"
:key="s.id"
:class="['session-item', { active: activeSession?.id === s.id }]"
@click="selectSession(s)"
>
<el-avatar :size="40">{{ s.username.charAt(0) }}</el-avatar>
<div class="session-info">
<div class="session-top">
<span class="session-name">{{ s.username }}</span>
<span class="session-time">{{ fromNow(s.updateTime) }}</span>
</div>
<div class="session-bottom">
<span class="last-msg">{{ s.lastMessage }}</span>
<el-badge v-if="s.unreadCount > 0" :value="s.unreadCount" class="badge" />
</div>
</div>
</div>
</div>
</div>
<!-- 聊天窗口 -->
<div class="chat-window">
<template v-if="activeSession">
<div class="chat-header">
<el-avatar :size="36">{{ activeSession.username.charAt(0) }}</el-avatar>
<div class="chat-user">
<div class="chat-name">{{ activeSession.username }}</div>
<div class="chat-no">会话号:{{ activeSession.sessionNo }}</div>
</div>
</div>
<div ref="messagesRef" class="chat-messages">
<div
v-for="m in messages"
:key="m.id"
:class="['msg-row', m.senderType === 1 ? 'mine' : 'theirs']"
>
<el-avatar v-if="m.senderType === 0" :size="32">
{{ activeSession.username.charAt(0) }}
</el-avatar>
<div class="msg-bubble">{{ m.content }}</div>
<el-avatar v-if="m.senderType === 1" :size="32" style="background: #1E40AF">
</el-avatar>
</div>
</div>
<div class="chat-input">
<el-input
v-model="inputText"
type="textarea"
:rows="3"
placeholder="请输入回复内容..."
@keydown.ctrl.enter="sendMessage"
/>
<div class="chat-actions">
<el-button :icon="Picture" text>图片</el-button>
<el-button :icon="Promotion" text>快捷回复</el-button>
<el-button type="primary" :disabled="!inputText.trim()" @click="sendMessage">
发送 (Ctrl+Enter)
</el-button>
</div>
</div>
</template>
<el-empty v-else description="请选择会话" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.chat-page {
height: calc(100vh - 100px);
}
.chat-container {
display: flex;
height: calc(100vh - 200px);
background: #fff;
border-radius: 10px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.session-list {
width: 280px;
border-right: 1px solid $color-border-light;
display: flex;
flex-direction: column;
}
.session-header {
padding: 12px;
border-bottom: 1px solid $color-border-light;
}
.session-scroll {
flex: 1;
overflow-y: auto;
}
.session-item {
display: flex;
gap: 10px;
padding: 12px;
cursor: pointer;
border-bottom: 1px solid $color-border-light;
transition: background 0.2s;
&:hover { background: $bg-hover; }
&.active { background: rgba(30, 64, 175, 0.08); }
}
.session-info {
flex: 1;
min-width: 0;
}
.session-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.session-name {
font-weight: 500;
font-size: 14px;
}
.session-time {
font-size: 11px;
color: $color-text-placeholder;
}
.session-bottom {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
.last-msg {
flex: 1;
font-size: 12px;
color: $color-text-secondary;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.chat-window {
flex: 1;
display: flex;
flex-direction: column;
}
.chat-header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 20px;
border-bottom: 1px solid $color-border-light;
}
.chat-name {
font-weight: 500;
}
.chat-no {
font-size: 12px;
color: $color-text-secondary;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
background: $bg-page;
}
.msg-row {
display: flex;
gap: 8px;
margin-bottom: 16px;
align-items: flex-start;
&.mine {
flex-direction: row-reverse;
}
}
.msg-bubble {
max-width: 60%;
padding: 8px 12px;
background: #fff;
border-radius: 8px;
font-size: 14px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.msg-row.mine .msg-bubble {
background: $brand-primary;
color: #fff;
}
.chat-input {
border-top: 1px solid $color-border-light;
padding: 12px 20px;
}
.chat-actions {
margin-top: 8px;
display: flex;
justify-content: flex-end;
align-items: center;
gap: 8px;
}
</style>