feat: 添加客服在线状态和聊天粘贴图片功能

**客服在线状态:**
- 后端:添加 admin_users.support_status 字段(online/offline/busy)
- 接口:PUT /admin/me/support-status 更新状态
- 前端:管理员顶部导航显示状态切换器(仅客服权限可见)
- 转接对话框显示客服在线状态和智能排序(在线优先)

**聊天粘贴图片:**
- 用户端、移动端、管理端聊天输入框支持 Ctrl+V/Cmd+V 直接粘贴图片
- 自动调用图片压缩和上传逻辑,与文件选择上传保持一致
- 最多支持粘贴9张图片

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yml
2026-06-05 19:23:34 +08:00
co-authored by Claude Opus 4.6
parent b380817940
commit 59093f8fd5
18 changed files with 308 additions and 36 deletions
+85 -1
View File
@@ -20,13 +20,14 @@ import { ElMessage } from 'element-plus'
import { computed, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { logoutAdmin } from '@/features/admin'
import { logoutAdmin, updateSupportStatus } from '@/features/admin'
import { useAdminSessionStore } from '@/stores/adminSession'
const router = useRouter()
const route = useRoute()
const adminSession = useAdminSessionStore()
const isCollapsed = ref(false)
const updatingStatus = ref(false)
interface NavItem {
label: string
@@ -60,6 +61,34 @@ const navItems = computed(() => {
const activeMenu = computed(() => route.path)
const adminName = computed(() => adminSession.nickname || adminSession.username || '管理员')
const supportStatus = computed(() => adminSession.supportStatus || 'offline')
const hasChatPermission = computed(() => adminSession.hasPermission('chat:view'))
const statusLabels: Record<string, string> = {
online: '在线',
offline: '离线',
busy: '忙碌',
}
const statusColors: Record<string, string> = {
online: '#10b981',
offline: '#9ca3af',
busy: '#f59e0b',
}
async function handleStatusChange(status: 'online' | 'offline' | 'busy') {
if (updatingStatus.value) return
updatingStatus.value = true
try {
await updateSupportStatus(status)
adminSession.setSupportStatus(status)
ElMessage.success(`已切换为${statusLabels[status]}`)
} catch {
ElMessage.error('状态更新失败')
} finally {
updatingStatus.value = false
}
}
async function handleLogout() {
try {
@@ -110,6 +139,28 @@ async function handleLogout() {
</el-breadcrumb>
</div>
<div class="topbar-right">
<el-dropdown v-if="hasChatPermission" trigger="click" @command="handleStatusChange" :disabled="updatingStatus">
<span class="status-badge" :style="{ borderColor: statusColors[supportStatus] }">
<span class="status-dot" :style="{ backgroundColor: statusColors[supportStatus] }"></span>
<span class="status-text">{{ statusLabels[supportStatus] }}</span>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="online">
<span class="status-dot" style="background-color: #10b981;"></span>
在线
</el-dropdown-item>
<el-dropdown-item command="busy">
<span class="status-dot" style="background-color: #f59e0b;"></span>
忙碌
</el-dropdown-item>
<el-dropdown-item command="offline">
<span class="status-dot" style="background-color: #9ca3af;"></span>
离线
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dropdown trigger="click">
<span class="user-info">
<el-avatar :size="32" :icon="User" />
@@ -256,6 +307,39 @@ async function handleLogout() {
.topbar-right {
display: flex;
align-items: center;
gap: 16px;
}
.status-badge {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border: 1px solid;
border-radius: 16px;
cursor: pointer;
transition: all 0.2s;
}
.status-badge:hover {
background-color: #f5f7fa;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
display: inline-block;
}
.status-text {
font-size: 13px;
color: #606266;
font-weight: 500;
}
:deep(.el-dropdown-menu__item .status-dot) {
margin-right: 8px;
}
.user-info {