671 lines
31 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import Head from 'next/head';
import { toast } from 'react-hot-toast';
import {
CogIcon,
ShieldCheckIcon,
CurrencyDollarIcon,
GlobeAltIcon,
BellIcon,
PhoneIcon,
CloudIcon,
KeyIcon
} from '@heroicons/react/24/outline';
import Layout from '@/components/Layout';
interface Settings {
// 基础设置
siteName: string;
siteDescription: string;
adminEmail: string;
supportEmail: string;
// 通话设置
maxCallDuration: number; // 分钟
callTimeout: number; // 秒
enableRecording: boolean;
enableVideoCall: boolean;
// 费用设置 - 修改为每个服务单独配置
serviceRates: {
ai_voice: number; // AI语音翻译费率元/分钟)
ai_video: number; // AI视频翻译费率元/分钟)
sign_language: number; // 手语翻译费率(元/分钟)
human_interpreter: number; // 真人翻译费率(元/分钟)
document_translation: number; // 文档翻译费率(元/字)
};
currency: string;
taxRate: number;
// 通知设置
emailNotifications: boolean;
smsNotifications: boolean;
pushNotifications: boolean;
// 安全设置
enableTwoFactor: boolean;
sessionTimeout: number; // 分钟
maxLoginAttempts: number;
// API设置
twilioAccountSid: string;
twilioAuthToken: string;
openaiApiKey: string;
// 语言设置
defaultLanguage: string;
supportedLanguages: string[];
}
export default function SettingsPage() {
const [settings, setSettings] = useState<Settings>({
siteName: '口译服务管理系统',
siteDescription: '专业的多语言口译服务平台',
adminEmail: 'admin@interpreter.com',
supportEmail: 'support@interpreter.com',
maxCallDuration: 120,
callTimeout: 30,
enableRecording: true,
enableVideoCall: true,
serviceRates: {
ai_voice: 2.0,
ai_video: 3.0,
sign_language: 5.0,
human_interpreter: 8.0,
document_translation: 0.1,
},
currency: 'CNY',
taxRate: 0.06,
emailNotifications: true,
smsNotifications: false,
pushNotifications: true,
enableTwoFactor: false,
sessionTimeout: 30,
maxLoginAttempts: 5,
twilioAccountSid: '',
twilioAuthToken: '',
openaiApiKey: '',
defaultLanguage: 'zh-CN',
supportedLanguages: ['zh-CN', 'en-US', 'ja-JP', 'ko-KR', 'fr-FR', 'de-DE', 'es-ES']
});
const [loading, setLoading] = useState(false);
const [activeTab, setActiveTab] = useState('basic');
// 保存设置
const saveSettings = async () => {
try {
setLoading(true);
// 这里应该调用API保存设置
// await api.updateSettings(settings);
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000));
toast.success('设置保存成功');
} catch (error) {
console.error('Error saving settings:', error);
toast.error('保存设置失败');
} finally {
setLoading(false);
}
};
// 重置设置
const resetSettings = () => {
setSettings({
siteName: '口译服务管理系统',
siteDescription: '专业的多语言口译服务平台',
adminEmail: 'admin@interpreter.com',
supportEmail: 'support@interpreter.com',
maxCallDuration: 120,
callTimeout: 30,
enableRecording: true,
enableVideoCall: true,
serviceRates: {
ai_voice: 2.0,
ai_video: 3.0,
sign_language: 5.0,
human_interpreter: 8.0,
document_translation: 0.1,
},
currency: 'CNY',
taxRate: 0.06,
emailNotifications: true,
smsNotifications: false,
pushNotifications: true,
enableTwoFactor: false,
sessionTimeout: 30,
maxLoginAttempts: 5,
twilioAccountSid: '',
twilioAuthToken: '',
openaiApiKey: '',
defaultLanguage: 'zh-CN',
supportedLanguages: ['zh-CN', 'en-US', 'ja-JP', 'ko-KR', 'fr-FR', 'de-DE', 'es-ES']
});
toast.success('设置已重置为默认值');
};
const tabs = [
{ id: 'basic', name: '基础设置', icon: CogIcon },
{ id: 'call', name: '通话设置', icon: PhoneIcon },
{ id: 'billing', name: '费用设置', icon: CurrencyDollarIcon },
{ id: 'notifications', name: '通知设置', icon: BellIcon },
{ id: 'security', name: '安全设置', icon: ShieldCheckIcon },
{ id: 'api', name: 'API设置', icon: KeyIcon },
{ id: 'language', name: '语言设置', icon: GlobeAltIcon }
];
return (
<Layout>
<Head>
<title> - </title>
</Head>
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
{/* 页面标题 */}
<div className="flex items-center justify-between mb-6">
<h1 className="text-2xl font-bold text-gray-900"></h1>
<div className="flex space-x-3">
<button
onClick={resetSettings}
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
</button>
<button
onClick={saveSettings}
disabled={loading}
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50"
>
{loading ? '保存中...' : '保存设置'}
</button>
</div>
</div>
<div className="bg-white shadow rounded-lg">
<div className="flex">
{/* 侧边栏标签 */}
<div className="w-64 border-r border-gray-200">
<nav className="space-y-1 p-4">
{tabs.map((tab) => {
const Icon = tab.icon;
return (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`w-full flex items-center px-3 py-2 text-sm font-medium rounded-md ${
activeTab === tab.id
? 'bg-blue-50 text-blue-700 border-blue-500'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<Icon className="h-5 w-5 mr-3" />
{tab.name}
</button>
);
})}
</nav>
</div>
{/* 主内容区域 */}
<div className="flex-1 p-6">
{/* 基础设置 */}
{activeTab === 'basic' && (
<div className="space-y-6">
<h3 className="text-lg font-medium text-gray-900"></h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<input
type="text"
value={settings.siteName}
onChange={(e) => setSettings({...settings, siteName: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<input
type="email"
value={settings.adminEmail}
onChange={(e) => setSettings({...settings, adminEmail: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="sm:col-span-2">
<label className="block text-sm font-medium text-gray-700"></label>
<textarea
rows={3}
value={settings.siteDescription}
onChange={(e) => setSettings({...settings, siteDescription: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<input
type="email"
value={settings.supportEmail}
onChange={(e) => setSettings({...settings, supportEmail: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
</div>
)}
{/* 通话设置 */}
{activeTab === 'call' && (
<div className="space-y-6">
<h3 className="text-lg font-medium text-gray-900"></h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-gray-700"> ()</label>
<input
type="number"
value={settings.maxCallDuration}
onChange={(e) => setSettings({...settings, maxCallDuration: parseInt(e.target.value)})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"> ()</label>
<input
type="number"
value={settings.callTimeout}
onChange={(e) => setSettings({...settings, callTimeout: parseInt(e.target.value)})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div className="sm:col-span-2">
<div className="flex items-center">
<input
type="checkbox"
checked={settings.enableRecording}
onChange={(e) => setSettings({...settings, enableRecording: e.target.checked})}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-900"></label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<div className="mt-1">
<input
type="checkbox"
checked={settings.enableVideoCall}
onChange={(e) => setSettings({...settings, enableVideoCall: e.target.checked})}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
</div>
</div>
</div>
</div>
)}
{/* 费用设置 */}
{activeTab === 'billing' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4"></h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-gray-700">
AI语音翻译费率
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input
type="number"
step="0.1"
min="0"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pr-12 sm:text-sm border-gray-300 rounded-md"
value={settings.serviceRates.ai_voice}
onChange={(e) => setSettings({
...settings,
serviceRates: {
...settings.serviceRates,
ai_voice: parseFloat(e.target.value) || 0
}
})}
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">/</span>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
AI视频翻译费率
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input
type="number"
step="0.1"
min="0"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pr-12 sm:text-sm border-gray-300 rounded-md"
value={settings.serviceRates.ai_video}
onChange={(e) => setSettings({
...settings,
serviceRates: {
...settings.serviceRates,
ai_video: parseFloat(e.target.value) || 0
}
})}
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">/</span>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input
type="number"
step="0.1"
min="0"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pr-12 sm:text-sm border-gray-300 rounded-md"
value={settings.serviceRates.sign_language}
onChange={(e) => setSettings({
...settings,
serviceRates: {
...settings.serviceRates,
sign_language: parseFloat(e.target.value) || 0
}
})}
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">/</span>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input
type="number"
step="0.1"
min="0"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pr-12 sm:text-sm border-gray-300 rounded-md"
value={settings.serviceRates.human_interpreter}
onChange={(e) => setSettings({
...settings,
serviceRates: {
...settings.serviceRates,
human_interpreter: parseFloat(e.target.value) || 0
}
})}
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">/</span>
</div>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
</label>
<div className="mt-1 relative rounded-md shadow-sm">
<input
type="number"
step="0.01"
min="0"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pr-12 sm:text-sm border-gray-300 rounded-md"
value={settings.serviceRates.document_translation}
onChange={(e) => setSettings({
...settings,
serviceRates: {
...settings.serviceRates,
document_translation: parseFloat(e.target.value) || 0
}
})}
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<span className="text-gray-500 sm:text-sm">/</span>
</div>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div>
<label className="block text-sm font-medium text-gray-700">
</label>
<select
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm rounded-md"
value={settings.currency}
onChange={(e) => setSettings({...settings, currency: e.target.value})}
>
<option value="CNY"> (CNY)</option>
<option value="USD"> (USD)</option>
<option value="EUR"> (EUR)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">
(%)
</label>
<input
type="number"
step="0.01"
min="0"
max="1"
className="mt-1 focus:ring-indigo-500 focus:border-indigo-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
value={settings.taxRate}
onChange={(e) => setSettings({...settings, taxRate: parseFloat(e.target.value) || 0})}
/>
</div>
</div>
</div>
)}
{/* 通知设置 */}
{activeTab === 'notifications' && (
<div className="space-y-6">
<h3 className="text-lg font-medium text-gray-900"></h3>
<div className="space-y-4">
<div className="flex items-center">
<input
type="checkbox"
checked={settings.emailNotifications}
onChange={(e) => setSettings({...settings, emailNotifications: e.target.checked})}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-900"></label>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={settings.smsNotifications}
onChange={(e) => setSettings({...settings, smsNotifications: e.target.checked})}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-900"></label>
</div>
<div className="flex items-center">
<input
type="checkbox"
checked={settings.pushNotifications}
onChange={(e) => setSettings({...settings, pushNotifications: e.target.checked})}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-900"></label>
</div>
</div>
</div>
)}
{/* 安全设置 */}
{activeTab === 'security' && (
<div className="space-y-6">
<h3 className="text-lg font-medium text-gray-900"></h3>
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div className="sm:col-span-2">
<div className="flex items-center">
<input
type="checkbox"
checked={settings.enableTwoFactor}
onChange={(e) => setSettings({...settings, enableTwoFactor: e.target.checked})}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-900"></label>
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"> ()</label>
<input
type="number"
value={settings.sessionTimeout}
onChange={(e) => setSettings({...settings, sessionTimeout: parseInt(e.target.value)})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<input
type="number"
value={settings.maxLoginAttempts}
onChange={(e) => setSettings({...settings, maxLoginAttempts: parseInt(e.target.value)})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
/>
</div>
</div>
</div>
)}
{/* API设置 */}
{activeTab === 'api' && (
<div className="space-y-6">
<h3 className="text-lg font-medium text-gray-900">API设置</h3>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700">Twilio Account SID</label>
<input
type="text"
value={settings.twilioAccountSid}
onChange={(e) => setSettings({...settings, twilioAccountSid: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="ACxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Twilio Auth Token</label>
<input
type="password"
value={settings.twilioAuthToken}
onChange={(e) => setSettings({...settings, twilioAuthToken: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="••••••••••••••••••••••••••••••••"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700">OpenAI API Key</label>
<input
type="password"
value={settings.openaiApiKey}
onChange={(e) => setSettings({...settings, openaiApiKey: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
placeholder="sk-••••••••••••••••••••••••••••••••••••••••••••••••"
/>
</div>
</div>
</div>
)}
{/* 语言设置 */}
{activeTab === 'language' && (
<div className="space-y-6">
<h3 className="text-lg font-medium text-gray-900"></h3>
<div>
<label className="block text-sm font-medium text-gray-700"></label>
<select
value={settings.defaultLanguage}
onChange={(e) => setSettings({...settings, defaultLanguage: e.target.value})}
className="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
>
<option value="zh-CN"> ()</option>
<option value="en-US">English (US)</option>
<option value="ja-JP"></option>
<option value="ko-KR"></option>
<option value="es-ES">Español</option>
<option value="fr-FR">Français</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2"></label>
<div className="space-y-2">
{[
{ code: 'zh-CN', name: '中文 (简体)' },
{ code: 'en-US', name: 'English (US)' },
{ code: 'ja-JP', name: '日本語' },
{ code: 'ko-KR', name: '한국어' },
{ code: 'es-ES', name: 'Español' },
{ code: 'fr-FR', name: 'Français' }
].map((lang) => (
<div key={lang.code} className="flex items-center">
<input
type="checkbox"
checked={settings.supportedLanguages.includes(lang.code)}
onChange={(e) => {
if (e.target.checked) {
setSettings({
...settings,
supportedLanguages: [...settings.supportedLanguages, lang.code]
});
} else {
setSettings({
...settings,
supportedLanguages: settings.supportedLanguages.filter(l => l !== lang.code)
});
}
}}
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
/>
<label className="ml-2 block text-sm text-gray-900">{lang.name}</label>
</div>
))}
</div>
</div>
</div>
)}
</div>
</div>
</div>
</div>
</div>
</Layout>
);
}