优化系统设置页布局并对齐顶栏

左右栏固定 56px 顶栏,设置分类导航与各配置面板视觉对齐效果图,保留渠道与租户设置能力。
This commit is contained in:
yml2213
2026-07-15 13:31:36 +08:00
parent 5257165c78
commit f6f7fd478e
+388 -319
View File
@@ -1,22 +1,24 @@
import { useEffect, useState } from 'react'
import { Form, Input, Switch, Select, Button, Card, Tag, message, Spin, Empty } from 'antd'
import { useEffect, useState, type ReactNode } from 'react'
import { Form, Input, Switch, Select, Button, message, Spin, Empty } from 'antd'
import {
LinkOutlined, WechatOutlined, PhoneOutlined, MailOutlined, MobileOutlined, CopyOutlined, PlusOutlined,
LinkOutlined, WechatOutlined, PhoneOutlined, MailOutlined, MobileOutlined, CopyOutlined,
PlusOutlined, SettingOutlined, ApiOutlined, TeamOutlined, SafetyCertificateOutlined,
MessageOutlined, ClockCircleOutlined, BellOutlined, GlobalOutlined,
} from '@ant-design/icons'
import {
createChannel, getChannels, getTenantSettings, updateChannel, updateTenantSettings,
type Channel, type TenantSettings, type WorkHours,
} from '@/services/api'
const typeMeta: Record<string, { icon: React.ReactNode; label: string }> = {
web: { icon: <LinkOutlined />, label: '网页聊天' },
wechat: { icon: <WechatOutlined />, label: '微信公众号' },
app: { icon: <MobileOutlined />, label: 'APP 内嵌' },
phone: { icon: <PhoneOutlined />, label: '电话客服' },
email: { icon: <MailOutlined />, label: '邮件工单' },
const typeMeta: Record<string, { icon: ReactNode; label: string; desc: string }> = {
web: { icon: <GlobalOutlined />, label: '网页聊天', desc: '嵌入官网或任意网页的在线客服窗口' },
wechat: { icon: <WechatOutlined />, label: '微信公众号', desc: '对接微信公众号客服消息(预留)' },
app: { icon: <MobileOutlined />, label: 'APP 内嵌', desc: '移动端 App 内嵌客服(预留)' },
phone: { icon: <PhoneOutlined />, label: '电话客服', desc: '电话进线与回拨(预留)' },
email: { icon: <MailOutlined />, label: '邮件工单', desc: '邮件自动创建工单(预留)' },
}
const weekDays: { key: keyof WorkHours | string; label: string }[] = [
const weekDays: { key: string; label: string }[] = [
{ key: 'monday', label: '周一' },
{ key: 'tuesday', label: '周二' },
{ key: 'wednesday', label: '周三' },
@@ -33,6 +35,16 @@ const hourOptions = [
{ value: '全天', label: '全天' },
]
const tabItems: { key: string; label: string; desc: string; icon: ReactNode }[] = [
{ key: 'basic', label: '基本设置', desc: '租户展示名称、默认昵称与时区', icon: <SettingOutlined /> },
{ key: 'channels', label: '渠道管理', desc: '配置客户服务接入渠道与嵌入代码', icon: <ApiOutlined /> },
{ key: 'assignment', label: '客服分配规则', desc: '会话自动分配策略说明', icon: <TeamOutlined /> },
{ key: 'permission', label: '权限管理', desc: '角色与能力说明', icon: <SafetyCertificateOutlined /> },
{ key: 'autoreply', label: '自动回复', desc: '欢迎语与离线留言提示', icon: <MessageOutlined /> },
{ key: 'worktime', label: '工作时间', desc: '在线服务时段与非工作时间提示', icon: <ClockCircleOutlined /> },
{ key: 'notify', label: '通知设置', desc: '新会话、留言与日报偏好', icon: <BellOutlined /> },
]
const Settings = () => {
const [activeTab, setActiveTab] = useState('basic')
const [basicForm] = Form.useForm()
@@ -46,15 +58,7 @@ const Settings = () => {
const [saving, setSaving] = useState(false)
const [togglingId, setTogglingId] = useState<number | null>(null)
const tabItems = [
{ key: 'basic', label: '基本设置' },
{ key: 'channels', label: '渠道管理' },
{ key: 'assignment', label: '分配规则' },
{ key: 'permission', label: '权限管理' },
{ key: 'autoreply', label: '自动回复' },
{ key: 'worktime', label: '工作时间' },
{ key: 'notify', label: '通知设置' },
]
const currentTab = tabItems.find(t => t.key === activeTab) || tabItems[0]
const loadChannels = async () => {
setLoadingChannels(true)
@@ -136,7 +140,7 @@ const Settings = () => {
setTogglingId(ch.id)
try {
const res = await updateChannel(ch.id, { status: enabled ? 'enabled' : 'disabled' })
setChannels(prev => prev.map(c => c.id === ch.id ? res.data : c))
setChannels(prev => prev.map(c => (c.id === ch.id ? res.data : c)))
message.success(enabled ? '渠道已启用' : '渠道已停用')
} catch (e) {
message.error(e instanceof Error ? e.message : '更新失败')
@@ -155,323 +159,388 @@ const Settings = () => {
}
}
const addChannelType = async (t: string) => {
try {
await createChannel({ type: t, name: typeMeta[t].label })
message.success('已添加')
await loadChannels()
} catch (e) {
message.error(e instanceof Error ? e.message : '添加失败')
}
}
const missingTypes = Object.keys(typeMeta).filter(t => !channels.some(c => c.type === t))
const panelHeaderAction = activeTab === 'channels' && missingTypes.includes('web') ? (
<Button type="primary" size="small" icon={<PlusOutlined />} className="!h-8" onClick={ensureWebChannel}>
</Button>
) : activeTab === 'channels' && missingTypes.length > 0 ? (
<Button type="primary" size="small" icon={<PlusOutlined />} className="!h-8" onClick={() => addChannelType(missingTypes[0])}>
</Button>
) : null
return (
<div className="h-full flex">
<div className="w-[200px] flex-shrink-0 bg-white border-r border-neutral-200 py-4">
<div className="px-4 mb-3">
<span className="text-xs text-neutral-400 font-medium"></span>
<div className="h-full flex min-h-0 overflow-hidden bg-neutral-50">
{/* 左侧设置导航 — 顶栏 56px 对齐 */}
<aside className="w-[220px] shrink-0 flex flex-col bg-white border-r border-neutral-200">
<div
className="shrink-0 px-4 flex flex-col justify-center border-b border-neutral-200"
style={{ height: 'var(--header-height)' }}
>
<div className="text-sm font-semibold text-neutral-900 leading-tight"></div>
<div className="text-[11px] text-neutral-400 truncate leading-tight mt-0.5"></div>
</div>
{tabItems.map(item => (
<div
key={item.key}
className={`px-4 py-2 text-sm cursor-pointer transition-colors ${
activeTab === item.key
? 'bg-blue-50 text-blue-600 font-medium border-r-2 border-blue-500'
: 'text-neutral-600 hover:bg-neutral-50'
}`}
onClick={() => setActiveTab(item.key)}
>
{item.label}
<nav className="flex-1 overflow-y-auto py-2 px-2">
{tabItems.map(item => {
const active = activeTab === item.key
return (
<button
key={item.key}
type="button"
onClick={() => setActiveTab(item.key)}
className={`w-full flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-left border-0 cursor-pointer transition-colors mb-0.5 ${
active
? 'bg-[#eff6ff] text-[#2563eb] font-medium'
: 'bg-transparent text-neutral-600 hover:bg-neutral-50'
}`}
>
<span className={`text-base leading-none ${active ? 'text-[#2563eb]' : 'text-neutral-400'}`}>
{item.icon}
</span>
<span className="truncate">{item.label}</span>
</button>
)
})}
</nav>
</aside>
{/* 右侧内容 */}
<section className="flex-1 flex flex-col min-w-0 overflow-hidden">
<div
className="shrink-0 px-6 flex items-center justify-between gap-3 border-b border-neutral-200 bg-white"
style={{ height: 'var(--header-height)' }}
>
<div className="min-w-0">
<h1 className="text-base font-semibold text-neutral-900 m-0 truncate">{currentTab.label}</h1>
</div>
))}
</div>
{panelHeaderAction}
</div>
<div className="flex-1 overflow-auto p-6">
{activeTab === 'basic' && (
<Card title="基本设置" className="max-w-2xl">
{loadingSettings && !settings ? (
<div className="py-10 text-center"><Spin /></div>
) : (
<Form
form={basicForm}
layout="vertical"
onFinish={values => savePartial({
display_name: values.display_name,
agent_nickname: values.agent_nickname,
timezone: values.timezone,
})}
>
<Form.Item name="display_name" label="租户名称" rules={[{ required: true }, { min: 1, max: 100 }]}>
<Input placeholder="公司名称 / 访客端展示名" />
</Form.Item>
<Form.Item name="agent_nickname" label="客服默认昵称">
<Input placeholder="客服在访客端显示的昵称" maxLength={50} />
</Form.Item>
<Form.Item name="timezone" label="时区">
<Select options={[
{ value: 'Asia/Shanghai', label: '东八区 (UTC+8)' },
{ value: 'Asia/Tokyo', label: '东京 (UTC+9)' },
{ value: 'UTC', label: 'UTC' },
]}
/>
</Form.Item>
<Form.Item name="language" label="语言">
<Select options={[{ value: 'zh-CN', label: '简体中文' }]} disabled />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
)}
</Card>
)}
<div className="flex-1 overflow-auto px-6 py-5">
<div className="max-w-3xl">
<p className="text-xs text-neutral-400 mt-0 mb-5">{currentTab.desc}</p>
{activeTab === 'channels' && (
<div>
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-base font-semibold text-neutral-800 m-0"></h3>
<p className="text-xs text-neutral-400 mt-1 mb-0"></p>
</div>
{missingTypes.includes('web') && (
<Button type="primary" icon={<PlusOutlined />} onClick={ensureWebChannel}></Button>
)}
</div>
{loadingChannels ? (
<div className="py-16 text-center"><Spin /></div>
) : channels.length === 0 ? (
<Empty description="暂无渠道,请先开通网页渠道">
<Button type="primary" onClick={ensureWebChannel}></Button>
</Empty>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{channels.map(ch => {
const meta = typeMeta[ch.type] || { icon: <LinkOutlined />, label: ch.name || ch.type }
const enabled = ch.status === 'enabled'
return (
<Card
key={ch.id}
size="small"
className="!rounded-lg"
title={(
<div className="flex items-center gap-2">
<span className="text-blue-500">{meta.icon}</span>
<span className="text-sm font-medium">{ch.name || meta.label}</span>
<Tag color={enabled ? 'green' : 'default'} className="ml-1">{enabled ? '已启用' : '未启用'}</Tag>
</div>
)}
>
<div className="space-y-3">
{ch.channel_key && (
<div className="flex items-center justify-between text-sm">
<span className="text-neutral-400"> ID</span>
<code className="text-xs bg-neutral-100 px-2 py-0.5 rounded">{ch.channel_key}</code>
</div>
)}
{ch.type === 'web' && ch.script_code && (
<div>
<div className="text-sm text-neutral-400 mb-1"></div>
<div className="bg-neutral-50 border border-neutral-200 rounded p-2 flex items-start justify-between gap-2">
<code className="text-xs text-neutral-600 break-all">
{ch.script_code.replace('src="/widget.js"', `src="${window.location.origin}/widget.js"`)}
</code>
<CopyOutlined
className="text-neutral-400 cursor-pointer hover:text-blue-500 flex-shrink-0 mt-0.5"
onClick={() => copyScript(ch.script_code)}
/>
</div>
</div>
)}
{ch.type !== 'web' && (
<div className="text-xs text-neutral-400"></div>
)}
<div className="flex justify-between items-center">
<span className="text-sm text-neutral-400"></span>
<Switch checked={enabled} size="small" loading={togglingId === ch.id} onChange={v => toggleChannel(ch, v)} />
</div>
</div>
</Card>
)
})}
</div>
)}
{missingTypes.length > 0 && channels.length > 0 && (
<div className="mt-4">
<div className="text-xs text-neutral-400 mb-2"></div>
<div className="flex flex-wrap gap-2">
{missingTypes.map(t => (
<Button
key={t}
size="small"
icon={<PlusOutlined />}
onClick={async () => {
try {
await createChannel({ type: t, name: typeMeta[t].label })
message.success('已添加')
await loadChannels()
} catch (e) {
message.error(e instanceof Error ? e.message : '添加失败')
}
}}
>
{typeMeta[t].label}
</Button>
))}
</div>
</div>
)}
</div>
)}
{activeTab === 'assignment' && (
<Card title="客服分配规则" className="max-w-2xl">
<div className="space-y-4">
<div className="p-4 border border-blue-100 bg-blue-50 rounded-lg">
<div className="text-sm font-medium text-neutral-800"> + </div>
<div className="text-xs text-neutral-500 mt-1">
线线访
</div>
</div>
<div className="p-4 border border-neutral-200 rounded-lg opacity-60">
<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>
<Switch disabled />
</div>
</div>
</div>
</Card>
)}
{activeTab === 'permission' && (
<Card title="权限管理" className="max-w-2xl">
<p className="text-sm text-neutral-500 m-0">
/ / / 线
</p>
</Card>
)}
{activeTab === 'autoreply' && (
<Card title="自动回复" className="max-w-2xl">
{loadingSettings && !settings ? (
<div className="py-10 text-center"><Spin /></div>
) : (
<Form
form={autoReplyForm}
layout="vertical"
onFinish={values => savePartial({
welcome_message: values.welcome_message,
offline_prompt: values.offline_prompt,
}, '自动回复已保存')}
>
<Form.Item name="welcome_message" label="欢迎语" rules={[{ max: 500 }]}>
<Input.TextArea rows={3} maxLength={500} showCount placeholder="访客打开窗口时展示" />
</Form.Item>
<Form.Item name="offline_prompt" label="离线留言提示" rules={[{ max: 500 }]}>
<Input.TextArea rows={3} maxLength={500} showCount placeholder="无客服在线时展示" />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
)}
</Card>
)}
{activeTab === 'worktime' && (
<Card title="工作时间" className="max-w-2xl">
{loadingSettings && !settings ? (
<div className="py-10 text-center"><Spin /></div>
) : (
<Form
form={workForm}
layout="vertical"
onFinish={values => {
const work_hours: WorkHours = {}
weekDays.forEach(d => {
work_hours[d.key] = values[d.key] || ''
})
return savePartial({
work_hours,
worktime_prompt: values.worktime_prompt,
}, '工作时间已保存')
}}
>
<p className="text-sm text-neutral-500 mb-4">
访
</p>
{weekDays.map(day => (
<div key={day.key} className="flex items-center justify-between py-2 border-b border-neutral-50">
<span className="text-sm text-neutral-700 w-12">{day.label}</span>
<Form.Item name={day.key} className="!mb-0">
<Select
placeholder="休息"
className="w-40"
size="small"
allowClear
options={hourOptions}
{activeTab === 'basic' && (
<div className="bg-white rounded-xl border border-neutral-200 shadow-sm p-6">
{loadingSettings && !settings ? (
<div className="py-10 text-center"><Spin /></div>
) : (
<Form
form={basicForm}
layout="vertical"
onFinish={values => savePartial({
display_name: values.display_name,
agent_nickname: values.agent_nickname,
timezone: values.timezone,
})}
>
<Form.Item name="display_name" label="租户名称" rules={[{ required: true }, { min: 1, max: 100 }]}>
<Input placeholder="公司名称 / 访客端展示名" />
</Form.Item>
<Form.Item name="agent_nickname" label="客服默认昵称">
<Input placeholder="客服在访客端显示的昵称" maxLength={50} />
</Form.Item>
<Form.Item name="timezone" label="时区">
<Select options={[
{ value: 'Asia/Shanghai', label: '东八区 (UTC+8)' },
{ value: 'Asia/Tokyo', label: '东京 (UTC+9)' },
{ value: 'UTC', label: 'UTC' },
]}
/>
</Form.Item>
</div>
))}
<Form.Item name="worktime_prompt" label="非工作时间提示" className="mt-4" rules={[{ max: 500 }]}>
<Input placeholder="当前为非工作时间,我们会在工作时段尽快回复您" maxLength={500} />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
<Form.Item name="language" label="语言">
<Select options={[{ value: 'zh-CN', label: '简体中文' }]} disabled />
</Form.Item>
<Form.Item className="!mb-0">
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
)}
</div>
)}
</Card>
)}
{activeTab === 'notify' && (
<Card title="通知设置" className="max-w-2xl">
{loadingSettings && !settings ? (
<div className="py-10 text-center"><Spin /></div>
) : (
<Form
form={notifyForm}
layout="vertical"
onFinish={values => savePartial({
notify_new_session: values.notify_new_session,
notify_offline_leave: values.notify_offline_leave,
notify_daily_report: values.notify_daily_report,
}, '通知设置已保存')}
>
<div className="space-y-3 mb-4">
<div className="flex items-center justify-between p-3 border border-neutral-100 rounded-lg">
<div>
<div className="text-sm font-medium text-neutral-800"></div>
<div className="text-xs text-neutral-400">访线</div>
</div>
<Form.Item name="notify_new_session" valuePropName="checked" className="!mb-0">
<Switch />
</Form.Item>
{activeTab === 'channels' && (
<div>
{loadingChannels ? (
<div className="py-16 text-center"><Spin /></div>
) : channels.length === 0 ? (
<div className="bg-white rounded-xl border border-neutral-200 p-10">
<Empty description="暂无渠道,请先开通网页渠道">
<Button type="primary" onClick={ensureWebChannel}></Button>
</Empty>
</div>
<div className="flex items-center justify-between p-3 border border-neutral-100 rounded-lg">
<div>
<div className="text-sm font-medium text-neutral-800">线</div>
<div className="text-xs text-neutral-400">访线</div>
</div>
<Form.Item name="notify_offline_leave" valuePropName="checked" className="!mb-0">
<Switch />
</Form.Item>
) : (
<div className="flex flex-col gap-3">
{channels.map(ch => {
const meta = typeMeta[ch.type] || { icon: <LinkOutlined />, label: ch.name || ch.type, desc: '' }
const enabled = ch.status === 'enabled'
return (
<div
key={ch.id}
className="bg-white rounded-xl border border-neutral-200 shadow-sm p-4"
>
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-[#eff6ff] text-[#2563eb] flex items-center justify-center text-lg shrink-0">
{meta.icon}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between gap-3 mb-1">
<div className="flex items-center gap-2 min-w-0">
<h3 className="text-sm font-medium text-neutral-900 m-0 truncate">
{ch.name || meta.label}
</h3>
<span
className={`inline-flex items-center rounded px-2 py-0.5 text-[11px] font-medium whitespace-nowrap ${
enabled ? 'bg-[#f0fdf4] text-[#16a34a]' : 'bg-neutral-100 text-neutral-500'
}`}
>
{enabled ? '已启用' : '未启用'}
</span>
</div>
<Switch
checked={enabled}
size="small"
loading={togglingId === ch.id}
onChange={v => toggleChannel(ch, v)}
/>
</div>
{meta.desc && (
<p className="text-xs text-neutral-400 m-0 mb-3">{meta.desc}</p>
)}
{ch.channel_key && (
<div className="flex items-center gap-2 text-xs mb-2">
<span className="text-neutral-400"> ID</span>
<code className="bg-neutral-100 text-neutral-600 px-2 py-0.5 rounded">
{ch.channel_key}
</code>
</div>
)}
{ch.type === 'web' && ch.script_code && (
<div className="mt-2">
<div className="text-xs text-neutral-400 mb-1"></div>
<div className="bg-neutral-50 border border-neutral-200 rounded-lg p-2.5 flex items-start justify-between gap-2">
<code className="text-[11px] text-neutral-600 break-all leading-relaxed">
{ch.script_code.replace(
'src="/widget.js"',
`src="${window.location.origin}/widget.js"`,
)}
</code>
<button
type="button"
className="shrink-0 w-7 h-7 rounded-md flex items-center justify-center text-neutral-400 hover:text-[#2563eb] hover:bg-blue-50 border-0 bg-transparent cursor-pointer"
title="复制"
onClick={() => copyScript(ch.script_code)}
>
<CopyOutlined />
</button>
</div>
</div>
)}
{ch.type !== 'web' && (
<div className="text-xs text-neutral-400 mt-1">
</div>
)}
</div>
</div>
</div>
)
})}
</div>
<div className="flex items-center justify-between p-3 border border-neutral-100 rounded-lg">
)}
{missingTypes.length > 0 && channels.length > 0 && (
<div className="mt-5">
<div className="text-xs text-neutral-400 mb-2"></div>
<div className="flex flex-wrap gap-2">
{missingTypes.map(t => (
<Button key={t} size="small" icon={<PlusOutlined />} onClick={() => addChannelType(t)}>
{typeMeta[t].label}
</Button>
))}
</div>
</div>
)}
</div>
)}
{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-400"></div>
<div className="text-sm font-medium text-neutral-800"> + </div>
<div className="text-xs text-neutral-500 mt-1.5 leading-relaxed">
线线访线
</div>
</div>
<Form.Item name="notify_daily_report" valuePropName="checked" className="!mb-0">
<Switch />
</Form.Item>
</div>
</div>
<Form.Item>
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
<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>
<Switch disabled />
</div>
</div>
</div>
)}
</Card>
)}
</div>
{activeTab === 'permission' && (
<div className="bg-white rounded-xl border border-neutral-200 shadow-sm p-6">
<div className="text-sm text-neutral-600 leading-relaxed space-y-3">
<p className="m-0"></p>
<ul className="m-0 pl-5 space-y-2 text-sm text-neutral-600">
<li><span className="font-medium text-neutral-800"></span> </li>
<li><span className="font-medium text-neutral-800"></span> </li>
<li><span className="font-medium text-neutral-800">线</span> </li>
</ul>
</div>
</div>
)}
{activeTab === 'autoreply' && (
<div className="bg-white rounded-xl border border-neutral-200 shadow-sm p-6">
{loadingSettings && !settings ? (
<div className="py-10 text-center"><Spin /></div>
) : (
<Form
form={autoReplyForm}
layout="vertical"
onFinish={values => savePartial({
welcome_message: values.welcome_message,
offline_prompt: values.offline_prompt,
}, '自动回复已保存')}
>
<Form.Item name="welcome_message" label="欢迎语" rules={[{ max: 500 }]}>
<Input.TextArea rows={3} maxLength={500} showCount placeholder="访客打开窗口时展示" />
</Form.Item>
<Form.Item name="offline_prompt" label="离线留言提示" rules={[{ max: 500 }]}>
<Input.TextArea rows={3} maxLength={500} showCount placeholder="无客服在线时展示" />
</Form.Item>
<Form.Item className="!mb-0">
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
)}
</div>
)}
{activeTab === 'worktime' && (
<div className="bg-white rounded-xl border border-neutral-200 shadow-sm p-6">
{loadingSettings && !settings ? (
<div className="py-10 text-center"><Spin /></div>
) : (
<Form
form={workForm}
layout="vertical"
onFinish={values => {
const work_hours: WorkHours = {}
weekDays.forEach(d => {
work_hours[d.key] = values[d.key] || ''
})
return savePartial({
work_hours,
worktime_prompt: values.worktime_prompt,
}, '工作时间已保存')
}}
>
<p className="text-sm text-neutral-500 mb-4">
访
</p>
<div className="rounded-lg border border-neutral-100 overflow-hidden mb-4">
{weekDays.map((day, i) => (
<div
key={day.key}
className={`flex items-center justify-between px-4 py-2.5 ${
i < weekDays.length - 1 ? 'border-b border-neutral-50' : ''
}`}
>
<span className="text-sm text-neutral-700 w-12">{day.label}</span>
<Form.Item name={day.key} className="!mb-0">
<Select
placeholder="休息"
className="!w-44"
size="small"
allowClear
options={hourOptions}
/>
</Form.Item>
</div>
))}
</div>
<Form.Item name="worktime_prompt" label="非工作时间提示" rules={[{ max: 500 }]}>
<Input placeholder="当前为非工作时间,我们会在工作时段尽快回复您" maxLength={500} />
</Form.Item>
<Form.Item className="!mb-0">
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
)}
</div>
)}
{activeTab === 'notify' && (
<div className="bg-white rounded-xl border border-neutral-200 shadow-sm p-6">
{loadingSettings && !settings ? (
<div className="py-10 text-center"><Spin /></div>
) : (
<Form
form={notifyForm}
layout="vertical"
onFinish={values => savePartial({
notify_new_session: values.notify_new_session,
notify_offline_leave: values.notify_offline_leave,
notify_daily_report: values.notify_daily_report,
}, '通知设置已保存')}
>
<div className="space-y-3 mb-5">
{[
{ name: 'notify_new_session', title: '新会话提醒', desc: '有新访客进线时提醒(推送通道后续接入)' },
{ name: 'notify_offline_leave', title: '离线留言通知', desc: '访客提交离线留言时提醒' },
{ name: 'notify_daily_report', title: '日报推送', desc: '每日服务数据摘要' },
].map(item => (
<div
key={item.name}
className="flex items-center justify-between p-3.5 border border-neutral-100 rounded-lg"
>
<div className="min-w-0 pr-3">
<div className="text-sm font-medium text-neutral-800">{item.title}</div>
<div className="text-xs text-neutral-400 mt-0.5">{item.desc}</div>
</div>
<Form.Item name={item.name} valuePropName="checked" className="!mb-0">
<Switch />
</Form.Item>
</div>
))}
</div>
<Form.Item className="!mb-0">
<Button type="primary" htmlType="submit" loading={saving}></Button>
</Form.Item>
</Form>
)}
</div>
)}
</div>
</div>
</section>
</div>
)
}