10/10
1. 设备管理界面完成 2. 用户管理界面完成 3. 会话列表完成
This commit is contained in:
146
src/views/system/conversation/compontents/ChatInput.vue
Normal file
146
src/views/system/conversation/compontents/ChatInput.vue
Normal file
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<div class="chat-input-wrap">
|
||||
<div class="input-area">
|
||||
<!-- 多行输入框 -->
|
||||
<el-input
|
||||
ref="inputRef"
|
||||
v-model="inputText"
|
||||
:autosize="{ minRows:5, maxRows:8 }"
|
||||
placeholder="请输入回复内容"
|
||||
type="textarea"
|
||||
@keydown="handleKeyDown"
|
||||
/>
|
||||
<!-- 发送按钮 -->
|
||||
<el-button
|
||||
:disabled="!canSend || sending"
|
||||
class="btn-send"
|
||||
size="large"
|
||||
type="primary"
|
||||
@click="handleSend"
|
||||
>
|
||||
{{ sending ? '发送中' : '发送' }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div>
|
||||
<!-- 图片上传按钮 -->
|
||||
<el-button
|
||||
class="btn-img"
|
||||
plain
|
||||
type="info"
|
||||
@click="handleSelectImage"
|
||||
>
|
||||
上传图片
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- 隐藏input用于选图 -->
|
||||
<input
|
||||
ref="fileRef"
|
||||
accept="image/*"
|
||||
style="display:none"
|
||||
type="file"
|
||||
@change="onFileChange"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, computed} from 'vue'
|
||||
import type {PropType} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
/** 是否正在发送中(外部控制,防止重复点击) */
|
||||
sending: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
/** 发送文本消息 */
|
||||
(e: 'send', text: string): void
|
||||
/** 选中图片文件,交给父组件上传oss */
|
||||
(e: 'uploadImage', file: File): void
|
||||
}>()
|
||||
|
||||
const inputRef = ref<InstanceType<typeof elInput>>()
|
||||
const fileRef = ref<HTMLInputElement | null>(null)
|
||||
const inputText = ref('')
|
||||
|
||||
// 是否可以发送
|
||||
const canSend = computed(() => {
|
||||
return !!inputText.value.trim()
|
||||
})
|
||||
|
||||
// 点击图片图标,唤起文件选择
|
||||
const handleSelectImage = () => {
|
||||
fileRef.value?.click()
|
||||
}
|
||||
|
||||
// 文件选择回调
|
||||
const onFileChange = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement
|
||||
const file = target.files?.[0]
|
||||
if (!file) return
|
||||
emit('uploadImage', file)
|
||||
// 清空file,选同一个图片也能触发change
|
||||
target.value = ''
|
||||
}
|
||||
|
||||
// 键盘事件:回车发送,Shift+Enter换行
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
if (e.shiftKey) {
|
||||
// shift+enter 原生换行,不拦截
|
||||
return
|
||||
}
|
||||
// 直接回车发送
|
||||
e.preventDefault()
|
||||
handleSend()
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
const handleSend = () => {
|
||||
const rawText = inputText.value.trim()
|
||||
if (!rawText) return
|
||||
// 把换行 \n 替换成 <br/>
|
||||
const htmlText = rawText.replaceAll('\n', '<br/>')
|
||||
emit('send', htmlText)
|
||||
// 发送成功后清空输入框
|
||||
inputText.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chat-input-wrap {
|
||||
flex-shrink: 0;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.input-area {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-end;
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: #f7f9fc;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-img {
|
||||
flex-shrink: 0;
|
||||
margin-top: 12px;
|
||||
padding: 9px 13px;
|
||||
color: rgba(75, 85, 99, 1);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.btn-send {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
:deep(.el-textarea__inner) {
|
||||
padding: 8px 12px;
|
||||
}
|
||||
</style>
|
||||
91
src/views/system/conversation/compontents/ChatNavBar.vue
Normal file
91
src/views/system/conversation/compontents/ChatNavBar.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<div :class="currentClass" class="chat-tip-bar">
|
||||
<!-- 提示条区域,放在MessageList内部最顶部,跟随滚动 -->
|
||||
<span>{{ currentText }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed} from 'vue'
|
||||
import {ChatNavBarType} from './types'
|
||||
import type {PropType} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: Number as PropType<ChatNavBarType>,
|
||||
required: true,
|
||||
},
|
||||
time: String,
|
||||
awaitNum: Number,
|
||||
agentInfo: {
|
||||
type: Object as PropType<{
|
||||
name: string,
|
||||
no: string,
|
||||
}>,
|
||||
}
|
||||
})
|
||||
|
||||
// 根据type自动切换文本
|
||||
const currentText = computed(() => {
|
||||
switch (props.type) {
|
||||
case ChatNavBarType.agent:
|
||||
return `${props.time} 用户点击「仍未解决,转人工」`
|
||||
case ChatNavBarType.await:
|
||||
return `排队提醒:当前排队第 ${props.awaitNum} 位,已等待 ${props.time}`
|
||||
case ChatNavBarType.accept:
|
||||
return `客服${props.agentInfo?.name}(工号 ${props.agentInfo?.no})已接入会话`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
// 样式类
|
||||
const currentClass = computed(() => {
|
||||
switch (props.type) {
|
||||
case ChatNavBarType.agent:
|
||||
return 'agent'
|
||||
case ChatNavBarType.await:
|
||||
return 'await'
|
||||
case ChatNavBarType.accept:
|
||||
return 'accept'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-tip-bar {
|
||||
min-width: 250px;
|
||||
max-width: 250px;
|
||||
overflow: hidden;
|
||||
margin: 7px auto;
|
||||
min-height: 24px;
|
||||
padding: 5px 12px;
|
||||
text-align: left;
|
||||
border-radius: 12px;
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
.chat-tip-bar span {
|
||||
text-wrap: wrap;
|
||||
}
|
||||
|
||||
.agent {
|
||||
color: rgba(156, 163, 175, 1);
|
||||
background-color: rgba(233, 236, 241, 1);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.await {
|
||||
color: rgba(234, 88, 12, 1);
|
||||
background-color: rgba(255, 247, 237, 1);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.accept {
|
||||
background-color: rgba(232, 247, 238, 1);
|
||||
color: rgba(22, 163, 74, 1);
|
||||
font-size: 11px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="agent-message">
|
||||
<img :src="avatar" class="avatar"/>
|
||||
<div class="msg-body">
|
||||
<div class="msg-header">
|
||||
<span class="name">客服{{ name }}</span>
|
||||
<span v-if="tag" class="tag agent-tag">{{ tag }}</span>
|
||||
<span class="time">{{ time }}</span>
|
||||
</div>
|
||||
<!-- 文本消息 -->
|
||||
<div v-if="type === 'text'" class="bubble" v-html="content"></div>
|
||||
<!-- 图片消息 -->
|
||||
<div v-else-if="type === 'image'" class="bubble img-bubble">
|
||||
<img :src="content" class="chat-img" @click="$emit('preview', content)"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
name: {type: String, default: '客服'},
|
||||
avatar: {type: String, default: ''},
|
||||
content: {type: String, required: true},
|
||||
type: {type: String as () => 'text' | 'image', default: 'text'},
|
||||
tag: {type: String, default: '人工客服'},
|
||||
time: {type: String, default: ''}
|
||||
})
|
||||
defineEmits(['preview'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.agent-message {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.msg-body {
|
||||
margin-left: 8px;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.msg-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.agent-tag {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
background: #fff;
|
||||
border-radius: 4px 16px 16px 16px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
color: rgba(31, 41, 55, 1);
|
||||
}
|
||||
|
||||
.img-bubble {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.chat-img {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<div class="robot-message">
|
||||
<div class="avatar">智</div>
|
||||
<div class="msg-body">
|
||||
<!-- 头部:客服名 + 标签 + 时间 -->
|
||||
<div class="msg-header">
|
||||
<span class="name">智能客服 小智</span>
|
||||
<span class="tag robot-tag">机器人</span>
|
||||
<span class="time">{{ time }}</span>
|
||||
</div>
|
||||
<!-- 气泡 -->
|
||||
<div class="bubble" v-html="content"></div>
|
||||
<!-- 快捷按钮 -->
|
||||
<div v-if="quickActions?.length" class="quick-actions">
|
||||
<button
|
||||
v-for="btn in quickActions"
|
||||
:key="btn.action"
|
||||
:class="{ primary: btn.primary }"
|
||||
class="action-btn"
|
||||
@click="$emit('quickAction', btn.action)"
|
||||
>
|
||||
{{ btn.label }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
interface QuickAction {
|
||||
label: string
|
||||
action: string
|
||||
primary?: boolean
|
||||
}
|
||||
|
||||
defineProps({
|
||||
name: {type: String, default: '智能客服'},
|
||||
avatar: {type: String, default: ''},
|
||||
content: {type: String, required: true},
|
||||
time: {type: String, default: ''},
|
||||
quickActions: {type: Array as () => QuickAction[], default: () => []}
|
||||
})
|
||||
defineEmits(['quickAction'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.robot-message {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
background-color: rgba(15, 118, 110, 1);
|
||||
}
|
||||
|
||||
.msg-body {
|
||||
margin-left: 8px;
|
||||
max-width: 75%;
|
||||
}
|
||||
|
||||
.msg-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 12px;
|
||||
color: rgba(156, 163, 175, 1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.robot-tag {
|
||||
background: #f3f0ff;
|
||||
color: #8b50ef;
|
||||
font-weight: bold;
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
background: #fff;
|
||||
border-radius: 12px 12px 12px 2px;
|
||||
padding: 10px 14px;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.quick-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 14px;
|
||||
border: 1px solid #2563eb;
|
||||
border-radius: 14px;
|
||||
background: #fff;
|
||||
color: #2563eb;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-btn.primary {
|
||||
background: #2563eb;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div class="system-message">
|
||||
<span class="system-text">{{ text }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
text: {type: String, required: true}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.system-message {
|
||||
text-align: center;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.system-text {
|
||||
display: inline-block;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
background: transparent;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="user-message">
|
||||
<div class="msg-body">
|
||||
<!-- 文本消息 -->
|
||||
<div v-if="type === 'text'" class="bubble" v-html="content"></div>
|
||||
<!-- 图片消息 -->
|
||||
<div v-else-if="type === 'image'" class="bubble img-bubble">
|
||||
<img :src="content" class="chat-img"/>
|
||||
</div>
|
||||
<div class="time">{{ time }}</div>
|
||||
</div>
|
||||
<img :src="avatar" class="avatar"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineProps({
|
||||
content: {type: String, required: true},
|
||||
type: {type: String as () => 'text' | 'image', default: 'text'},
|
||||
avatar: {type: String, default: ''},
|
||||
time: {type: String, default: ''}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.user-message {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.msg-body {
|
||||
margin-right: 8px;
|
||||
max-width: 363px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
background-color: rgba(37, 99, 235, 1);
|
||||
color: #fff;
|
||||
border-radius: 16px 4px 16px 16px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
word-break: break-all;
|
||||
|
||||
|
||||
line-height: 22px;
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
.img-bubble {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.chat-img {
|
||||
max-width: 200px;
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
260
src/views/system/conversation/compontents/MessageList/index.vue
Normal file
260
src/views/system/conversation/compontents/MessageList/index.vue
Normal file
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<div
|
||||
ref="listRef"
|
||||
class="message-list "
|
||||
@scroll.passive="handleScroll"
|
||||
>
|
||||
<!-- 加载历史消息提示 -->
|
||||
<div v-if="loadingHistory" class="load-more-tip">
|
||||
<span class="spinner"></span> 加载中...
|
||||
</div>
|
||||
|
||||
<!-- 消息列表:key 必须用唯一 id,禁止 index -->
|
||||
<template v-for="msg in messages" :key="msg.id">
|
||||
<!-- 2. 用户消息 -->
|
||||
<UserMessage
|
||||
v-if="msg.role === 'user'"
|
||||
:avatar="userAvatar"
|
||||
:content="msg.content"
|
||||
:msg-type="msg.msgType"
|
||||
:status="msg.status"
|
||||
:time="msg.time"
|
||||
@retry="handleRetry(msg)"
|
||||
/>
|
||||
|
||||
<!-- 3. 机器人消息 -->
|
||||
<RobotMessage
|
||||
v-else-if="msg.agentType === 'robot'"
|
||||
:avatar="msg.avatar"
|
||||
:content="msg.content"
|
||||
:msg-type="msg.msgType"
|
||||
:name="msg.agentName"
|
||||
:quick-actions="msg.quickActions"
|
||||
:time="msg.time"
|
||||
@quick-action="handleQuickAction"
|
||||
/>
|
||||
|
||||
<!-- 4. 人工客服消息 -->
|
||||
<AgentMessage
|
||||
v-else
|
||||
:avatar="msg.avatar"
|
||||
:content="msg.content"
|
||||
:msg-type="msg.msgType"
|
||||
:name="msg.agentName"
|
||||
:time="msg.time"
|
||||
@preview="handlePreview"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<div class="bottom-space"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watch, nextTick, onMounted} from 'vue'
|
||||
import SystemMessage from './SystemMessage.vue'
|
||||
import UserMessage from './UserMessage.vue'
|
||||
import RobotMessage from './RobotMessage.vue'
|
||||
import AgentMessage from './AgentMessage.vue'
|
||||
|
||||
interface ChatMessage {
|
||||
id: string | number
|
||||
type: 'system' | 'chat'
|
||||
role?: 'user' | 'agent'
|
||||
agentType?: 'robot' | 'human'
|
||||
content: string
|
||||
msgType?: 'text' | 'image'
|
||||
agentName?: string
|
||||
avatar?: string
|
||||
time: string
|
||||
status?: 'sending' | 'sent' | 'failed'
|
||||
quickActions?: { label: string; action: string; primary?: boolean }[]
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
messages: ChatMessage[]
|
||||
userAvatar: string
|
||||
loadingHistory?: boolean
|
||||
/** 是否还有更多历史可加载(可选,建议父组件传) */
|
||||
hasMoreHistory?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'loadMore'): void
|
||||
(e: 'quickAction', action: string): void
|
||||
(e: 'retry', msg: ChatMessage): void
|
||||
(e: 'preview', url: string): void
|
||||
}>()
|
||||
|
||||
const listRef = ref<HTMLElement>()
|
||||
|
||||
/** 用户是否贴近底部(阈值 50px) */
|
||||
const isAtBottom = ref(true)
|
||||
|
||||
/** 首屏标记:首次渲染不走平滑滚动 */
|
||||
let isFirstRender = true
|
||||
|
||||
/** 加载历史前的 scrollHeight 快照 */
|
||||
let prevScrollHeight = 0
|
||||
|
||||
/** 加载历史防抖锁 */
|
||||
let isLoadingMore = false
|
||||
|
||||
/** 阈值常量 */
|
||||
const BOTTOM_THRESHOLD = 50
|
||||
const TOP_THRESHOLD = 30
|
||||
|
||||
/* ========== 判断是否贴近底部 ========== */
|
||||
const checkAtBottom = (): boolean => {
|
||||
const el = listRef.value
|
||||
if (!el) return true
|
||||
return el.scrollHeight - el.scrollTop - el.clientHeight < BOTTOM_THRESHOLD
|
||||
}
|
||||
|
||||
/* ========== 滚动到底部 ========== */
|
||||
const scrollToBottom = (smooth = false) => {
|
||||
const el = listRef.value
|
||||
if (!el) return
|
||||
el.scrollTo({
|
||||
top: el.scrollHeight,
|
||||
behavior: smooth ? 'smooth' : 'auto'
|
||||
})
|
||||
}
|
||||
|
||||
/* ========== 滚动事件 ========== */
|
||||
const handleScroll = () => {
|
||||
const el = listRef.value
|
||||
if (!el) return
|
||||
|
||||
// 1. 更新底部状态
|
||||
isAtBottom.value = checkAtBottom()
|
||||
|
||||
// 2. 触顶加载历史
|
||||
if (
|
||||
el.scrollTop <= TOP_THRESHOLD &&
|
||||
!isLoadingMore &&
|
||||
!props.loadingHistory &&
|
||||
props.hasMoreHistory !== false
|
||||
) {
|
||||
isLoadingMore = true
|
||||
// 关键:在 emit 之前记录当前 scrollHeight 快照
|
||||
prevScrollHeight = el.scrollHeight
|
||||
emit('loadMore')
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 首屏 + 新消息滚动 ========== */
|
||||
watch(
|
||||
() => props.messages?.length ?? 0,
|
||||
async (newLen, oldLen) => {
|
||||
await nextTick()
|
||||
|
||||
if (isFirstRender) {
|
||||
// 场景 ①:首次进入页面,无动画直接滚到底
|
||||
scrollToBottom(false)
|
||||
isFirstRender = false
|
||||
isAtBottom.value = true
|
||||
return
|
||||
}
|
||||
|
||||
// 场景 ②:新消息到达 && 用户在底部 → 平滑滚到底
|
||||
// 场景 ③:用户在翻历史 → 不滚动
|
||||
if (newLen > oldLen && isAtBottom.value) {
|
||||
scrollToBottom(true)
|
||||
}
|
||||
},
|
||||
{immediate: true}
|
||||
)
|
||||
|
||||
/* ========== 加载历史后,保持视觉位置 ========== */
|
||||
watch(
|
||||
() => props.loadingHistory,
|
||||
async (loading, wasLoading) => {
|
||||
// 只在 loading 从 true → false 的瞬间处理
|
||||
if (wasLoading && !loading) {
|
||||
await nextTick()
|
||||
const el = listRef.value
|
||||
if (!el) return
|
||||
|
||||
// 场景 ④:内容变高了,把 scrollTop 平移到等价位置
|
||||
const delta = el.scrollHeight - prevScrollHeight
|
||||
el.scrollTop = el.scrollTop + delta
|
||||
|
||||
// 重算底部状态(此时肯定不在底部)
|
||||
isAtBottom.value = checkAtBottom()
|
||||
|
||||
// 解锁,允许下一次加载
|
||||
isLoadingMore = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/* ========== 挂载后兜底:图片等异步内容渲染后可能改变 scrollHeight ========== */
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
scrollToBottom(false)
|
||||
isAtBottom.value = true
|
||||
isFirstRender = false
|
||||
})
|
||||
})
|
||||
|
||||
/* ========== 对外暴露手动滚底(如发送消息成功后) ========== */
|
||||
defineExpose({
|
||||
scrollToBottom: (smooth = true) => scrollToBottom(smooth)
|
||||
})
|
||||
|
||||
/* ========== 事件转发 ========== */
|
||||
const handleQuickAction = (action: string) => emit('quickAction', action)
|
||||
const handleRetry = (msg: ChatMessage) => emit('retry', msg)
|
||||
const handlePreview = (url: string) => emit('preview', url)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.message-list {
|
||||
flex: 1;
|
||||
min-height: 0; /* ⚠️ 关键:flex 子项内滚动必须加 */
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 12px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
/* 隐藏滚动条(Chrome / Firefox / IE) */
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.message-list::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.load-more-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 2px solid #ddd;
|
||||
border-top-color: #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.bottom-space {
|
||||
height: 8px;
|
||||
}
|
||||
</style>
|
||||
287
src/views/system/conversation/compontents/data.ts
Normal file
287
src/views/system/conversation/compontents/data.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
// src/mock/chatData.ts
|
||||
|
||||
export interface QuickAction {
|
||||
label: string
|
||||
action: string
|
||||
primary?: boolean
|
||||
}
|
||||
|
||||
import {ChatMessage} from './types'
|
||||
|
||||
/** 用户头像(也可以从 store / 用户信息里取) */
|
||||
export const userAvatar = 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix'
|
||||
|
||||
/** 机器人头像 */
|
||||
export const robotAvatar = 'https://api.dicebear.com/7.x/bottts/svg?seed=Robot'
|
||||
|
||||
/** 人工客服头像 */
|
||||
export const agentAvatar = 'https://api.dicebear.com/7.x/avataaars/svg?seed=Aneka'
|
||||
|
||||
/**
|
||||
* 初始消息(首屏展示)
|
||||
* 涵盖:系统消息 / 机器人消息(含快捷按钮) / 用户消息(各种状态) / 人工客服消息(文本+图片)
|
||||
*/
|
||||
export const initMessages: ChatMessage[] = [
|
||||
// ========== 系统消息:时间分割 ==========
|
||||
{
|
||||
id: 'sys_001',
|
||||
type: 'system',
|
||||
content: '2026-01-15 14:00',
|
||||
time: '14:00',
|
||||
},
|
||||
|
||||
// ========== 系统消息:排队提示 ==========
|
||||
{
|
||||
id: 'sys_002',
|
||||
type: 'system',
|
||||
content: '您已进入排队,当前前面有 2 位用户,预计等待 1 分钟',
|
||||
time: '14:00',
|
||||
},
|
||||
|
||||
// ========== 机器人欢迎消息 ==========
|
||||
{
|
||||
id: 'robot_001',
|
||||
type: 'chat',
|
||||
role: 'agent',
|
||||
agentType: 'robot',
|
||||
agentName: '智能助手',
|
||||
avatar: robotAvatar,
|
||||
content: '您好!我是智能客服小助手 🤖\n请问有什么可以帮您的?\n\n您可以选择下面的常见问题,或直接输入您的问题。',
|
||||
msgType: 'text',
|
||||
time: '14:01',
|
||||
quickActions: [
|
||||
{label: '查询订单', action: 'query_order', primary: true},
|
||||
{label: '退换货政策', action: 'refund_policy'},
|
||||
{label: '发票问题', action: 'invoice'},
|
||||
{label: '联系人工', action: 'transfer_agent'},
|
||||
],
|
||||
},
|
||||
|
||||
// ========== 用户消息:正常文本 ==========
|
||||
{
|
||||
id: 'user_001',
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
content: '我想查一下我的订单状态',
|
||||
msgType: 'text',
|
||||
time: '14:02',
|
||||
status: 'sent',
|
||||
},
|
||||
|
||||
// ========== 机器人回复 + 快捷按钮 ==========
|
||||
{
|
||||
id: 'robot_002',
|
||||
type: 'chat',
|
||||
role: 'agent',
|
||||
agentType: 'robot',
|
||||
agentName: '智能助手',
|
||||
avatar: robotAvatar,
|
||||
content: '好的,请提供您的订单编号,我帮您查询。\n订单编号格式为:ORD-XXXXXXXX',
|
||||
msgType: 'text',
|
||||
time: '14:02',
|
||||
quickActions: [
|
||||
{label: '不知道订单号', action: 'no_order_id'},
|
||||
{label: '转人工', action: 'transfer_agent', primary: true},
|
||||
],
|
||||
},
|
||||
|
||||
// ========== 用户消息:图片 ==========
|
||||
{
|
||||
id: 'user_002',
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
content: 'https://picsum.photos/400/300?random=1',
|
||||
msgType: 'image',
|
||||
time: '14:03',
|
||||
status: 'sent',
|
||||
},
|
||||
|
||||
// ========== 系统消息:转接人工 ==========
|
||||
{
|
||||
id: 'sys_003',
|
||||
type: 'system',
|
||||
content: '正在为您转接人工客服,请稍候...',
|
||||
time: '14:03',
|
||||
},
|
||||
|
||||
// ========== 系统消息:人工接入 ==========
|
||||
{
|
||||
id: 'sys_004',
|
||||
type: 'system',
|
||||
content: '人工客服已接入,正在为您服务',
|
||||
time: '14:04',
|
||||
},
|
||||
|
||||
// ========== 人工客服:文本 ==========
|
||||
{
|
||||
id: 'agent_001',
|
||||
type: 'chat',
|
||||
role: 'agent',
|
||||
agentType: 'human',
|
||||
agentName: '客服小林',
|
||||
avatar: agentAvatar,
|
||||
content: '您好,我是人工客服小林,很高兴为您服务!\n请把订单号发给我,我马上帮您查。',
|
||||
msgType: 'text',
|
||||
time: '14:04',
|
||||
},
|
||||
|
||||
// ========== 人工客服:图片(订单截图示例) ==========
|
||||
{
|
||||
id: 'agent_002',
|
||||
type: 'chat',
|
||||
role: 'agent',
|
||||
agentType: 'human',
|
||||
agentName: '客服小林',
|
||||
avatar: agentAvatar,
|
||||
content: 'https://picsum.photos/400/300?random=2',
|
||||
msgType: 'image',
|
||||
time: '14:05',
|
||||
},
|
||||
|
||||
// ========== 用户消息:发送中 ==========
|
||||
{
|
||||
id: 'user_003',
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
content: '好的,我看到了,谢谢',
|
||||
msgType: 'text',
|
||||
time: '14:05',
|
||||
status: 'sending',
|
||||
},
|
||||
|
||||
// ========== 用户消息:发送成功 ==========
|
||||
{
|
||||
id: 'user_004',
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
content: '请问大概什么时候能到?',
|
||||
msgType: 'text',
|
||||
time: '14:06',
|
||||
status: 'sent',
|
||||
},
|
||||
|
||||
// ========== 用户消息:发送失败(红色感叹号,可重发) ==========
|
||||
{
|
||||
id: 'user_005',
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
content: '快递单号能再发一下吗?',
|
||||
msgType: 'text',
|
||||
time: '14:06',
|
||||
status: 'failed',
|
||||
},
|
||||
|
||||
// ========== 人工客服:富文本/长文本换行 ==========
|
||||
{
|
||||
id: 'agent_003',
|
||||
type: 'chat',
|
||||
role: 'agent',
|
||||
agentType: 'human',
|
||||
agentName: '客服小林',
|
||||
avatar: agentAvatar,
|
||||
content:
|
||||
'订单已于今天上午发货 📦\n物流单号:SF1234567890\n\n预计 1 月 16 日下午送达,请保持手机畅通。\n如未按时收到,可以随时联系我~',
|
||||
msgType: 'text',
|
||||
time: '14:07',
|
||||
},
|
||||
]
|
||||
|
||||
/**
|
||||
* 加载历史消息(模拟分页返回,用于验证"保持视觉位置"逻辑)
|
||||
* 每次调用返回 5~8 条更早的消息,id 递增
|
||||
*/
|
||||
let historyBatch = 0
|
||||
|
||||
export function fetchMoreHistory(): ChatMessage[] {
|
||||
historyBatch += 1
|
||||
const base = 100 * historyBatch
|
||||
|
||||
return [
|
||||
// 时间分割
|
||||
{
|
||||
id: `history_${base}_00`,
|
||||
type: 'system',
|
||||
content: `2026-01-15 ${String(13 - historyBatch).padStart(2, '0')}:50`,
|
||||
time: `${String(13 - historyBatch).padStart(2, '0')}:50`,
|
||||
},
|
||||
|
||||
// 用户历史消息
|
||||
{
|
||||
id: `history_${base}_01`,
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
content: `这是第 ${historyBatch} 批历史消息中的用户提问 1`,
|
||||
msgType: 'text',
|
||||
time: `${String(13 - historyBatch).padStart(2, '0')}:50`,
|
||||
status: 'sent',
|
||||
},
|
||||
|
||||
// 机器人回复
|
||||
{
|
||||
id: `history_${base}_02`,
|
||||
type: 'chat',
|
||||
role: 'agent',
|
||||
agentType: 'robot',
|
||||
agentName: '智能助手',
|
||||
avatar: robotAvatar,
|
||||
content: `这是机器人对历史问题 ${historyBatch}-1 的回复。`,
|
||||
msgType: 'text',
|
||||
time: `${String(13 - historyBatch).padStart(2, '0')}:51`,
|
||||
},
|
||||
|
||||
// 用户历史图片
|
||||
{
|
||||
id: `history_${base}_03`,
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
content: `https://picsum.photos/400/300?random=${base + 10}`,
|
||||
msgType: 'image',
|
||||
time: `${String(13 - historyBatch).padStart(2, '0')}:51`,
|
||||
status: 'sent',
|
||||
},
|
||||
|
||||
// 人工客服历史回复
|
||||
{
|
||||
id: `history_${base}_04`,
|
||||
type: 'chat',
|
||||
role: 'agent',
|
||||
agentType: 'human',
|
||||
agentName: '客服小林',
|
||||
avatar: agentAvatar,
|
||||
content: `历史人工回复:已为您记录,请稍候。`,
|
||||
msgType: 'text',
|
||||
time: `${String(13 - historyBatch).padStart(2, '0')}:52`,
|
||||
},
|
||||
|
||||
// 系统消息
|
||||
{
|
||||
id: `history_${base}_05`,
|
||||
type: 'system',
|
||||
content: `—— 以上为第 ${historyBatch} 批历史消息 ——`,
|
||||
time: `${String(13 - historyBatch).padStart(2, '0')}:52`,
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* 模拟机器人回复(发送消息后延迟触发)
|
||||
*/
|
||||
export function mockRobotReply(userText: string): ChatMessage {
|
||||
return {
|
||||
id: `robot_${Date.now()}`,
|
||||
type: 'chat',
|
||||
role: 'agent',
|
||||
agentType: 'robot',
|
||||
agentName: '智能助手',
|
||||
avatar: robotAvatar,
|
||||
content: `收到您的问题:"${userText}"\n正在为您查询,请稍候...`,
|
||||
msgType: 'text',
|
||||
time: formatNowTime(),
|
||||
}
|
||||
}
|
||||
|
||||
/** 格式化当前时间为 HH:mm */
|
||||
export function formatNowTime(): string {
|
||||
const d = new Date()
|
||||
return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
|
||||
}
|
||||
37
src/views/system/conversation/compontents/types.ts
Normal file
37
src/views/system/conversation/compontents/types.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
// 消息类型
|
||||
type MessageType = 'text' | 'image' | 'system' | 'robot' | 'action'
|
||||
|
||||
|
||||
// chatNavBar
|
||||
// export type ChatNavBarType = {
|
||||
// /** 需要转人工 */
|
||||
// agent: 'agent',
|
||||
// /** 排队提醒 */
|
||||
// await: 'await',
|
||||
// /** 客服已接入会话*/
|
||||
// accept: 'accept',
|
||||
// }
|
||||
|
||||
export enum ChatNavBarType {
|
||||
/** 需要转人工 */
|
||||
agent,
|
||||
/** 排队提醒 */
|
||||
await,
|
||||
/** 客服已接入会话*/
|
||||
accept,
|
||||
}
|
||||
|
||||
|
||||
export interface ChatMessage {
|
||||
id: string | number
|
||||
type: 'system' | 'chat'
|
||||
role?: 'user' | 'agent'
|
||||
agentType?: 'robot' | 'human'
|
||||
content: string
|
||||
msgType?: 'text' | 'image'
|
||||
agentName?: string
|
||||
avatar?: string
|
||||
time: string
|
||||
status?: 'sending' | 'sent' | 'failed'
|
||||
quickActions?: { label: string; action: string; primary?: boolean }[]
|
||||
}
|
||||
413
src/views/system/conversation/conversationMain.vue
Normal file
413
src/views/system/conversation/conversationMain.vue
Normal file
@@ -0,0 +1,413 @@
|
||||
<script lang="ts" setup>
|
||||
import pageCrumb from '@/components/Pagecrumb/index.vue'
|
||||
|
||||
|
||||
import ChatNavBar from "./compontents/ChatNavBar.vue";
|
||||
import {ChatNavBarType} from "./compontents/types";
|
||||
|
||||
import MessageList from './compontents/MessageList/index.vue'
|
||||
import {initMessages, userAvatar} from "./compontents/data";
|
||||
import ChatInput from "@/views/system/conversation/compontents/ChatInput.vue";
|
||||
|
||||
const defaultImage = import.meta.env.VITE_DEFAULT_USER_IMAGE;
|
||||
const userDevicesList = [
|
||||
{
|
||||
label: 'Z2 空气净化器 · 客厅',
|
||||
model: 'CZ-Z2023',
|
||||
mac: "AA:BB:CC:DD:A3:B6",
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
label: 'Z2 空气净化器 · 主卧',
|
||||
model: 'CZ-Z2023',
|
||||
mac: "AA:BB:CC:DD:A3:B1",
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
label: 'Z2 空气净化器 · 儿童房',
|
||||
model: 'CZ-Z2023',
|
||||
mac: "AA:BB:CC:DD:A3:B2",
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
label: 'Z2 空气净化器 · 次卧',
|
||||
model: 'CZ-Z2023',
|
||||
mac: "AA:BB:CC:DD:A3:B3",
|
||||
status: 0,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const form = ref({
|
||||
status: ChatNavBarType.await as ChatNavBarType,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
awaitNum: 3,
|
||||
agentInfo: {
|
||||
no: '8823',
|
||||
name: '小雨'
|
||||
},
|
||||
message: []
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
form.value.message = [...initMessages]
|
||||
})
|
||||
|
||||
|
||||
const sending = ref(false)
|
||||
// 发送文本消息
|
||||
const onSendMsg = async (text: string) => {
|
||||
sending.value = true
|
||||
// 组装消息,推入messages列表
|
||||
const newMsg = {
|
||||
id: Date.now().toString(),
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
msgType: 'text',
|
||||
content: text,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
status: 'sending'
|
||||
}
|
||||
console.log(text)
|
||||
form.value.message.push(newMsg)
|
||||
try {
|
||||
// 请求后端接口
|
||||
// await api.sendChat(...)
|
||||
newMsg.status = 'sent'
|
||||
} catch (err) {
|
||||
newMsg.status = 'failed'
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传,拿到File,上传oss拿到url,生成图片消息
|
||||
const onUploadImage = async (file: File) => {
|
||||
// 调用oss上传接口,拿到真实图片url
|
||||
// const url = await uploadOss(file)
|
||||
const url = 'https://xxx.oss-cn-beijing.aliyuncs.com/demo.jpg'
|
||||
const imgMsg = {
|
||||
id: Date.now().toString(),
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
msgType: 'image',
|
||||
content: url,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
status: 'sending'
|
||||
}
|
||||
form.value.message.push(imgMsg)
|
||||
}
|
||||
|
||||
|
||||
// 1. 获取用户列表
|
||||
// 2. 点击用户,开始会话,ws
|
||||
// 3. 查看用户信息,
|
||||
// 4. 获取过往信息
|
||||
// 5. 自动发送开场白
|
||||
// 6. ws 发送消息
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="Main beautify_scroll">
|
||||
<pageCrumb>
|
||||
<template #rightSlot>
|
||||
<el-button plain type="info">返回会话列表</el-button>
|
||||
</template>
|
||||
</pageCrumb>
|
||||
<div class="commonCard">
|
||||
<card-nav
|
||||
:show-line="false"
|
||||
subTitle="开始时间 2026-09-10 12:44:52 · 等待时长 00:38 · 接入方式:机器人接待 → 转人工"
|
||||
title="CS20260918100038"
|
||||
>
|
||||
<template #rightTitle>
|
||||
<el-tag type="primary">人工接待</el-tag>
|
||||
</template>
|
||||
<template #rightSlot>
|
||||
<el-button plain type="default">导出聊天记录</el-button>
|
||||
<el-button plain type="danger">关闭会话</el-button>
|
||||
</template>
|
||||
</card-nav>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :lg="16" :md="15" :span="24" :xl="18">
|
||||
<div class="commonCard userWindowBox">
|
||||
<div class="userWindowNav userWindowNavItem">
|
||||
<el-image :src="defaultImage" class="userAvatar"></el-image>
|
||||
<div class="userWindowNavItem-l">
|
||||
<div class="userWindowNavItem-l-info">
|
||||
<div>
|
||||
<span class="name">林筱曼</span>
|
||||
<span class="phone">138****6688</span>
|
||||
</div>
|
||||
<span class="time">会话时长 06:12</span>
|
||||
</div>
|
||||
<div class="userWindowNavItem-l-status">
|
||||
<i class="circle"></i>
|
||||
<span>用户在线</span>
|
||||
<span style="margin:0 0.5rem;">·</span>
|
||||
<span>正在输入</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content beautify_scroll">
|
||||
<ChatNavBar
|
||||
:agent-info="form.agentInfo"
|
||||
:await-num="form.awaitNum"
|
||||
:time="form.time"
|
||||
:type="form.status"
|
||||
></ChatNavBar>
|
||||
<MessageList :messages="form.message" :user-avatar="userAvatar"></MessageList>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<!-- 输入框组件 -->
|
||||
<ChatInput :sending="sending" @send="onSendMsg" @uploadImage="onUploadImage"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :lg="8" :md="9" :span="24" :xl="6">
|
||||
<div class="commonCard cardUserInfoBox">
|
||||
<card-nav title="用户信息">
|
||||
<template slot="rightTitle">
|
||||
<el-tag type="primary">用户详情</el-tag>
|
||||
</template>
|
||||
</card-nav>
|
||||
<ul class="cardUserInfo">
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">用户ID</span>
|
||||
<span class="value">U20260918001</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">昵称</span>
|
||||
<span class="value">林小满</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">手机号</span>
|
||||
<span class="value">138****6688</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">注册时间</span>
|
||||
<span class="value">2026-01-10 14:20</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="userDevicesBox">
|
||||
<card-nav :show-line="false">
|
||||
<template #title>
|
||||
<span class="titleName">名下设备(3台)</span>
|
||||
</template>
|
||||
<template #rightSlot>
|
||||
<span class="tip">点击可跳转设备详情</span>
|
||||
</template>
|
||||
</card-nav>
|
||||
<ul class="userDevicesList beautify_scroll">
|
||||
<li v-for="(item,index) in userDevicesList" :key="index" class="userDevicesItem">
|
||||
<div class="userDevicesItem_item">
|
||||
<div class="userDevicesName">
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
<el-tag v-if="item.status === 0">在线</el-tag>
|
||||
<el-tag v-else>离线</el-tag>
|
||||
</div>
|
||||
<div class="userDevices_describe">
|
||||
{{ item.model }} · {{ item.mac }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="commonCard">
|
||||
<card-nav line-color="#14b8a6" title="会话信息"></card-nav>
|
||||
<ul class="cardUserInfo" style="margin-bottom: 0">
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">会话 ID</span>
|
||||
<span class="value">U20260918001</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">会话开始时间</span>
|
||||
<span class="value">2026-09-10 12:44:52</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">接入方式</span>
|
||||
<span class="value">机器人接待 → 转人工</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">当前等待时长</span>
|
||||
<span class="value" style="color: #EA580C">00:38</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.Main {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 40px;
|
||||
padding-top: 20px;
|
||||
|
||||
.cardUserInfoBox {
|
||||
.cardUserInfo {
|
||||
margin-bottom: 40px;
|
||||
min-height: 128px;
|
||||
margin-top: 27px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.cardUserInfo_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.label {
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.value {
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userDevicesBox {
|
||||
.titleName {
|
||||
color: rgba(31, 41, 55, 1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
line-height: 14px;
|
||||
color: rgba(156, 163, 175, 1);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.userDevicesList {
|
||||
padding-right: 10px;
|
||||
min-height: (75+8)*3px;
|
||||
max-height: (75+8)*3px;
|
||||
margin-top: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
.userDevicesItem {
|
||||
min-height: 64px;
|
||||
border-radius: 12px 12px 12px 12px;
|
||||
background-color: rgba(247, 249, 252, 1);
|
||||
border: 1px solid rgba(238, 240, 243, 1);
|
||||
padding: 15px 13px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.userDevicesItem_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.userDevicesName {
|
||||
width: 230px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 16px;
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.userDevices_describe {
|
||||
line-height: 14px;
|
||||
color: rgba(156, 163, 175, 1);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userWindowBox {
|
||||
padding: 25px 0;
|
||||
background-color: #fff;
|
||||
|
||||
.userWindowNav.userWindowNavItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 21px;
|
||||
padding-bottom: 19px;
|
||||
|
||||
.userAvatar {
|
||||
width: 41px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background-color: rgba(241, 245, 249, 1);
|
||||
object-fit: contain;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.userWindowNavItem-l {
|
||||
flex: 1;
|
||||
|
||||
.userWindowNavItem-l-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.name {
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.phone, .time {
|
||||
line-height: 14px;
|
||||
color: rgba(156, 163, 175, 1);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.userWindowNavItem-l-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 12px;
|
||||
|
||||
.circle {
|
||||
display: block;
|
||||
width: 7px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
margin-right: 6px;
|
||||
background-color: rgba(22, 163, 74, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 650px;
|
||||
background-color: rgba(247, 248, 250, 1);
|
||||
padding: 20px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="p-2 app-container monitor-online-page">
|
||||
<Pagecrumb></Pagecrumb>
|
||||
<Pagecrumb style="margin-top:22px"></Pagecrumb>
|
||||
<div class="search-wrap">
|
||||
<el-card class="search-panel" shadow="hover">
|
||||
<el-form ref="queryFormRef" :inline="true" :model="queryParams" class="query-form" label-position="top">
|
||||
@@ -22,10 +22,8 @@
|
||||
size="large"
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option label="机器人" value="0"></el-option>
|
||||
<el-option label="排队" value="1"></el-option>
|
||||
<el-option label="人工" value="2"></el-option>
|
||||
<el-option label="已结束" value="3"></el-option>
|
||||
<el-option v-for="(item,index) in com_session_state " :key="index" :label="item.label"
|
||||
:value="item.value"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="问题类型" prop="problemType">
|
||||
@@ -87,10 +85,10 @@
|
||||
<span style="margin:0 8px;color: #fff;">全部</span>
|
||||
<span style="color: #fff;font-weight: bold">1286</span>
|
||||
</el-button>
|
||||
<tableTag circle-color="#7c3aed" num="862" title="机器人接待"></tableTag>
|
||||
<tableTag circle-color="#ea580c" num="7" title="排队等待"></tableTag>
|
||||
<tableTag num="12" title="人工接待"></tableTag>
|
||||
<tableTag circle-color="#94a3b8" num="405" title="已结束"></tableTag>
|
||||
<tableTag :num="862" circle-color="#7c3aed" title="机器人接待"></tableTag>
|
||||
<tableTag :num="7" circle-color="#ea580c" title="排队等待"></tableTag>
|
||||
<tableTag :num="12" title="人工接待"></tableTag>
|
||||
<tableTag :num="405" circle-color="#94a3b8" title="已结束"></tableTag>
|
||||
</div>
|
||||
<div>
|
||||
<el-button plain type="info">批量导出</el-button>
|
||||
@@ -106,41 +104,55 @@
|
||||
style="width: 100%"
|
||||
>
|
||||
<el-table-column align="center" type="selection"></el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="会话ID" prop="tokenId"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="用户信息" prop="userName"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="关联设备" prop="clientKey"/>
|
||||
<el-table-column align="center" label="会话状态">
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="会话ID" prop="sessionNo"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="用户信息" prop="userNickName"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="关联设备" prop="deviceId"/>
|
||||
<el-table-column align="center" label="会话状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag type="primary">在线</el-tag>
|
||||
<el-tag type="danger">离线</el-tag>
|
||||
<dict-tag :options="com_session_state" :value="scope.row.status"></dict-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="当前接待人" prop="deptName"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="用户问题摘要" prop="ipaddr"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="排队序号" prop="loginLocation"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="等待时长" prop="os"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="最后消息时间" prop="browser"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="当前接待人" prop="agentName" width="160">
|
||||
<template #default="scope">
|
||||
<div class="name">{{ scope.row.agentName }}</div>
|
||||
<div class="no"></div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="用户问题摘要" prop="summary"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="排队序号" prop="queueNo" width="100"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="等待时长" prop="os" width="100"/>
|
||||
<el-table-column :show-overflow-tooltip="true" align="center" label="最后消息时间" prop="lastMessageTime"
|
||||
width="160"/>
|
||||
<el-table-column align="center" class-name="small-padding fixed-width" label="操作" min-width="200">
|
||||
<template #default="scope">
|
||||
<div>
|
||||
<el-button
|
||||
v-if="scope.row.staus !== '3'"
|
||||
v-hasPermi="['cs:session:query']"
|
||||
type="primary"
|
||||
@click="handleOpenConversation(scope.row)"
|
||||
>进入会话
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['monitor:online:forceLogout']"
|
||||
type="primary"
|
||||
v-else
|
||||
@click="handleMain(scope.row)"
|
||||
>进入会话
|
||||
>查看会话
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['monitor:online:forceLogout']"
|
||||
plain
|
||||
type="info"
|
||||
@click="handleMain(scope.row)"
|
||||
@click="handleUserMain(scope.row)"
|
||||
>用户档案
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['monitor:online:forceLogout']"
|
||||
plain
|
||||
type="danger"
|
||||
@click="handleMain(scope.row)"
|
||||
v-if="scope.row.status === '2'"
|
||||
@click="handleCloseConversation(scope.row)"
|
||||
>关闭会话
|
||||
</el-button>
|
||||
</div>
|
||||
@@ -160,13 +172,13 @@
|
||||
|
||||
<script lang="ts" name="Online" setup>
|
||||
import {to} from 'await-to-js';
|
||||
import {listCsSession as initData} from '@/api/system/conversation/index';
|
||||
import {closeCsSession, listCsSession as initData} from '@/api/system/conversation/index';
|
||||
import {CsSessionQuery, CsSessionVO} from '@/api/system/conversation/types';
|
||||
import modal from '@/plugins/modal';
|
||||
import {useDict} from '@/utils/dict';
|
||||
import {parseTime} from '@/utils/ruoyi';
|
||||
|
||||
const {sys_device_type} = toRefs<any>(useDict('sys_device_type'));
|
||||
const {com_session_state} = toRefs<any>(useDict('com_session_state'));
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
@@ -202,7 +214,35 @@ const resetQuery = () => {
|
||||
handleQuery();
|
||||
};
|
||||
|
||||
function handleMain(row) {
|
||||
// 用户 档案
|
||||
function handleUserMain(row: Partial<CsSessionVO>) {
|
||||
router.push({
|
||||
path: '/customer/customerMain',
|
||||
query: {
|
||||
id: row.userId,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 进入会话
|
||||
function handleOpenConversation(row: Partial<CsSessionVO>) {
|
||||
router.push({
|
||||
path: '/conversation/conversationMain',
|
||||
query: {
|
||||
id: row.sessionId,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// close conversation
|
||||
function handleCloseConversation(row: Partial<CsSessionVO>) {
|
||||
closeCsSession(row.sessionId).then(() => {
|
||||
getList();
|
||||
modal.msgSuccess('关闭会话成功')
|
||||
})
|
||||
}
|
||||
|
||||
function handleMain(row: Partial<CsSessionVO>) {
|
||||
console.log(row);
|
||||
router.push({
|
||||
path: '/conversation/conversationMain'
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
<script lang="ts" setup>
|
||||
import pageCrumb from '@/components/Pagecrumb/index.vue'
|
||||
|
||||
|
||||
import ChatNavBar from "./compontents/ChatNavBar.vue";
|
||||
import {ChatNavBarType} from "./compontents/types";
|
||||
|
||||
import MessageList from './compontents/MessageList/index.vue'
|
||||
import {initMessages, userAvatar} from "./compontents/data";
|
||||
import ChatInput from "@/views/system/conversation/compontents/ChatInput.vue";
|
||||
|
||||
const defaultImage = import.meta.env.VITE_DEFAULT_USER_IMAGE;
|
||||
const userDevicesList = [
|
||||
{
|
||||
label: 'Z2 空气净化器 · 客厅',
|
||||
model: 'CZ-Z2023',
|
||||
mac: "AA:BB:CC:DD:A3:B6",
|
||||
status: 0,
|
||||
},
|
||||
{
|
||||
label: 'Z2 空气净化器 · 主卧',
|
||||
model: 'CZ-Z2023',
|
||||
mac: "AA:BB:CC:DD:A3:B1",
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
label: 'Z2 空气净化器 · 儿童房',
|
||||
model: 'CZ-Z2023',
|
||||
mac: "AA:BB:CC:DD:A3:B2",
|
||||
status: 1,
|
||||
},
|
||||
{
|
||||
label: 'Z2 空气净化器 · 次卧',
|
||||
model: 'CZ-Z2023',
|
||||
mac: "AA:BB:CC:DD:A3:B3",
|
||||
status: 0,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
const form = ref({
|
||||
status: ChatNavBarType.await as ChatNavBarType,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
awaitNum: 3,
|
||||
agentInfo: {
|
||||
no: '8823',
|
||||
name: '小雨'
|
||||
},
|
||||
message: []
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
form.value.message = [...initMessages]
|
||||
})
|
||||
|
||||
|
||||
const sending = ref(false)
|
||||
// 发送文本消息
|
||||
const onSendMsg = async (text: string) => {
|
||||
sending.value = true
|
||||
// 组装消息,推入messages列表
|
||||
const newMsg = {
|
||||
id: Date.now().toString(),
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
msgType: 'text',
|
||||
content: text,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
status: 'sending'
|
||||
}
|
||||
console.log(text)
|
||||
form.value.message.push(newMsg)
|
||||
try {
|
||||
// 请求后端接口
|
||||
// await api.sendChat(...)
|
||||
newMsg.status = 'sent'
|
||||
} catch (err) {
|
||||
newMsg.status = 'failed'
|
||||
} finally {
|
||||
sending.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 图片上传,拿到File,上传oss拿到url,生成图片消息
|
||||
const onUploadImage = async (file: File) => {
|
||||
// 调用oss上传接口,拿到真实图片url
|
||||
// const url = await uploadOss(file)
|
||||
const url = 'https://xxx.oss-cn-beijing.aliyuncs.com/demo.jpg'
|
||||
const imgMsg = {
|
||||
id: Date.now().toString(),
|
||||
type: 'chat',
|
||||
role: 'user',
|
||||
msgType: 'image',
|
||||
content: url,
|
||||
time: new Date().toLocaleTimeString(),
|
||||
status: 'sending'
|
||||
}
|
||||
form.value.message.push(imgMsg)
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="Main beautify_scroll">
|
||||
<pageCrumb>
|
||||
<template #rightSlot>
|
||||
<el-button plain type="info">返回会话列表</el-button>
|
||||
</template>
|
||||
</pageCrumb>
|
||||
<div class="commonCard">
|
||||
<card-nav
|
||||
:show-line="false"
|
||||
subTitle="开始时间 2026-09-10 12:44:52 · 等待时长 00:38 · 接入方式:机器人接待 → 转人工"
|
||||
title="CS20260918100038"
|
||||
>
|
||||
<template #rightTitle>
|
||||
<el-tag type="primary">人工接待</el-tag>
|
||||
</template>
|
||||
<template #rightSlot>
|
||||
<el-button plain type="default">导出聊天记录</el-button>
|
||||
<el-button plain type="danger">关闭会话</el-button>
|
||||
</template>
|
||||
</card-nav>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :lg="16" :md="15" :span="24" :xl="18">
|
||||
<div class="commonCard userWindowBox">
|
||||
<div class="userWindowNav userWindowNavItem">
|
||||
<el-image :src="defaultImage" class="userAvatar"></el-image>
|
||||
<div class="userWindowNavItem-l">
|
||||
<div class="userWindowNavItem-l-info">
|
||||
<div>
|
||||
<span class="name">林筱曼</span>
|
||||
<span class="phone">138****6688</span>
|
||||
</div>
|
||||
<span class="time">会话时长 06:12</span>
|
||||
</div>
|
||||
<div class="userWindowNavItem-l-status">
|
||||
<i class="circle"></i>
|
||||
<span>用户在线</span>
|
||||
<span style="margin:0 0.5rem;">·</span>
|
||||
<span>正在输入</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content beautify_scroll">
|
||||
<ChatNavBar
|
||||
:agent-info="form.agentInfo"
|
||||
:await-num="form.awaitNum"
|
||||
:time="form.time"
|
||||
:type="form.status"
|
||||
></ChatNavBar>
|
||||
<MessageList :messages="form.message" :user-avatar="userAvatar"></MessageList>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<!-- 输入框组件 -->
|
||||
<ChatInput :sending="sending" @send="onSendMsg" @uploadImage="onUploadImage"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :lg="8" :md="9" :span="24" :xl="6">
|
||||
<div class="commonCard cardUserInfoBox">
|
||||
<card-nav title="用户信息">
|
||||
<template slot="rightTitle">
|
||||
<el-tag type="primary">用户详情</el-tag>
|
||||
</template>
|
||||
</card-nav>
|
||||
<ul class="cardUserInfo">
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">用户ID</span>
|
||||
<span class="value">U20260918001</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">昵称</span>
|
||||
<span class="value">林小满</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">手机号</span>
|
||||
<span class="value">138****6688</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">注册时间</span>
|
||||
<span class="value">2026-01-10 14:20</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="userDevicesBox">
|
||||
<card-nav :show-line="false">
|
||||
<template #title>
|
||||
<span class="titleName">名下设备(3台)</span>
|
||||
</template>
|
||||
<template #rightSlot>
|
||||
<span class="tip">点击可跳转设备详情</span>
|
||||
</template>
|
||||
</card-nav>
|
||||
<ul class="userDevicesList beautify_scroll">
|
||||
<li v-for="(item,index) in userDevicesList" :key="index" class="userDevicesItem">
|
||||
<div class="userDevicesItem_item">
|
||||
<div class="userDevicesName">
|
||||
<span>{{ item.label }}</span>
|
||||
</div>
|
||||
<el-tag v-if="item.status === 0">在线</el-tag>
|
||||
<el-tag v-else>离线</el-tag>
|
||||
</div>
|
||||
<div class="userDevices_describe">
|
||||
{{ item.model }} · {{ item.mac }}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="commonCard">
|
||||
<card-nav line-color="#14b8a6" title="会话信息"></card-nav>
|
||||
<ul class="cardUserInfo" style="margin-bottom: 0">
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">会话 ID</span>
|
||||
<span class="value">U20260918001</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">会话开始时间</span>
|
||||
<span class="value">2026-09-10 12:44:52</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">接入方式</span>
|
||||
<span class="value">机器人接待 → 转人工</span>
|
||||
</li>
|
||||
<li class="cardUserInfo_item">
|
||||
<span class="label">当前等待时长</span>
|
||||
<span class="value" style="color: #EA580C">00:38</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.Main {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
max-height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding-bottom: 40px;
|
||||
padding-top: 20px;
|
||||
|
||||
.cardUserInfoBox {
|
||||
.cardUserInfo {
|
||||
margin-bottom: 40px;
|
||||
min-height: 128px;
|
||||
margin-top: 27px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
|
||||
.cardUserInfo_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.label {
|
||||
height: 14px;
|
||||
line-height: 14px;
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.value {
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userDevicesBox {
|
||||
.titleName {
|
||||
color: rgba(31, 41, 55, 1);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.tip {
|
||||
line-height: 14px;
|
||||
color: rgba(156, 163, 175, 1);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.userDevicesList {
|
||||
padding-right: 10px;
|
||||
min-height: (75+8)*3px;
|
||||
max-height: (75+8)*3px;
|
||||
margin-top: 20px;
|
||||
overflow-y: auto;
|
||||
|
||||
.userDevicesItem {
|
||||
min-height: 64px;
|
||||
border-radius: 12px 12px 12px 12px;
|
||||
background-color: rgba(247, 249, 252, 1);
|
||||
border: 1px solid rgba(238, 240, 243, 1);
|
||||
padding: 15px 13px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.userDevicesItem_item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
|
||||
.userDevicesName {
|
||||
width: 230px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: 16px;
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
.userDevices_describe {
|
||||
line-height: 14px;
|
||||
color: rgba(156, 163, 175, 1);
|
||||
font-size: 11px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.userWindowBox {
|
||||
padding: 25px 0;
|
||||
background-color: #fff;
|
||||
|
||||
.userWindowNav.userWindowNavItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 21px;
|
||||
padding-bottom: 19px;
|
||||
|
||||
.userAvatar {
|
||||
width: 41px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background-color: rgba(241, 245, 249, 1);
|
||||
object-fit: contain;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.userWindowNavItem-l {
|
||||
flex: 1;
|
||||
|
||||
.userWindowNavItem-l-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.name {
|
||||
height: 18px;
|
||||
line-height: 18px;
|
||||
color: rgba(17, 24, 39, 1);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.phone, .time {
|
||||
line-height: 14px;
|
||||
color: rgba(156, 163, 175, 1);
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.userWindowNavItem-l-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: rgba(107, 114, 128, 1);
|
||||
font-size: 12px;
|
||||
|
||||
.circle {
|
||||
display: block;
|
||||
width: 7px;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
margin-right: 6px;
|
||||
background-color: rgba(22, 163, 74, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 650px;
|
||||
background-color: rgba(247, 248, 250, 1);
|
||||
padding: 20px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.footer {
|
||||
padding: 0 21px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user