客服可以给会话设置个人备注, 设置 快捷回复

建群自动话术
This commit is contained in:
yml2213
2026-05-27 06:43:24 +08:00
parent a002987784
commit 138f0514a9
15 changed files with 1462 additions and 34 deletions
+4
View File
@@ -18,6 +18,9 @@ declare module 'vue' {
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -26,6 +29,7 @@ declare module 'vue' {
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSelect: typeof import('element-plus/es')['ElSelect']
+69 -2
View File
@@ -7,6 +7,7 @@ export interface ChatParticipant {
participant_type: 'user' | 'admin'
participant_id: number
role: 'renter' | 'owner' | 'support'
remark: string
display_name: string
avatar_url: string
last_read_at?: string
@@ -78,9 +79,9 @@ export async function markChatRead(id: number) {
return data.data
}
export async function fetchAdminChats(page = 1, pageSize = 50) {
export async function fetchAdminChats(page = 1, pageSize = 50, filter = 'all') {
const { data } = await apiClient.get<ApiResponse<PaginatedResult<ChatConversation>>>('/admin/chats', {
params: { page, page_size: pageSize },
params: { page, page_size: pageSize, filter },
})
return data.data
}
@@ -106,3 +107,69 @@ export async function markAdminChatRead(id: number) {
const { data } = await apiClient.post<ApiResponse<{ read: boolean }>>(`/admin/chats/${id}/read`)
return data.data
}
export interface SupportAdmin {
id: number
nickname: string
chat_count: number
}
export async function fetchSupportAdmins() {
const { data } = await apiClient.get<ApiResponse<SupportAdmin[]>>('/admin/chats/support-admins')
return data.data
}
export async function transferChat(id: number, toAdminId: number) {
const { data } = await apiClient.post<ApiResponse<{ transferred: boolean }>>(`/admin/chats/${id}/transfer`, {
to_admin_id: toAdminId,
})
return data.data
}
export async function updateChatRemark(id: number, remark: string) {
const { data } = await apiClient.put<ApiResponse<{ updated: boolean }>>(`/admin/chats/${id}/remark`, { remark })
return data.data
}
export interface QuickReply {
id: number
admin_user_id: number
title: string
content: string
sort_order: number
is_global: boolean
}
export async function fetchQuickReplies() {
const { data } = await apiClient.get<ApiResponse<QuickReply[]>>('/admin/chats/quick-replies')
return data.data
}
export async function createQuickReply(title: string, content: string, sortOrder = 0) {
const { data } = await apiClient.post<ApiResponse<QuickReply>>('/admin/chats/quick-replies', {
title,
content,
sort_order: sortOrder,
})
return data.data
}
export async function updateQuickReply(id: number, updates: { title?: string; content?: string; sort_order?: number }) {
const { data } = await apiClient.put<ApiResponse<{ updated: boolean }>>(`/admin/chats/quick-replies/${id}`, updates)
return data.data
}
export async function deleteQuickReply(id: number) {
const { data } = await apiClient.delete<ApiResponse<{ deleted: boolean }>>(`/admin/chats/quick-replies/${id}`)
return data.data
}
export async function fetchAutoWelcomeMessage() {
const { data } = await apiClient.get<ApiResponse<{ message: string }>>('/admin/chats/auto-welcome')
return data.data.message
}
export async function updateAutoWelcomeMessage(message: string) {
const { data } = await apiClient.put<ApiResponse<{ updated: boolean }>>('/admin/chats/auto-welcome', { message })
return data.data
}
+236 -30
View File
@@ -5,13 +5,18 @@ import {
fetchAdminChat,
fetchAdminChatMessages,
fetchAdminChats,
fetchQuickReplies,
markAdminChatRead,
sendAdminChatMessage,
updateChatRemark,
type ChatConversation,
type ChatMessage,
type QuickReply,
} from '@/api/chats'
import { useChatSSE, type ChatEvent } from '@/composables/useChatSSE'
import { formatDateMinute } from '@/utils/time'
import TransferDialog from './components/TransferDialog.vue'
import QuickReplyDialog from './components/QuickReplyDialog.vue'
const currentAdminId = Number(localStorage.getItem('admin_id') || 0)
@@ -23,12 +28,30 @@ const messageLoading = ref(false)
const sending = ref(false)
const content = ref('')
const listRef = ref<HTMLElement | null>(null)
const filter = ref<'all' | 'mine' | 'unassigned'>('mine')
const transferVisible = ref(false)
const quickReplyVisible = ref(false)
const quickReplies = ref<QuickReply[]>([])
const remarkEditing = ref(false)
const remarkValue = ref('')
const activeMembers = computed(() => {
const participants = active.value?.participants || []
return participants.map(item => `${roleLabel(item.role)}${item.display_name}`).join(' / ')
return participants.map(item => {
const remark = getParticipantRemark(item)
const name = remark ? `${remark}(${item.display_name})` : item.display_name
return `${roleLabel(item.role)}${name}`
}).join(' / ')
})
function getParticipantRemark(participant: any) {
if (!active.value) return ''
const myParticipant = active.value.participants?.find(
p => p.participant_type === 'admin' && p.participant_id === currentAdminId
)
return myParticipant?.remark || ''
}
function handleSSEEvent(event: ChatEvent) {
if (event.type === 'conversation_updated') {
loadConversations(false)
@@ -59,13 +82,13 @@ const { onEvent } = useChatSSE('admin', '/api/admin/chats/events')
onEvent(handleSSEEvent)
onMounted(async () => {
await loadConversations()
await Promise.all([loadConversations(), loadQuickReplies()])
})
async function loadConversations(showLoading = true) {
if (showLoading) loading.value = true
try {
const res = await fetchAdminChats(1, 100)
const res = await fetchAdminChats(1, 100, filter.value)
conversations.value = res.items
const first = conversations.value[0]
if (!active.value && first) {
@@ -78,6 +101,12 @@ async function loadConversations(showLoading = true) {
}
}
async function loadQuickReplies() {
try {
quickReplies.value = await fetchQuickReplies()
} catch { /* ignore */ }
}
async function openConversation(item: ChatConversation) {
messageLoading.value = true
try {
@@ -85,6 +114,8 @@ async function openConversation(item: ChatConversation) {
await loadMessages(item.id)
await markAdminChatRead(item.id)
await loadConversations(false)
remarkEditing.value = false
remarkValue.value = ''
} catch {
ElMessage.error('会话详情加载失败')
} finally {
@@ -115,6 +146,49 @@ async function handleSend() {
}
}
function handleQuickReplySelect(reply: QuickReply) {
content.value = reply.content
quickReplyVisible.value = false
}
function handleFilterChange(val: string) {
filter.value = val as typeof filter.value
active.value = null
messages.value = []
loadConversations()
}
function handleTransferSuccess() {
loadConversations(false)
if (active.value) {
loadMessages(active.value.id, false)
}
}
async function startEditRemark() {
if (!active.value) return
const myParticipant = active.value.participants?.find(
p => p.participant_type === 'admin' && p.participant_id === currentAdminId
)
remarkValue.value = myParticipant?.remark || active.value.title
remarkEditing.value = true
}
async function saveRemark() {
if (!active.value) return
try {
await updateChatRemark(active.value.id, remarkValue.value)
ElMessage.success('备注已更新')
remarkEditing.value = false
await fetchAdminChat(active.value.id).then(chat => {
active.value = chat
})
await loadConversations(false)
} catch {
ElMessage.error('更新备注失败')
}
}
function scrollBottom() {
const el = listRef.value
if (!el) return
@@ -135,6 +209,18 @@ function senderLabel(item: ChatMessage) {
if (item.sender_type === 'system') return '系统'
return `${roleLabel(item.sender_role)} · ${item.sender_name}`
}
function getConversationTitle(item: ChatConversation) {
const myParticipant = item.participants?.find(
p => p.participant_type === 'admin' && p.participant_id === currentAdminId
)
return myParticipant?.remark || item.title
}
function getSupportName(item: ChatConversation) {
const support = item.participants?.find(p => p.role === 'support')
return support?.display_name || '未分配'
}
</script>
<template>
@@ -144,11 +230,21 @@ function senderLabel(item: ChatMessage) {
<h1>客服群聊</h1>
<p>处理订单三方沟通</p>
</div>
<el-button :loading="loading" @click="loadConversations()">刷新</el-button>
<div class="head-right">
<el-button @click="quickReplyVisible = true">快捷回复管理</el-button>
<el-button :loading="loading" @click="loadConversations()">刷新</el-button>
</div>
</div>
<div class="chat-workbench">
<aside class="conversation-pane" v-loading="loading">
<div class="filter-tabs">
<el-radio-group v-model="filter" size="small" @change="handleFilterChange">
<el-radio-button value="mine">我的会话</el-radio-button>
<el-radio-button value="all">全部</el-radio-button>
<el-radio-button value="unassigned">未分配</el-radio-button>
</el-radio-group>
</div>
<button
v-for="item in conversations"
:key="item.id"
@@ -158,11 +254,14 @@ function senderLabel(item: ChatMessage) {
@click="openConversation(item)"
>
<div class="row-title">
<strong>{{ item.title }}</strong>
<strong>{{ getConversationTitle(item) }}</strong>
<span>{{ formatDateMinute(item.last_message_at || item.created_at) }}</span>
</div>
<p>{{ item.last_message_preview || '订单群聊已创建' }}</p>
<em v-if="item.unread_count > 0">{{ item.unread_count }}</em>
<div class="row-meta">
<span class="support-name">{{ getSupportName(item) }}</span>
<em v-if="item.unread_count > 0">{{ item.unread_count }}</em>
</div>
</button>
<el-empty v-if="!loading && conversations.length === 0" description="暂无客服会话" />
</aside>
@@ -170,11 +269,29 @@ function senderLabel(item: ChatMessage) {
<main class="message-pane">
<template v-if="active">
<header class="message-head">
<div>
<h2>{{ active.title }}</h2>
<p>{{ activeMembers }}</p>
<div class="head-title">
<template v-if="remarkEditing">
<el-input
v-model="remarkValue"
size="small"
style="width: 200px"
placeholder="输入备注"
@keyup.enter="saveRemark"
/>
<el-button size="small" type="primary" @click="saveRemark">保存</el-button>
<el-button size="small" @click="remarkEditing = false">取消</el-button>
</template>
<template v-else>
<h2>{{ getConversationTitle(active) }} <el-button link size="small" @click="startEditRemark">编辑备注</el-button></h2>
<p>{{ activeMembers }}</p>
</template>
</div>
<div class="head-actions">
<el-button size="small" @click="transferVisible = true">转接</el-button>
<RouterLink :to="`/admin/orders/${active.order_id}`">
<el-button size="small">查看订单</el-button>
</RouterLink>
</div>
<RouterLink :to="`/admin/orders/${active.order_id}`">查看订单</RouterLink>
</header>
<div ref="listRef" class="message-list" v-loading="messageLoading">
@@ -195,21 +312,55 @@ function senderLabel(item: ChatMessage) {
</div>
<footer class="composer">
<el-input
v-model="content"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="输入客服回复"
@keydown.enter.exact.prevent="handleSend"
/>
<el-button type="primary" :loading="sending" :disabled="!content.trim()" @click="handleSend">发送</el-button>
<div class="composer-tools">
<el-dropdown trigger="click" @command="handleQuickReplySelect">
<el-button size="small" text>快捷回复</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
v-for="reply in quickReplies"
:key="reply.id"
:command="reply"
>
<span class="reply-title">{{ reply.title }}</span>
<span class="reply-preview">{{ reply.content.slice(0, 30) }}{{ reply.content.length > 30 ? '...' : '' }}</span>
</el-dropdown-item>
<el-dropdown-item v-if="quickReplies.length === 0" disabled>
暂无快捷回复
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="composer-input">
<el-input
v-model="content"
type="textarea"
:rows="3"
maxlength="1000"
show-word-limit
placeholder="输入客服回复"
@keydown.enter.exact.prevent="handleSend"
/>
<el-button type="primary" :loading="sending" :disabled="!content.trim()" @click="handleSend">发送</el-button>
</div>
</footer>
</template>
<el-empty v-else description="请选择会话" />
</main>
</div>
<TransferDialog
v-if="active"
v-model="transferVisible"
:conversation-id="active.id"
@success="handleTransferSuccess"
/>
<QuickReplyDialog
v-model="quickReplyVisible"
@success="loadQuickReplies"
/>
</section>
</template>
@@ -231,6 +382,11 @@ function senderLabel(item: ChatMessage) {
color: #6b7280;
}
.head-right {
display: flex;
gap: 8px;
}
.chat-workbench {
display: grid;
min-height: 640px;
@@ -247,6 +403,11 @@ function senderLabel(item: ChatMessage) {
background: #f8fafc;
}
.filter-tabs {
padding: 12px;
border-bottom: 1px solid #e5e7eb;
}
.conversation-row {
position: relative;
display: block;
@@ -292,10 +453,19 @@ function senderLabel(item: ChatMessage) {
white-space: nowrap;
}
.conversation-row em {
position: absolute;
right: 12px;
bottom: 12px;
.row-meta {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
}
.support-name {
color: #8a94a6;
font-size: 12px;
}
.row-meta em {
min-width: 18px;
height: 18px;
padding: 0 5px;
@@ -317,23 +487,41 @@ function senderLabel(item: ChatMessage) {
.message-head {
display: flex;
align-items: center;
align-items: flex-start;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #e5e7eb;
}
.message-head h2 {
margin: 0;
font-size: 18px;
.head-title {
flex: 1;
min-width: 0;
}
.message-head p {
.head-title h2 {
margin: 0;
font-size: 18px;
display: flex;
align-items: center;
gap: 8px;
}
.head-title p {
margin: 6px 0 0;
color: #6b7280;
font-size: 13px;
}
.head-actions {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.head-actions a {
text-decoration: none;
}
.message-list {
overflow-y: auto;
padding: 18px;
@@ -386,11 +574,29 @@ function senderLabel(item: ChatMessage) {
}
.composer {
border-top: 1px solid #e5e7eb;
}
.composer-tools {
padding: 8px 14px;
border-bottom: 1px solid #f0f0f0;
}
.composer-input {
display: grid;
grid-template-columns: minmax(0, 1fr) 88px;
gap: 12px;
align-items: end;
padding: 14px;
border-top: 1px solid #e5e7eb;
}
.reply-title {
font-weight: 500;
margin-right: 8px;
}
.reply-preview {
color: #9ca3af;
font-size: 12px;
}
</style>
@@ -25,6 +25,7 @@ import SalePriceDialog from './components/SalePriceDialog.vue'
import HomeAnnouncementsDialog from './components/HomeAnnouncementsDialog.vue'
import HomeBannersDialog from './components/HomeBannersDialog.vue'
import GeneralConfigDialog from './components/GeneralConfigDialog.vue'
import AutoWelcomeConfig from './components/AutoWelcomeConfig.vue'
const loading = ref(false)
const configs = ref<SystemConfig[]>([])
@@ -267,6 +268,8 @@ function formatConfigValue(row: SystemConfig) {
</div>
</section>
<AutoWelcomeConfig />
<el-table v-loading="loading" class="table-panel" :data="regularConfigs">
<el-table-column prop="key" label="配置项" min-width="260" />
<el-table-column label="当前值" min-width="180" show-overflow-tooltip>
@@ -0,0 +1,136 @@
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { fetchAutoWelcomeMessage, updateAutoWelcomeMessage } from '@/api/chats'
const loading = ref(false)
const saving = ref(false)
const message = ref('')
const editing = ref(false)
onMounted(async () => {
await loadMessage()
})
async function loadMessage() {
loading.value = true
try {
message.value = await fetchAutoWelcomeMessage()
} catch {
ElMessage.error('加载自动话术失败')
} finally {
loading.value = false
}
}
async function handleSave() {
if (!message.value.trim()) {
ElMessage.warning('话术内容不能为空')
return
}
saving.value = true
try {
await updateAutoWelcomeMessage(message.value.trim())
ElMessage.success('保存成功')
editing.value = false
} catch {
ElMessage.error('保存失败')
} finally {
saving.value = false
}
}
function startEdit() {
editing.value = true
}
function cancelEdit() {
editing.value = false
loadMessage()
}
</script>
<template>
<div class="auto-welcome-config" v-loading="loading">
<div class="config-header">
<h3>建群自动话术</h3>
<p>订单群聊创建后自动发送的欢迎消息</p>
</div>
<div class="config-content">
<template v-if="editing">
<el-input
v-model="message"
type="textarea"
:rows="4"
maxlength="500"
show-word-limit
placeholder="输入建群后自动发送的话术"
/>
<div class="config-actions">
<el-button size="small" @click="cancelEdit">取消</el-button>
<el-button size="small" type="primary" :loading="saving" @click="handleSave">
保存
</el-button>
</div>
</template>
<template v-else>
<div class="preview-box">
<p>{{ message || '未设置' }}</p>
</div>
<el-button size="small" @click="startEdit">编辑</el-button>
</template>
</div>
</div>
</template>
<style scoped>
.auto-welcome-config {
padding: 20px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
.config-header {
margin-bottom: 16px;
}
.config-header h3 {
margin: 0 0 4px;
font-size: 16px;
color: #111827;
}
.config-header p {
margin: 0;
color: #6b7280;
font-size: 13px;
}
.config-content {
display: flex;
flex-direction: column;
gap: 12px;
}
.config-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.preview-box {
padding: 12px;
background: #f8fafc;
border: 1px solid #e5e7eb;
border-radius: 6px;
min-height: 60px;
}
.preview-box p {
margin: 0;
color: #374151;
line-height: 1.5;
white-space: pre-wrap;
}
</style>
@@ -0,0 +1,253 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {
fetchQuickReplies,
createQuickReply,
updateQuickReply,
deleteQuickReply,
type QuickReply,
} from '@/api/chats'
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: []
}>()
const visible = ref(false)
const loading = ref(false)
const replies = ref<QuickReply[]>([])
const editingId = ref<number | null>(null)
const form = ref({
title: '',
content: '',
sort_order: 0,
})
watch(() => props.modelValue, (val) => {
visible.value = val
if (val) {
loadReplies()
}
})
watch(visible, (val) => {
emit('update:modelValue', val)
})
async function loadReplies() {
loading.value = true
try {
replies.value = await fetchQuickReplies()
} catch {
ElMessage.error('加载快捷回复失败')
} finally {
loading.value = false
}
}
function resetForm() {
form.value = { title: '', content: '', sort_order: 0 }
editingId.value = null
}
function startEdit(reply: QuickReply) {
editingId.value = reply.id
form.value = {
title: reply.title,
content: reply.content,
sort_order: reply.sort_order,
}
}
async function handleSubmit() {
if (!form.value.title || !form.value.content) {
ElMessage.warning('标题和内容不能为空')
return
}
try {
if (editingId.value) {
await updateQuickReply(editingId.value, form.value)
ElMessage.success('更新成功')
} else {
await createQuickReply(form.value.title, form.value.content, form.value.sort_order)
ElMessage.success('创建成功')
}
resetForm()
await loadReplies()
emit('success')
} catch {
ElMessage.error('操作失败')
}
}
async function handleDelete(reply: QuickReply) {
if (reply.is_global) {
ElMessage.warning('不能删除全局快捷回复')
return
}
try {
await ElMessageBox.confirm('确定删除这条快捷回复?', '确认删除', {
type: 'warning',
})
await deleteQuickReply(reply.id)
ElMessage.success('删除成功')
await loadReplies()
emit('success')
} catch { /* ignore */ }
}
function handleCancel() {
resetForm()
}
</script>
<template>
<el-dialog v-model="visible" title="快捷回复管理" width="600px">
<div class="quick-reply-content">
<div class="reply-form">
<el-input
v-model="form.title"
placeholder="快捷回复标题"
maxlength="64"
style="margin-bottom: 8px"
/>
<el-input
v-model="form.content"
type="textarea"
:rows="3"
placeholder="回复内容"
maxlength="500"
show-word-limit
style="margin-bottom: 8px"
/>
<div class="form-actions">
<el-input-number
v-model="form.sort_order"
:min="0"
:max="999"
size="small"
placeholder="排序"
style="width: 120px"
/>
<div>
<el-button v-if="editingId" size="small" @click="handleCancel">取消</el-button>
<el-button size="small" type="primary" @click="handleSubmit">
{{ editingId ? '更新' : '添加' }}
</el-button>
</div>
</div>
</div>
<div class="reply-list" v-loading="loading">
<div
v-for="reply in replies"
:key="reply.id"
class="reply-item"
:class="{ global: reply.is_global }"
>
<div class="reply-info">
<div class="reply-header">
<span class="reply-title">{{ reply.title }}</span>
<el-tag v-if="reply.is_global" size="small" type="info">全局</el-tag>
<el-tag v-else size="small" type="success">个人</el-tag>
</div>
<div class="reply-content">{{ reply.content }}</div>
</div>
<div class="reply-actions">
<el-button link size="small" @click="startEdit(reply)">编辑</el-button>
<el-button
v-if="!reply.is_global"
link
size="small"
type="danger"
@click="handleDelete(reply)"
>
删除
</el-button>
</div>
</div>
<el-empty v-if="!loading && replies.length === 0" description="暂无快捷回复" />
</div>
</div>
</el-dialog>
</template>
<style scoped>
.quick-reply-content {
display: flex;
flex-direction: column;
gap: 16px;
max-height: 60vh;
}
.reply-form {
padding: 16px;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e5e7eb;
}
.form-actions {
display: flex;
align-items: center;
justify-content: space-between;
}
.reply-list {
overflow-y: auto;
max-height: 400px;
}
.reply-item {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-bottom: 8px;
}
.reply-item.global {
background: #f0f9ff;
border-color: #bae6fd;
}
.reply-info {
flex: 1;
min-width: 0;
}
.reply-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.reply-title {
font-weight: 500;
color: #111827;
}
.reply-content {
color: #6b7280;
font-size: 13px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.reply-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
margin-left: 12px;
}
</style>
@@ -0,0 +1,131 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { fetchSupportAdmins, transferChat, type SupportAdmin } from '@/api/chats'
const props = defineProps<{
modelValue: boolean
conversationId: number
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: []
}>()
const visible = ref(false)
const loading = ref(false)
const submitting = ref(false)
const admins = ref<SupportAdmin[]>([])
const selectedAdminId = ref<number | null>(null)
watch(() => props.modelValue, (val) => {
visible.value = val
if (val) {
loadAdmins()
}
})
watch(visible, (val) => {
emit('update:modelValue', val)
})
async function loadAdmins() {
loading.value = true
try {
admins.value = await fetchSupportAdmins()
} catch {
ElMessage.error('加载客服列表失败')
} finally {
loading.value = false
}
}
async function handleSubmit() {
if (!selectedAdminId.value) {
ElMessage.warning('请选择目标客服')
return
}
submitting.value = true
try {
await transferChat(props.conversationId, selectedAdminId.value)
ElMessage.success('转接成功')
visible.value = false
emit('success')
} catch {
ElMessage.error('转接失败')
} finally {
submitting.value = false
}
}
</script>
<template>
<el-dialog v-model="visible" title="转接会话" width="400px">
<div v-loading="loading" class="transfer-content">
<p class="tip">选择要转接给的客服</p>
<el-radio-group v-model="selectedAdminId" class="admin-list">
<el-radio
v-for="admin in admins"
:key="admin.id"
:value="admin.id"
class="admin-item"
>
<span class="admin-name">{{ admin.nickname }}</span>
<span class="admin-count">当前 {{ admin.chat_count }} 个会话</span>
</el-radio>
</el-radio-group>
<el-empty v-if="!loading && admins.length === 0" description="暂无可用客服" />
</div>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" :disabled="!selectedAdminId" @click="handleSubmit">
确认转接
</el-button>
</template>
</el-dialog>
</template>
<style scoped>
.transfer-content {
min-height: 100px;
}
.tip {
margin: 0 0 16px;
color: #6b7280;
font-size: 14px;
}
.admin-list {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.admin-item {
display: flex;
align-items: center;
justify-content: space-between;
height: auto;
padding: 12px;
border: 1px solid #e5e7eb;
border-radius: 8px;
margin-right: 0;
}
.admin-item.is-checked {
border-color: #409eff;
background: #ecf5ff;
}
.admin-name {
font-weight: 500;
}
.admin-count {
color: #9ca3af;
font-size: 13px;
}
</style>