支持可配置的会话自动分配策略
- 租户可设置负载最低或轮询,以及每位坐席最大进行中会话数 - 自动分配按策略过滤在线一线客服,满并发则保持排队 - 系统设置「客服分配规则」页可保存上述配置
This commit is contained in:
@@ -55,7 +55,7 @@ const tabItems: { key: string; label: string; desc: string; icon: ReactNode }[]
|
||||
{ key: 'basic', label: '基本设置', desc: '租户展示名称、默认昵称与时区', icon: <SettingOutlined /> },
|
||||
{ key: 'channels', label: '渠道管理', desc: '配置客户服务接入渠道与嵌入代码', icon: <ApiOutlined /> },
|
||||
{ key: 'staff', label: '坐席账号', desc: '管理客服/主管账号与坐席配额', icon: <UserSwitchOutlined /> },
|
||||
{ key: 'assignment', label: '客服分配规则', desc: '会话自动分配策略说明', icon: <TeamOutlined /> },
|
||||
{ key: 'assignment', label: '客服分配规则', desc: '自动分配策略与并发上限', icon: <TeamOutlined /> },
|
||||
{ key: 'autoreply', label: '自动回复', desc: '欢迎语与离线留言提示', icon: <MessageOutlined /> },
|
||||
{ key: 'worktime', label: '工作时间', desc: '在线服务时段与非工作时间提示', icon: <ClockCircleOutlined /> },
|
||||
{ key: 'notify', label: '通知设置', desc: '新会话、留言与日报偏好', icon: <BellOutlined /> },
|
||||
@@ -71,6 +71,7 @@ const Settings = () => {
|
||||
const [autoReplyForm] = Form.useForm()
|
||||
const [workForm] = Form.useForm()
|
||||
const [notifyForm] = Form.useForm()
|
||||
const [assignForm] = Form.useForm()
|
||||
const [staffForm] = Form.useForm()
|
||||
const [channels, setChannels] = useState<Channel[]>([])
|
||||
const [settings, setSettings] = useState<TenantSettings | null>(null)
|
||||
@@ -142,6 +143,10 @@ const Settings = () => {
|
||||
notify_offline_leave: data.notify_offline_leave,
|
||||
notify_daily_report: data.notify_daily_report,
|
||||
})
|
||||
assignForm.setFieldsValue({
|
||||
assign_strategy: data.assign_strategy || 'least_load',
|
||||
max_active_per_agent: data.max_active_per_agent ?? 0,
|
||||
})
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '加载设置失败')
|
||||
} finally {
|
||||
@@ -151,7 +156,7 @@ const Settings = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'channels') loadChannels()
|
||||
if (['basic', 'autoreply', 'worktime', 'notify'].includes(activeTab)) loadSettings()
|
||||
if (['basic', 'autoreply', 'worktime', 'notify', 'assignment'].includes(activeTab)) loadSettings()
|
||||
if (activeTab === 'staff' && canViewStaff) loadStaff()
|
||||
}, [activeTab])
|
||||
|
||||
@@ -504,29 +509,89 @@ const Settings = () => {
|
||||
)}
|
||||
|
||||
{activeTab === 'assignment' && (
|
||||
<div className="space-y-3">
|
||||
<div className="bg-white rounded-xl border border-[#dbeafe] shadow-sm p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-[#eff6ff] text-[#2563eb] flex items-center justify-center shrink-0">
|
||||
<TeamOutlined />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-sm font-medium text-neutral-800">当前生效:负载最低优先 + 工作时间</div>
|
||||
<div className="text-xs text-neutral-500 mt-1.5 leading-relaxed">
|
||||
工作时间内,新会话自动分配给进行中会话最少的在线客服;非工作时间或无客服在线时,访客可提交离线留言。
|
||||
<div className="space-y-4">
|
||||
{loadingSettings ? (
|
||||
<div className="py-16 text-center"><Spin /></div>
|
||||
) : (
|
||||
<>
|
||||
<div className="bg-white rounded-xl border border-[#dbeafe] shadow-sm p-5">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-9 h-9 rounded-lg bg-[#eff6ff] text-[#2563eb] flex items-center justify-center shrink-0">
|
||||
<TeamOutlined />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-neutral-800">自动分配规则</div>
|
||||
<div className="text-xs text-neutral-500 mt-1.5 leading-relaxed">
|
||||
工作时间内,访客进线会按下列策略分配给<strong className="font-medium text-neutral-700">在线一线客服</strong>;
|
||||
非工作时间或无可接坐席时进入排队/留言。人工领取与转接不受此策略限制。
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-neutral-200 p-5 opacity-70">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="text-sm font-medium text-neutral-800">轮询分配</div>
|
||||
<div className="text-xs text-neutral-400 mt-1">后续版本可切换策略</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-neutral-200 shadow-sm p-5">
|
||||
<Form
|
||||
form={assignForm}
|
||||
layout="vertical"
|
||||
disabled={!isAdmin}
|
||||
onFinish={async (values: {
|
||||
assign_strategy: 'least_load' | 'round_robin'
|
||||
max_active_per_agent: number
|
||||
}) => {
|
||||
await savePartial({
|
||||
assign_strategy: values.assign_strategy,
|
||||
max_active_per_agent: Number(values.max_active_per_agent) || 0,
|
||||
}, '分配规则已保存')
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="assign_strategy"
|
||||
label="分配策略"
|
||||
rules={[{ required: true, message: '请选择策略' }]}
|
||||
extra="负载最低:优先进行中会话更少的坐席;轮询:在可接坐席中依次分配,更均匀。"
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'least_load', label: '负载最低(推荐)' },
|
||||
{ value: 'round_robin', label: '轮询分配' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="max_active_per_agent"
|
||||
label="每位坐席最大进行中会话数"
|
||||
rules={[
|
||||
{ required: true, message: '请填写上限' },
|
||||
{
|
||||
validator: async (_, v) => {
|
||||
const n = Number(v)
|
||||
if (!Number.isFinite(n) || n < 0 || n > 200) {
|
||||
throw new Error('请填写 0–200 的整数,0 表示不限制')
|
||||
}
|
||||
},
|
||||
},
|
||||
]}
|
||||
extra="达到上限的坐席不再自动分入新会话;全部坐席已满时会话保持排队。0 = 不限制。"
|
||||
>
|
||||
<Input type="number" min={0} max={200} placeholder="0 表示不限制" />
|
||||
</Form.Item>
|
||||
{isAdmin ? (
|
||||
<Button type="primary" htmlType="submit" loading={saving}>
|
||||
保存分配规则
|
||||
</Button>
|
||||
) : (
|
||||
<p className="text-xs text-neutral-400 m-0">仅租户管理员可修改分配规则</p>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
<Switch disabled />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-neutral-50 rounded-xl border border-neutral-200 p-4 text-xs text-neutral-500 leading-relaxed space-y-1">
|
||||
<div className="font-medium text-neutral-700 mb-1">说明</div>
|
||||
<p className="m-0">· 候选范围:角色为客服、状态为「在线」的账号(忙碌/离线不参与自动分配)。</p>
|
||||
<p className="m-0">· 技能组 / 渠道专属坐席将在后续版本支持。</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -318,6 +318,8 @@ export const deleteStaff = (id: number) => del(`/staff/${id}`)
|
||||
|
||||
// Tenant settings
|
||||
export type WorkHours = Record<string, string>
|
||||
export type AssignStrategy = 'least_load' | 'round_robin'
|
||||
|
||||
export interface TenantSettings {
|
||||
tenant_id: number
|
||||
display_name: string
|
||||
@@ -330,6 +332,10 @@ export interface TenantSettings {
|
||||
notify_new_session: boolean
|
||||
notify_offline_leave: boolean
|
||||
notify_daily_report: boolean
|
||||
/** least_load 负载最低 | round_robin 轮询 */
|
||||
assign_strategy?: AssignStrategy
|
||||
/** 每位坐席最大进行中会话数,0=不限制 */
|
||||
max_active_per_agent?: number
|
||||
}
|
||||
export const getTenantSettings = () => get<TenantSettings>('/settings')
|
||||
export const updateTenantSettings = (data: Partial<TenantSettings> & { work_hours?: WorkHours }) =>
|
||||
|
||||
Reference in New Issue
Block a user