增加客服分组管理
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
import { apiClient } from '@/shared/api/client'
|
||||
|
||||
import type { ApiResponse } from '@/shared/types/types'
|
||||
|
||||
export interface SupportGroupMember {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string
|
||||
support_status: string
|
||||
}
|
||||
|
||||
export interface SupportGroup {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
description: string
|
||||
status: string
|
||||
sort_order: number
|
||||
members: SupportGroupMember[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface SaveSupportGroupRequest {
|
||||
code?: string
|
||||
name: string
|
||||
description?: string
|
||||
status?: string
|
||||
sort_order?: number
|
||||
member_ids: number[]
|
||||
}
|
||||
|
||||
export async function fetchSupportGroups() {
|
||||
const { data } = await apiClient.get<ApiResponse<SupportGroup[]>>('/admin/chat-support-groups')
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function fetchSupportGroupAdmins() {
|
||||
const { data } = await apiClient.get<ApiResponse<SupportGroupMember[]>>(
|
||||
'/admin/chat-support-groups/support-admins'
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function createSupportGroup(req: SaveSupportGroupRequest) {
|
||||
const { data } = await apiClient.post<ApiResponse<SupportGroup>>(
|
||||
'/admin/chat-support-groups',
|
||||
req
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function updateSupportGroup(id: number, req: SaveSupportGroupRequest) {
|
||||
const { data } = await apiClient.put<ApiResponse<SupportGroup>>(
|
||||
`/admin/chat-support-groups/${id}`,
|
||||
req
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function deleteSupportGroup(id: number) {
|
||||
const { data } = await apiClient.delete<ApiResponse<{ deleted: boolean }>>(
|
||||
`/admin/chat-support-groups/${id}`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
@@ -7,6 +7,7 @@ export * from './api/adminPayments'
|
||||
export * from './api/adminFinance'
|
||||
export * from './api/adminAudit'
|
||||
export * from './api/systemConfigs'
|
||||
export * from './api/supportGroups'
|
||||
export * from './composables/useAdminTable'
|
||||
export * from './composables/useAdminPaginatedTable'
|
||||
|
||||
|
||||
@@ -0,0 +1,272 @@
|
||||
<script setup lang="ts">
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
import {
|
||||
createSupportGroup,
|
||||
deleteSupportGroup,
|
||||
fetchSupportGroupAdmins,
|
||||
fetchSupportGroups,
|
||||
updateSupportGroup,
|
||||
type SaveSupportGroupRequest,
|
||||
type SupportGroup,
|
||||
type SupportGroupMember,
|
||||
} from '@/features/admin/api/supportGroups'
|
||||
import { readError } from '@/shared/utils/error'
|
||||
|
||||
const protectedCodes = new Set(['owner_onboarding', 'renter_handoff'])
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const groups = ref<SupportGroup[]>([])
|
||||
const supportAdmins = ref<SupportGroupMember[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const editingGroup = ref<SupportGroup | null>(null)
|
||||
const form = reactive<SaveSupportGroupRequest>({
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
status: 'active',
|
||||
sort_order: 0,
|
||||
member_ids: [],
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
void Promise.all([loadGroups(), loadSupportAdmins()])
|
||||
})
|
||||
|
||||
async function loadGroups() {
|
||||
loading.value = true
|
||||
try {
|
||||
groups.value = await fetchSupportGroups()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '客服分组加载失败'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSupportAdmins() {
|
||||
try {
|
||||
supportAdmins.value = await fetchSupportGroupAdmins()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '客服列表加载失败'))
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingGroup.value = null
|
||||
Object.assign(form, {
|
||||
code: '',
|
||||
name: '',
|
||||
description: '',
|
||||
status: 'active',
|
||||
sort_order: 0,
|
||||
member_ids: [],
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function openEdit(row: SupportGroup) {
|
||||
editingGroup.value = row
|
||||
Object.assign(form, {
|
||||
code: row.code,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
status: row.status,
|
||||
sort_order: row.sort_order,
|
||||
member_ids: row.members.map(item => item.id),
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
async function saveGroup() {
|
||||
if (!form.name.trim()) {
|
||||
ElMessage.warning('请填写分组名称')
|
||||
return
|
||||
}
|
||||
saving.value = true
|
||||
try {
|
||||
const payload = {
|
||||
...form,
|
||||
name: form.name.trim(),
|
||||
description: form.description?.trim(),
|
||||
member_ids: form.member_ids || [],
|
||||
}
|
||||
if (editingGroup.value) {
|
||||
await updateSupportGroup(editingGroup.value.id, payload)
|
||||
ElMessage.success('客服分组已更新')
|
||||
} else {
|
||||
await createSupportGroup(payload)
|
||||
ElMessage.success('客服分组已创建')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
await loadGroups()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '保存失败'))
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(row: SupportGroup) {
|
||||
try {
|
||||
await ElMessageBox.confirm(`确定删除客服分组「${row.name}」吗?`, '删除确认', {
|
||||
confirmButtonText: '确认删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
await deleteSupportGroup(row.id)
|
||||
ElMessage.success('客服分组已删除')
|
||||
await loadGroups()
|
||||
} catch {
|
||||
// 用户取消或接口已提示
|
||||
}
|
||||
}
|
||||
|
||||
function statusLabel(status: string) {
|
||||
return status === 'disabled' ? '停用' : '启用'
|
||||
}
|
||||
|
||||
function supportName(item: SupportGroupMember) {
|
||||
return item.nickname || item.username
|
||||
}
|
||||
|
||||
function supportStatusLabel(status: string) {
|
||||
const map: Record<string, string> = {
|
||||
online: '在线',
|
||||
busy: '忙碌',
|
||||
offline: '离线',
|
||||
}
|
||||
return map[status] || status
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page">
|
||||
<div class="page-header-row">
|
||||
<div class="page-header">
|
||||
<p class="eyebrow">Support Groups</p>
|
||||
<h1>客服分组</h1>
|
||||
<p>配置收号、卖号等业务阶段的客服成员,系统会按分组自动分配群聊客服。</p>
|
||||
</div>
|
||||
<div class="toolbar-actions">
|
||||
<el-button @click="loadGroups">刷新</el-button>
|
||||
<el-button type="primary" @click="openCreate">新建分组</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" class="table-panel" :data="groups">
|
||||
<el-table-column prop="name" label="分组名称" min-width="150" />
|
||||
<el-table-column prop="code" label="业务编码" min-width="150" />
|
||||
<el-table-column prop="description" label="说明" min-width="240" />
|
||||
<el-table-column label="成员" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="member-tags">
|
||||
<el-tag v-for="member in row.members" :key="member.id" size="small">
|
||||
{{ supportName(member) }}
|
||||
</el-tag>
|
||||
<span v-if="!row.members?.length" class="muted">未配置</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'disabled' ? 'info' : 'success'" size="small">
|
||||
{{ statusLabel(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort_order" label="排序" width="80" align="center" />
|
||||
<el-table-column label="操作" width="170" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button
|
||||
v-if="!protectedCodes.has(row.code)"
|
||||
size="small"
|
||||
type="danger"
|
||||
@click="handleDelete(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-dialog
|
||||
v-model="dialogVisible"
|
||||
:title="editingGroup ? '编辑客服分组' : '新建客服分组'"
|
||||
width="560px"
|
||||
>
|
||||
<el-form label-position="top">
|
||||
<el-form-item label="分组名称">
|
||||
<el-input v-model="form.name" placeholder="例如:收号组-客服" />
|
||||
</el-form-item>
|
||||
<el-form-item label="业务编码">
|
||||
<el-input
|
||||
v-model="form.code"
|
||||
:disabled="Boolean(editingGroup)"
|
||||
placeholder="留空自动生成;默认分组编码不可修改"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="说明">
|
||||
<el-input v-model="form.description" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
<div class="form-grid">
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status">
|
||||
<el-option label="启用" value="active" />
|
||||
<el-option label="停用" value="disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="排序">
|
||||
<el-input-number v-model="form.sort_order" :min="0" :step="10" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item label="客服成员">
|
||||
<el-select
|
||||
v-model="form.member_ids"
|
||||
multiple
|
||||
filterable
|
||||
collapse-tags
|
||||
collapse-tags-tooltip
|
||||
placeholder="选择客服人员"
|
||||
>
|
||||
<el-option
|
||||
v-for="admin in supportAdmins"
|
||||
:key="admin.id"
|
||||
:label="`${supportName(admin)}(${supportStatusLabel(admin.support_status)})`"
|
||||
:value="admin.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveGroup">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar-actions,
|
||||
.member-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #8f9bba;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
:deep(.el-select) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user