增加收藏更新邮件发送

This commit is contained in:
mtvpls
2026-01-16 16:11:31 +08:00
parent 036e909f74
commit 6bce099a8d
14 changed files with 2468 additions and 176 deletions
+394
View File
@@ -33,6 +33,7 @@ import {
ExternalLink,
FileText,
FolderOpen,
Mail,
Palette,
Settings,
Tv,
@@ -8852,6 +8853,386 @@ const XiaoyaConfigComponent = ({
);
};
// 邮件配置组件
const EmailConfigComponent = ({
config,
refreshConfig,
}: {
config: AdminConfig | null;
refreshConfig: () => Promise<void>;
}) => {
const { alertModal, showAlert, hideAlert } = useAlertModal();
const { isLoading, withLoading } = useLoadingState();
const [enabled, setEnabled] = useState(false);
const [provider, setProvider] = useState<'smtp' | 'resend'>('smtp');
// SMTP配置
const [smtpHost, setSmtpHost] = useState('');
const [smtpPort, setSmtpPort] = useState(587);
const [smtpSecure, setSmtpSecure] = useState(false);
const [smtpUser, setSmtpUser] = useState('');
const [smtpPassword, setSmtpPassword] = useState('');
const [smtpFrom, setSmtpFrom] = useState('');
// Resend配置
const [resendApiKey, setResendApiKey] = useState('');
const [resendFrom, setResendFrom] = useState('');
// 测试邮件
const [testEmail, setTestEmail] = useState('');
useEffect(() => {
if (config?.EmailConfig) {
setEnabled(config.EmailConfig.enabled || false);
setProvider(config.EmailConfig.provider || 'smtp');
if (config.EmailConfig.smtp) {
setSmtpHost(config.EmailConfig.smtp.host || '');
setSmtpPort(config.EmailConfig.smtp.port || 587);
setSmtpSecure(config.EmailConfig.smtp.secure || false);
setSmtpUser(config.EmailConfig.smtp.user || '');
setSmtpPassword(config.EmailConfig.smtp.password || '');
setSmtpFrom(config.EmailConfig.smtp.from || '');
}
if (config.EmailConfig.resend) {
setResendApiKey(config.EmailConfig.resend.apiKey || '');
setResendFrom(config.EmailConfig.resend.from || '');
}
}
}, [config]);
const handleSave = async () => {
await withLoading('saveEmail', async () => {
try {
const emailConfig: AdminConfig['EmailConfig'] = {
enabled,
provider,
smtp: provider === 'smtp' ? {
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
user: smtpUser,
password: smtpPassword,
from: smtpFrom,
} : undefined,
resend: provider === 'resend' ? {
apiKey: resendApiKey,
from: resendFrom,
} : undefined,
};
const response = await fetch('/api/admin/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'save',
config: emailConfig,
}),
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || '保存失败');
}
showSuccess('保存成功', showAlert);
await refreshConfig();
} catch (error) {
showError(error instanceof Error ? error.message : '保存失败', showAlert);
throw error;
}
});
};
const handleTest = async () => {
if (!testEmail) {
showError('请输入测试邮箱地址', showAlert);
return;
}
await withLoading('testEmail', async () => {
try {
const emailConfig: AdminConfig['EmailConfig'] = {
enabled: true,
provider,
smtp: provider === 'smtp' ? {
host: smtpHost,
port: smtpPort,
secure: smtpSecure,
user: smtpUser,
password: smtpPassword,
from: smtpFrom,
} : undefined,
resend: provider === 'resend' ? {
apiKey: resendApiKey,
from: resendFrom,
} : undefined,
};
const response = await fetch('/api/admin/email', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
action: 'test',
config: emailConfig,
testEmail,
}),
});
const data = await response.json();
if (data.success) {
showSuccess('测试邮件发送成功,请检查收件箱', showAlert);
} else {
showError(data.error || '发送失败', showAlert);
}
} catch (error) {
showError(error instanceof Error ? error.message : '发送失败', showAlert);
throw error;
}
});
};
return (
<div className='space-y-6'>
<div className='bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4'>
<h3 className='text-sm font-medium text-blue-900 dark:text-blue-100 mb-2'>
</h3>
<div className='text-sm text-blue-800 dark:text-blue-200 space-y-1'>
<p> </p>
<p> SMTP Resend </p>
<p> </p>
</div>
</div>
<div className='space-y-4'>
{/* 启用开关 */}
<div className='flex items-center justify-between py-3 border-b border-gray-200 dark:border-gray-700'>
<div>
<h3 className='text-sm font-medium text-gray-900 dark:text-white'>
</h3>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</p>
</div>
<button
onClick={() => setEnabled(!enabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-gray-700'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
enabled ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{/* 发送方式选择 */}
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</label>
<div className='flex gap-4'>
<label className='flex items-center'>
<input
type='radio'
value='smtp'
checked={provider === 'smtp'}
onChange={(e) => setProvider(e.target.value as 'smtp')}
className='mr-2'
/>
<span className='text-sm text-gray-700 dark:text-gray-300'>SMTP</span>
</label>
<label className='flex items-center'>
<input
type='radio'
value='resend'
checked={provider === 'resend'}
onChange={(e) => setProvider(e.target.value as 'resend')}
className='mr-2'
/>
<span className='text-sm text-gray-700 dark:text-gray-300'>Resend</span>
</label>
</div>
</div>
{/* SMTP配置 */}
{provider === 'smtp' && (
<div className='space-y-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700'>
<h4 className='text-sm font-medium text-gray-900 dark:text-white'>SMTP </h4>
<div className='grid grid-cols-1 sm:grid-cols-2 gap-4'>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
SMTP *
</label>
<input
type='text'
value={smtpHost}
onChange={(e) => setSmtpHost(e.target.value)}
placeholder='smtp.gmail.com'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
SMTP *
</label>
<input
type='number'
value={smtpPort}
onChange={(e) => setSmtpPort(parseInt(e.target.value))}
placeholder='587'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
/>
</div>
</div>
<div className='flex items-center'>
<input
type='checkbox'
checked={smtpSecure}
onChange={(e) => setSmtpSecure(e.target.checked)}
className='mr-2'
/>
<label className='text-sm text-gray-700 dark:text-gray-300'>
使 SSL/TLS 465
</label>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
SMTP *
</label>
<input
type='text'
value={smtpUser}
onChange={(e) => setSmtpUser(e.target.value)}
placeholder='[email protected]'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
SMTP *
</label>
<input
type='password'
value={smtpPassword}
onChange={(e) => setSmtpPassword(e.target.value)}
placeholder='应用专用密码'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
/>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
*
</label>
<input
type='email'
value={smtpFrom}
onChange={(e) => setSmtpFrom(e.target.value)}
placeholder='[email protected]'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
/>
</div>
</div>
)}
{/* Resend配置 */}
{provider === 'resend' && (
<div className='space-y-4 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-700'>
<h4 className='text-sm font-medium text-gray-900 dark:text-white'>Resend </h4>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
Resend API Key *
</label>
<input
type='password'
value={resendApiKey}
onChange={(e) => setResendApiKey(e.target.value)}
placeholder='re_xxxxx'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
/>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
<a href='https://resend.com/api-keys' target='_blank' rel='noopener noreferrer' className='text-blue-600 hover:underline'>Resend </a>
</p>
</div>
<div>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'>
*
</label>
<input
type='email'
value={resendFrom}
onChange={(e) => setResendFrom(e.target.value)}
placeholder='[email protected]'
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white'
/>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
Resend
</p>
</div>
</div>
)}
{/* 测试邮件 */}
<div className='p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg'>
<h4 className='text-sm font-medium text-blue-900 dark:text-blue-100 mb-2'>
</h4>
<div className='flex flex-col sm:flex-row gap-2'>
<input
type='email'
value={testEmail}
onChange={(e) => setTestEmail(e.target.value)}
placeholder='输入测试邮箱地址'
className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-gray-900 dark:text-white text-sm'
/>
<button
onClick={handleTest}
disabled={isLoading('testEmail') || !testEmail}
className={`${buttonStyles.primary} whitespace-nowrap`}
>
{isLoading('testEmail') ? '发送中...' : '发送测试'}
</button>
</div>
</div>
{/* 保存按钮 */}
<div className='flex gap-3'>
<button
onClick={handleSave}
disabled={isLoading('saveEmail')}
className={buttonStyles.success}
>
{isLoading('saveEmail') ? '保存中...' : '保存配置'}
</button>
</div>
</div>
<AlertModal
isOpen={alertModal.isOpen}
onClose={hideAlert}
type={alertModal.type}
title={alertModal.title}
message={alertModal.message}
timer={alertModal.timer}
showConfirm={alertModal.showConfirm}
/>
</div>
);
};
// 求片列表组件
const MovieRequestsComponent = ({ config, refreshConfig }: { config: AdminConfig | null; refreshConfig: () => Promise<void> }) => {
const { alertModal, showAlert, hideAlert } = useAlertModal();
@@ -10322,6 +10703,7 @@ function AdminPageClient() {
dataMigration: false,
customAdFilter: false,
themeConfig: false,
emailConfig: false,
});
// 获取管理员配置
@@ -10712,6 +11094,18 @@ function AdminPageClient() {
<AIConfigComponent config={config} refreshConfig={fetchConfig} />
</CollapsibleTab>
{/* 邮件配置标签 */}
<CollapsibleTab
title='邮件配置'
icon={
<Mail size={20} className='text-gray-600 dark:text-gray-400' />
}
isExpanded={expandedTabs.emailConfig}
onToggle={() => toggleTab('emailConfig')}
>
<EmailConfigComponent config={config} refreshConfig={fetchConfig} />
</CollapsibleTab>
{/* 分类配置标签 */}
<CollapsibleTab
title='分类配置'