- Dashboard 2591→1944 行,抽出 dashboard/ 纯函数、客户资料字段与 5 个状态自持弹窗 - Settings 1602→478 行,抽出 settings/ 16 个分区模块(设置卡片、弹窗与工具函数) - VisitorChat 1343→1045 行,抽出 visitor/ 头部、消息区、输入区与评价浮层 - 均为纯代码搬移:行为、路由入口与导出不变;tsc -b 与 vite build 通过
114 lines
5.1 KiB
TypeScript
114 lines
5.1 KiB
TypeScript
import { Button, Form, Input, Select, Spin } from 'antd'
|
||
import { TeamOutlined } from '@ant-design/icons'
|
||
import type { FormInstance } from 'antd'
|
||
import type { SaveSettingsFn } from './types'
|
||
|
||
interface AssignmentSectionProps {
|
||
assignForm: FormInstance
|
||
loading: boolean
|
||
isAdmin: boolean
|
||
saving: boolean
|
||
onSave: SaveSettingsFn
|
||
}
|
||
|
||
/** 客服分配规则:自动分配策略与并发上限 */
|
||
const AssignmentSection = ({ assignForm, loading, isAdmin, saving, onSave }: AssignmentSectionProps) => {
|
||
return (
|
||
<div className="space-y-5">
|
||
{loading ? (
|
||
<div className="py-16 text-center"><Spin /></div>
|
||
) : (
|
||
<>
|
||
<div className="bg-blue-50/50 rounded-2xl border border-blue-100 p-5">
|
||
<div className="flex items-start gap-3.5">
|
||
<div className="w-10 h-10 rounded-xl bg-blue-100 text-blue-600 flex items-center justify-center shrink-0 text-lg">
|
||
<TeamOutlined />
|
||
</div>
|
||
<div className="min-w-0">
|
||
<div className="text-sm font-bold text-slate-900">自动分配规则</div>
|
||
<div className="text-xs text-slate-600 mt-1 leading-relaxed">
|
||
工作时间内,访客进线会按下列策略自动分配给<strong className="font-semibold text-blue-700">在线一线客服</strong>;
|
||
非工作时间或无可接坐席时进入排队或离线留言。人工领取与内部转接不受此策略限制。
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="bg-white rounded-2xl border border-slate-200/80 shadow-xs p-6 md:p-8">
|
||
<Form
|
||
form={assignForm}
|
||
layout="vertical"
|
||
disabled={!isAdmin}
|
||
onFinish={async (values: {
|
||
assign_strategy: 'least_load' | 'round_robin'
|
||
max_active_per_agent: number
|
||
}) => {
|
||
await onSave({
|
||
assign_strategy: values.assign_strategy,
|
||
max_active_per_agent: Number(values.max_active_per_agent) || 0,
|
||
}, '分配规则已保存')
|
||
}}
|
||
>
|
||
<Form.Item
|
||
name="assign_strategy"
|
||
label={<span className="text-xs font-semibold text-slate-700">分配策略</span>}
|
||
rules={[{ required: true, message: '请选择策略' }]}
|
||
extra={<span className="text-[11px] text-slate-400">负载最低:优先分配给进行中会话最少的坐席;轮询:在可接坐席中均匀依次分发。</span>}
|
||
>
|
||
<Select
|
||
className="!rounded-lg"
|
||
options={[
|
||
{ value: 'least_load', label: '负载最低策略(推荐)' },
|
||
{ value: 'round_robin', label: '轮询轮转分配策略' },
|
||
]}
|
||
/>
|
||
</Form.Item>
|
||
<Form.Item
|
||
name="max_active_per_agent"
|
||
label={<span className="text-xs font-semibold text-slate-700">每位坐席最大并发会话数</span>}
|
||
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={<span className="text-[11px] text-slate-400">达到上限后不再自动接入新访客,全部坐席满载时访客将进入等待队列。0 表示不限上限。</span>}
|
||
>
|
||
<Input type="number" min={0} max={200} placeholder="0 表示不限制" className="!rounded-lg" />
|
||
</Form.Item>
|
||
|
||
<div className="pt-4 border-t border-slate-100 flex items-center justify-between">
|
||
{isAdmin ? (
|
||
<Button
|
||
type="primary"
|
||
htmlType="submit"
|
||
loading={saving}
|
||
className="!h-9 !px-6 !rounded-xl !text-xs !font-semibold !bg-gradient-to-r !from-blue-600 !to-blue-500 shadow-sm shadow-blue-500/20"
|
||
>
|
||
保存分配规则
|
||
</Button>
|
||
) : (
|
||
<p className="text-xs text-slate-400 m-0">仅租户管理员可修改分配规则</p>
|
||
)}
|
||
</div>
|
||
</Form>
|
||
</div>
|
||
|
||
<div className="bg-slate-50/80 rounded-2xl border border-slate-200/80 p-4 text-xs text-slate-500 leading-relaxed space-y-1">
|
||
<div className="font-bold text-slate-700 mb-1">使用说明</div>
|
||
<p className="m-0">· 自动分发候选范围:角色为客服、当前状态为「在线」的账号(忙碌/离线状态不参与自动分发)。</p>
|
||
<p className="m-0">· 专属坐席组与渠道绑定策略将在后续版本逐步上线。</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export default AssignmentSection
|