feat: release contextual omnichannel customer platform

Add Telegram, WhatsApp and email account management, realtime messaging, contextual multilingual AI, customer intelligence, knowledge workflows, enterprise controls, a unified design system and complete repository documentation.
This commit is contained in:
wmq
2026-07-30 02:29:53 +08:00
parent 9804341463
commit af49d22ca7
66 changed files with 3181 additions and 554 deletions
+42 -22
View File
@@ -17,7 +17,7 @@ const SECTIONS = [
{ key: 'company', label: '企业资料', icon: UserCog },
{ key: 'lang', label: '语言与时区', icon: Globe },
{ key: 'notifications', label: '消息通知', icon: Bell },
{ key: 'ai', label: 'AI回复设置', icon: Languages },
{ key: 'ai', label: '智能回复', icon: Languages },
{ key: 'safety', label: '安全设置', icon: ShieldCheck },
{ key: 'api', label: 'API Key', icon: KeyRound },
]
@@ -55,19 +55,33 @@ export function SettingsPage() {
})
return (
<div className="flex h-[calc(100vh-56px)]">
<div className="w-48 shrink-0 border-e border-border p-3 space-y-1 overflow-y-auto scrollbar-thin hidden md:block">
<div className="page-shell">
<PageHeader title="设置" description="管理企业资料、通知、智能能力与账号安全" />
<div className="mb-4 rounded-[12px] border border-slate-200 bg-white p-2 shadow-sm md:hidden">
<Select
aria-label="设置分类"
value={section}
onChange={(event) => setSection(event.target.value)}
className="w-full"
options={SECTIONS.map((item) => ({ value: item.key, label: item.label }))}
/>
</div>
<div className="grid items-start gap-5 md:grid-cols-[220px_minmax(0,1fr)]">
<div className="sticky top-4 hidden space-y-1 rounded-[14px] border border-slate-200 bg-white p-2 shadow-sm md:block">
<p className="px-3 pb-2 pt-1 text-[10px] font-semibold uppercase tracking-[0.08em] text-slate-400">设置分类</p>
{SECTIONS.map((s) => (
<button key={s.key} onClick={() => setSection(s.key)} className={`flex w-full items-center gap-2 rounded px-2 py-1.5 text-sm ${section === s.key ? 'bg-primary-50 font-medium text-primary-700' : 'text-muted-foreground hover:text-foreground'}`}>
<button key={s.key} onClick={() => setSection(s.key)} className={`flex w-full items-center gap-2 rounded-[9px] px-3 py-2.5 text-sm transition ${section === s.key ? 'bg-primary-50 font-semibold text-primary-700 shadow-[inset_0_0_0_1px_rgba(37,99,235,.08)]' : 'text-slate-500 hover:bg-slate-50 hover:text-slate-900'}`}>
<s.icon className="h-4 w-4" />{s.label}
</button>
))}
</div>
<div className="flex-1 overflow-y-auto p-4 md:p-6 scrollbar-thin">
<div className="min-w-0">
{section === 'company' && settings && (
<Card>
<CardHeader title="企业资料" />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 p-4">
<CardHeader title="企业资料" description="用于团队空间、通知邮件和对外业务资料" />
<div className="grid grid-cols-1 gap-5 p-5 md:grid-cols-2">
<Field label="企业名称"><Input value={companyDraft.name} onChange={(e) => setCompanyDraft((v) => ({ ...v, name: e.target.value }))} /></Field>
<Field label="所属行业"><Input value={companyDraft.industry} onChange={(e) => setCompanyDraft((v) => ({ ...v, industry: e.target.value }))} /></Field>
<Field label="联系方式"><Input value={companyDraft.contact} onChange={(e) => setCompanyDraft((v) => ({ ...v, contact: e.target.value }))} /></Field>
@@ -79,20 +93,25 @@ export function SettingsPage() {
)}
{section === 'ai' && settings && (
<Card>
<CardHeader title="AI 回复设置" />
<div className="space-y-4 p-4">
<CardHeader title="智能回复" description="控制回复建议、分析和发送前确认规则" />
<div className="divide-y divide-slate-100 px-5">
{[
{ key: 'autoGenerate', label: '自动生成回复建议' },
{ key: 'requireConfirm', label: '发送前必须人工确认' },
{ key: 'sentimentAnalysis', label: '启用情感分析' },
{ key: 'messageIntentAnalysis', label: '逐条客户消息意图识别' },
{ key: 'contextualTranslation', label: '翻译时参考会话上下文' },
{ key: 'riskAlert', label: '启用风险预警' },
{ key: 'cultureCheck', label: '启用文化敏感检测' },
{ key: 'allowHistoryRef', label: '允许AI引用历史成功回复' },
].map((item) => (
<div key={item.key} className="flex items-center justify-between">
<div key={item.key} className="flex items-center justify-between py-4">
<span className="text-sm">{item.label}</span>
<Switch
checked={(settings.ai as any)[item.key]}
ariaLabel={item.label}
checked={['messageIntentAnalysis', 'contextualTranslation'].includes(item.key)
? (settings.ai as any)[item.key] !== false
: Boolean((settings.ai as any)[item.key])}
onCheckedChange={(v) => updateMut.mutate({ ai: { [item.key]: v } })}
/>
</div>
@@ -102,8 +121,8 @@ export function SettingsPage() {
)}
{section === 'notifications' && settings && (
<Card>
<CardHeader title="消息通知设置" />
<div className="space-y-4 p-4">
<CardHeader title="消息通知" description="选择需要发送到通知中心的业务事件" />
<div className="divide-y divide-slate-100 px-5">
{[
{ key: 'newInquiry', label: '新询盘提醒' },
{ key: 'highIntent', label: '高意向客户提醒' },
@@ -113,7 +132,7 @@ export function SettingsPage() {
{ key: 'channelOffline', label: '渠道断开提醒' },
{ key: 'planExpire', label: '套餐到期提醒' },
].map((item) => (
<div key={item.key} className="flex items-center justify-between">
<div key={item.key} className="flex items-center justify-between py-4">
<span className="text-sm">{item.label}</span>
<Switch
checked={(settings.notification as any)[item.key]}
@@ -126,12 +145,12 @@ export function SettingsPage() {
)}
{section === 'safety' && (
<Card>
<CardHeader title="安全设置" />
<div className="space-y-4 p-4">
<CardHeader title="安全设置" description="修改密码后会撤销其他设备上的登录会话" />
<div className="space-y-5 p-5">
<Field label="当前密码"><Input type="password" value={passwords.currentPassword} onChange={(e) => setPasswords((v) => ({ ...v, currentPassword: e.target.value }))} /></Field>
<Field label="新密码"><Input type="password" placeholder="至少10位,包含字母和数字" value={passwords.newPassword} onChange={(e) => setPasswords((v) => ({ ...v, newPassword: e.target.value }))} /></Field>
<Field label="确认新密码"><Input type="password" value={passwords.confirmPassword} onChange={(e) => setPasswords((v) => ({ ...v, confirmPassword: e.target.value }))} /></Field>
<div className="rounded bg-emerald-50 p-3 text-sm text-emerald-700"><ShieldCheck className="inline h-4 w-4 mr-1" />修改密码后,其他已登录设备需要重新登录。</div>
<div className="rounded-[10px] border border-primary-100 bg-primary-50 p-3 text-sm text-primary-700"><ShieldCheck className="mr-1 inline h-4 w-4" />修改密码后,其他已登录设备需要重新登录。</div>
<Button onClick={async () => {
if (passwords.newPassword !== passwords.confirmPassword) return toast.error('两次输入的新密码不一致')
try {
@@ -146,8 +165,8 @@ export function SettingsPage() {
)}
{section === 'api' && (
<Card>
<CardHeader title="API 与 Webhook" />
<div className="space-y-3 p-4">
<CardHeader title="API Key" description="为自有系统或服务端集成创建访问凭据" />
<div className="space-y-4 p-5">
<p className="text-sm text-muted-foreground">密钥只在创建时完整显示一次,请立即复制并妥善保存。</p>
<div className="flex gap-2">
<Input value={apiName} onChange={(e) => setApiName(e.target.value)} placeholder="密钥名称" />
@@ -168,7 +187,7 @@ export function SettingsPage() {
</div>
</div>
)}
<div className="divide-y divide-border rounded-lg border border-border">
<div className="divide-y divide-slate-100 overflow-hidden rounded-[10px] border border-slate-200">
{(apiKeys || []).map((key) => (
<div key={key.id} className="flex items-center gap-3 px-3 py-2">
<div className="min-w-0 flex-1"><p className="text-sm font-medium">{key.name}</p><p className="text-xs text-muted-foreground">{key.prefix}•••••• · {key.revoked ? '已撤销' : '有效'}</p></div>
@@ -181,8 +200,8 @@ export function SettingsPage() {
)}
{section === 'lang' && (
<Card>
<CardHeader title="语言与时区" />
<div className="space-y-4 p-4">
<CardHeader title="语言与时区" description="调整界面显示语言与业务时间基准" />
<div className="space-y-5 p-5">
<Field label="界面语言">
<Select value={localeDraft} options={[{value:'zh',label:'简体中文'},{value:'en',label:'English'},{value:'ar',label:'العربية (RTL)'}]} onChange={(e) => setLocaleDraft(e.target.value as any)} />
</Field>
@@ -195,6 +214,7 @@ export function SettingsPage() {
</Card>
)}
</div>
</div>
</div>
)
}